diff --git a/changelog.d/1058-bot-task-kanban.added.md b/changelog.d/1058-bot-task-kanban.added.md
new file mode 100644
index 000000000..bc88b25b4
--- /dev/null
+++ b/changelog.d/1058-bot-task-kanban.added.md
@@ -0,0 +1 @@
+Each Bot keeps a task list it updates from the conversation, and a board shows Not started, In progress and Done for that Bot, a child Bot, and — for a group admin — the tasks member Bots are tracking in the group ([#1058](https://github.com/ThinkFlowLab/FlyBot/issues/1058)).
diff --git a/core/client.js b/core/client.js
index dd8512a05..ed263df3e 100644
--- a/core/client.js
+++ b/core/client.js
@@ -664,6 +664,29 @@ var ZH = {
"Latest: {text}": "最新:{text}",
"{count} tool calls": "{count} 次工具调用",
"Untitled task": "未命名任务",
+ // Bot task board (#1058)
+ Tasks: "任务",
+ "Close tasks": "关闭任务",
+ "Task board": "任务板",
+ "Not started": "未开始",
+ "Groups you admin": "你管理的群",
+ "No tasks yet.": "还没有任务。",
+ "Nothing in this column.": "这一列是空的。",
+ Highest: "最高",
+ High: "较高",
+ Low: "较低",
+ Claimed: "已认领",
+ Blocked: "受阻",
+ Start: "开始",
+ Complete: "完成",
+ "Start task": "开始任务",
+ "Complete task": "完成任务",
+ "Add task": "添加任务",
+ "Task title": "任务标题",
+ Progress: "进度",
+ Group: "群",
+ "{percent}%": "{percent}%",
+ "{done}/{total}": "{done}/{total}",
"for {time}": "已 {time}",
"just started": "刚开始",
"Open {name}": "打开 {name}",
@@ -1968,8 +1991,8 @@ function bodyOf(def, paint, flat = false) {
return "";
}
}
-var eyeMarkup = ([cx, cy, w, h42, tilt]) => ``;
-var happyEyeMarkup = ([cx, cy, w, h42, tilt]) => ``;
+var eyeMarkup = ([cx, cy, w, h43, tilt]) => ``;
+var happyEyeMarkup = ([cx, cy, w, h43, 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%";
@@ -2665,29 +2688,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: h42, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) {
- if (!preview || typeof h42 !== "function") return null;
+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;
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 h42(
+ return h43(
"div",
{ className: "fs-preview", "data-flybot": at, role: "status" },
- h42("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"),
- preview.lead ? h42("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null,
- preview.people ? h42("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null,
- preview.expiresAt ? h42("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null,
- onJoin || onClear ? h42(
+ 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(
"div",
{ className: "fs-preview-foot", "data-flybot": child("foot") },
- showClear && onClear ? h42("button", {
+ showClear && onClear ? h43("button", {
type: "button",
className: "fs-btn fs-btn-sm",
"data-flybot": child("clear"),
disabled: !!busy,
onClick: onClear
}, clearLabel) : null,
- onJoin ? h42("button", {
+ onJoin ? h43("button", {
type: "button",
className: "fs-btn fs-btn-sm fs-btn-primary",
"data-flybot": child("join"),
@@ -3211,12 +3234,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 h42 = dt / steps;
+ 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) * h42;
- value.x += value.v * h42;
+ 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;
@@ -7521,23 +7544,497 @@ var INBOX_CSS = `
.bt-inbox-actions .bt-send{height:28px;font-size:12px}
`;
-// src/client/sidebar.js
+// src/shared/task-board.js
+var COLUMNS = ["not-started", "in-progress", "done"];
+var PRIORITIES = ["urgent", "high", "normal", "low"];
+var COLUMN_OF = {
+ open: "not-started",
+ claimed: "not-started",
+ working: "in-progress",
+ blocked: "in-progress",
+ "result-ready": "in-progress",
+ accepted: "done",
+ cancelled: "done",
+ failed: "done"
+};
+function columnOf(task) {
+ return COLUMN_OF[task?.status] ?? "not-started";
+}
+function priorityOf(task) {
+ return PRIORITIES.includes(task?.priority) ? task.priority : "normal";
+}
+function progressOf(task) {
+ const value = task?.progress;
+ if (typeof value !== "number" || !Number.isFinite(value)) return columnOf(task) === "done" && task?.status !== "cancelled" && task?.status !== "failed" ? 100 : 0;
+ return Math.max(0, Math.min(100, Math.round(value)));
+}
+
+// src/client/kanban.js
var import_react13 = require("react");
+var POLL_MS3 = 15e3;
+function BoardIcon() {
+ return (0, import_react13.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 })
+ );
+}
+function shortId(id) {
+ const hex = String(id ?? "").replace(/^task-/, "");
+ return `#${hex.slice(-6)}`;
+}
+function boardColumns(tasks) {
+ const columns = { "not-started": [], "in-progress": [], done: [] };
+ for (const task of tasks ?? []) {
+ const column = columnOf(task);
+ (columns[column] ?? columns["not-started"]).push(task);
+ }
+ for (const list of Object.values(columns)) list.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
+ return columns;
+}
+function adminOf(roster, room2) {
+ const me = roster?.self?.memberId;
+ if (!me) return room2?.origin !== "face-to-face";
+ const human = (room2?.participants ?? []).find((person) => person.kind === "human" && person.id === me && person.leftSeq === void 0);
+ if (human?.role === "admin") return true;
+ const admin = roster?.byId?.[room2?.admin];
+ return Boolean(admin && !admin.remote && admin.ownerId === me);
+}
+function boardRail(roster) {
+ const me = roster?.self?.memberId;
+ const bots = (roster?.bots ?? []).filter((bot) => bot && !bot.remote && (!me || !bot.ownerId || bot.ownerId === me));
+ const ids = new Set(bots.map((bot) => bot.id));
+ const children = /* @__PURE__ */ new Map();
+ for (const bot of bots) {
+ const parent = bot.createdBy && ids.has(bot.createdBy) ? bot.createdBy : "";
+ const list = children.get(parent) ?? [];
+ list.push(bot);
+ children.set(parent, list);
+ }
+ for (const list of children.values()) list.sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0) || String(a.name).localeCompare(String(b.name)));
+ const rows = [];
+ const walk = (parent, depth) => {
+ for (const bot of children.get(parent) ?? []) {
+ rows.push({ kind: "bot", id: bot.id, name: bot.name, depth, bot });
+ walk(bot.id, depth + 1);
+ }
+ };
+ 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 }));
+ return { bots: rows, groups };
+}
+function canMoveTask(roster, task) {
+ const me = roster?.self?.memberId;
+ if (!me) return true;
+ const bot = roster?.byId?.[task?.ownerBotId];
+ return Boolean(bot && !bot.remote && (!bot.ownerId || bot.ownerId === me));
+}
+function priorityLabel(priority) {
+ if (priority === "urgent") return t("Highest");
+ if (priority === "high") return t("High");
+ if (priority === "low") return t("Low");
+ return t("Normal");
+}
+function statusLabel(status) {
+ switch (status) {
+ case "claimed":
+ return t("Claimed");
+ case "working":
+ return t("In progress");
+ case "blocked":
+ return t("Blocked");
+ case "result-ready":
+ return t("Result ready");
+ case "accepted":
+ return t("Done");
+ case "cancelled":
+ return t("Cancelled");
+ case "failed":
+ return t("Failed");
+ default:
+ return t("Not started");
+ }
+}
+function dropLabel(column) {
+ if (column === "in-progress") return t("Start task");
+ if (column === "done") return t("Complete task");
+ return t("Not started");
+}
+function columnTitle(column) {
+ if (column === "in-progress") return t("In progress");
+ if (column === "done") return t("Done");
+ return t("Not started");
+}
+function botName(roster, task) {
+ return roster?.byId?.[task.ownerBotId]?.name || task.ownerBotName || "";
+}
+function lanesOf(tasks, roster) {
+ const order = [];
+ const map = /* @__PURE__ */ new Map();
+ for (const task of tasks) {
+ const id = task.ownerBotId ?? "";
+ if (!map.has(id)) {
+ map.set(id, []);
+ order.push(id);
+ }
+ map.get(id).push(task);
+ }
+ 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 key = scope?.kind === "group" ? `group:${scope.roomId}` : "own";
+ (0, import_react13.useEffect)(() => {
+ let stopped = false;
+ const load = () => {
+ const job = scope?.kind === "group" ? Promise.resolve(actions?.tasks?.({ roomId: scope.roomId })) : Promise.resolve(actions?.tasks?.());
+ void job.then((list) => {
+ if (stopped || !Array.isArray(list)) return;
+ if (scope?.kind === "group") setGroupTasks(list);
+ else setOwn(list);
+ }).catch(() => void 0);
+ };
+ load();
+ const timer = setInterval(load, POLL_MS3);
+ return () => {
+ stopped = true;
+ clearInterval(timer);
+ };
+ }, [key]);
+ const tasks = scope?.kind === "group" ? groupTasks : own.filter((task) => !scope?.botId || task.ownerBotId === scope.botId);
+ const replace = (task) => {
+ const write = (list) => list.some((item) => item.id === task.id) ? list.map((item) => item.id === task.id ? task : item) : [...list, task];
+ if (scope?.kind === "group") setGroupTasks(write);
+ else setOwn(write);
+ };
+ return { own, tasks, replace };
+}
+function RailButton({ row, selected, meta, onSelect }) {
+ return (0, import_react13.createElement)(
+ "button",
+ {
+ type: "button",
+ className: "bt-board-rail-item",
+ "data-kind": row.kind,
+ "data-id": row.id,
+ "data-selected": selected || void 0,
+ "aria-current": selected ? "true" : void 0,
+ 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
+ );
+}
+function BoardCard({ task, roster, movable, onMove, onDrag }) {
+ const column = columnOf(task);
+ const priority = priorityOf(task);
+ const progress = progressOf(task);
+ const who = botName(roster, task);
+ return (0, import_react13.createElement)(
+ "article",
+ {
+ className: "bt-board-card",
+ "data-task": task.id,
+ "data-column": column,
+ "data-priority": priority,
+ draggable: movable || void 0,
+ onDragStart: (event) => {
+ event.dataTransfer?.setData("text/plain", task.id);
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
+ onDrag?.(task.id);
+ },
+ onDragEnd: () => onDrag?.(null)
+ },
+ (0, import_react13.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_react13.createElement)("span", { className: "bt-board-title" }, task.title || t("Untitled task")),
+ task.note ? (0, import_react13.createElement)("span", { className: "bt-board-note" }, task.note) : null,
+ (0, import_react13.createElement)(
+ "span",
+ { className: "bt-board-foot" },
+ who ? (0, import_react13.createElement)("span", { className: "bt-board-who" }, who) : null,
+ (0, import_react13.createElement)(
+ "span",
+ { className: "bt-board-progress" },
+ (0, import_react13.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_react13.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
+ );
+}
+function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag }) {
+ return (0, import_react13.createElement)(
+ "section",
+ {
+ className: "bt-board-column",
+ "data-column": column,
+ "aria-label": columnTitle(column),
+ onDragOver: (event) => {
+ if (!dragging) return;
+ event.preventDefault();
+ },
+ onDrop: (event) => {
+ event.preventDefault();
+ const id = event.dataTransfer?.getData("text/plain");
+ if (id) onMove?.(id, column);
+ onDrag?.(null);
+ }
+ },
+ (0, import_react13.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))
+ ),
+ 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.")) : lanes ? lanesOf(tasks, roster).map((lane) => (0, import_react13.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, task, roster, movable: canMoveTask(roster, task), onMove, onDrag }))
+ )) : tasks.map((task) => (0, import_react13.createElement)(BoardCard, { key: task.id, task, roster, movable: canMoveTask(roster, task), onMove, onDrag }))
+ );
+}
+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)(
+ "div",
+ { className: "bt-board-body" },
+ (0, import_react13.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) => {
+ const count = counts2?.[row.id] ?? { done: 0, total: 0 };
+ return (0, import_react13.createElement)(RailButton, {
+ key: row.id,
+ row,
+ selected: row.id === selectedBot,
+ onSelect,
+ 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 }))
+ ),
+ (0, import_react13.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)(
+ "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"))
+ ) : null,
+ (0, import_react13.createElement)(
+ "div",
+ { className: "bt-board-columns" },
+ ["not-started", "in-progress", "done"].map((column) => (0, import_react13.createElement)(BoardColumn, {
+ key: column,
+ column,
+ tasks: columns[column],
+ roster,
+ lanes,
+ dragging,
+ onMove,
+ onDrag
+ }))
+ )
+ )
+ );
+}
+function KanbanRow({ wide, useUi, actions }) {
+ const open = useUi ? useUi((value) => Boolean(value.board)) : false;
+ const label = t("Tasks");
+ return (0, import_react13.createElement)(
+ "button",
+ {
+ type: "button",
+ className: "bt-row bt-board-row",
+ "aria-label": label,
+ title: wide ? void 0 : label,
+ "aria-haspopup": "dialog",
+ "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
+ );
+}
+function scopeName(roster, scope) {
+ if (scope?.kind === "group") return roster.roomsById?.[scope.roomId]?.name || t("Group");
+ if (scope?.kind === "bot") return roster.byId?.[scope.botId]?.name || "";
+ return "";
+}
+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 { own, tasks, replace } = useBoardTasks(actions, scope);
+ const close = (0, import_react13.useCallback)(() => actions.closeBoard(), []);
+ useEscape(close);
+ const counts2 = {};
+ for (const task of own) {
+ const id = task.ownerBotId;
+ const bucket = counts2[id] ?? (counts2[id] = { done: 0, total: 0 });
+ if (task.status === "cancelled" || task.status === "failed") continue;
+ bucket.total += 1;
+ if (columnOf(task) === "done") bucket.done += 1;
+ }
+ const move = async (id, column) => {
+ const task = tasks.find((item) => item.id === id) ?? own.find((item) => item.id === id);
+ if (!task || !canMoveTask(roster, task) || columnOf(task) === column) return;
+ setError("");
+ try {
+ replace(await actions.taskUpdate({ id, column }));
+ } catch (failure) {
+ setError(failure?.message ?? String(failure));
+ }
+ };
+ const create = async (title) => {
+ const text = String(title ?? "").trim();
+ if (!text || scope?.kind !== "bot") return;
+ setError("");
+ try {
+ replace(await actions.taskCreate({ botId: scope.botId, title: text, column: "not-started" }));
+ setDraft("");
+ } catch (failure) {
+ setError(failure?.message ?? String(failure));
+ }
+ };
+ const name = scopeName(roster, scope);
+ return (0, import_react13.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)(
+ "div",
+ { className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" },
+ (0, import_react13.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_react13.createElement)(BoardView, {
+ roster,
+ tasks,
+ rail,
+ scope,
+ counts: counts2,
+ dragging,
+ error,
+ draft,
+ onDraft: setDraft,
+ onSelect: setScope,
+ onMove: move,
+ onDrag: setDragging,
+ onCreate: create
+ })
+ )
+ );
+}
+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)}
+.bt-board-mark svg{width:18px;height:18px}
+.bt-board-label{flex:1 1 auto;min-width:0;font-size:14px;line-height:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.bt-mem.bt-board{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))}
+.bt-board-body{flex:1 1 auto;min-height:0;display:flex;gap:0}
+.bt-board-rail{flex:none;width:232px;overflow:auto;border-right:.5px solid var(--bt-line);padding:8px 8px 16px;display:flex;flex-direction:column;gap:2px}
+.bt-board-rail-head{margin:10px 8px 4px;font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3)}
+.bt-board-rail-item{display:flex;align-items:center;gap:8px;width:100%;min-height:36px;padding:6px 10px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer}
+.bt-board-rail-item:hover{background:var(--bt-hover)}
+.bt-board-rail-item[data-selected]{background:var(--bt-active,var(--bt-hover))}
+.bt-board-rail-name{flex:1 1 auto;min-width:0;font-size:13.5px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.bt-board-rail-meta{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:10px;padding:12px 14px 16px}
+.bt-board-add{display:flex;gap:8px}
+.bt-board-add input{flex:1 1 auto;min-width:0;height:32px;padding:0 10px;border:.5px solid var(--bt-line-2);border-radius:10px;background:var(--bt-card,var(--bt-main));color:var(--bt-ink);font:inherit}
+.bt-board-columns{flex:1 1 auto;min-height:0;display:flex;gap:10px}
+.bt-board-column{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:8px;padding:8px;border-radius:14px;background:var(--bt-hover);overflow:auto}
+.bt-board-column-head{display:flex;align-items:center;gap:6px;margin:0;padding:2px 4px;font-size:13px;line-height:18px;font-weight:600;color:var(--bt-ink-2)}
+.bt-board-count{font-weight:500;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-drop{margin:0;padding:18px 8px;border:1px dashed var(--bt-line-2);border-radius:12px;text-align:center;font-size:13px;line-height:18px;color:var(--bt-ink-3)}
+.bt-board-empty{margin:0;padding:8px 4px;font-size:13px;line-height:18px;color:var(--bt-ink-3)}
+.bt-board-error{margin:0;font-size:13px;line-height:18px;color:#c21d2e}
+.bt-board-lane{display:flex;flex-direction:column;gap:6px}
+.bt-board-lane-name{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3);padding:0 2px}
+.bt-board-card{display:flex;flex-direction:column;gap:6px;padding:10px;border:.5px solid var(--bt-line-2);border-radius:12px;background:var(--bt-card,var(--bt-main));cursor:grab}
+.bt-board-card-top{display:flex;align-items:center;gap:6px;min-width:0}
+.bt-board-id{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-priority{flex:none;height:18px;padding:0 6px;border-radius:999px;font-size:11px;line-height:18px;font-weight:600}
+.bt-board-priority[data-priority=urgent]{background:rgba(224,33,53,.12);color:#c21d2e}
+.bt-board-priority[data-priority=high]{background:rgba(217,119,6,.14);color:#b45309}
+.bt-board-priority[data-priority=normal]{background:color-mix(in srgb,var(--bt-accent) 14%,transparent);color:color-mix(in srgb,var(--bt-accent) 80%,var(--bt-ink))}
+.bt-board-priority[data-priority=low]{background:color-mix(in srgb,var(--bt-green) 16%,transparent);color:color-mix(in srgb,var(--bt-green) 70%,var(--bt-ink))}
+.bt-board-status{margin-left:auto;font-size:11.5px;line-height:16px;color:var(--bt-ink-3);white-space:nowrap}
+.bt-board-title{font-size:13.5px;line-height:19px;color:var(--bt-ink)}
+.bt-board-note{font-size:12px;line-height:16px;color:var(--bt-ink-2)}
+.bt-board-foot{display:flex;align-items:center;gap:8px}
+.bt-board-who{flex:none;max-width:40%;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.bt-board-progress{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:6px}
+.bt-board-progress-bar{flex:1 1 auto;height:4px;border-radius:999px;background:var(--bt-line);overflow:hidden}
+.bt-board-progress-bar>span{display:block;height:100%;background:var(--bt-accent)}
+.bt-board-percent{flex:none;font-size:11.5px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-card .bt-soft{align-self:flex-start}
+@media (max-width:800px){
+ .bt-mem.bt-board{width:min(100vw,calc(100vw - 16px));height:100%}
+ .bt-board-body{flex-direction:column}
+ .bt-board-rail{width:auto;max-height:180px;border-right:0;border-bottom:.5px solid var(--bt-line)}
+ .bt-board-columns{overflow:auto}
+ .bt-board-column{min-width:220px}
+}
+@media (max-width:640px){
+ .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_react14 = 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_react13.useEffect)(() => {
+ (0, import_react14.useEffect)(() => {
if (active && marked) actions.clearUnread(id);
}, [active, marked]);
}
-var PinMark = () => (0, import_react13.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react13.createElement)(PinMarkIcon));
+var PinMark = () => (0, import_react14.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react14.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_react13.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`);
+var workLine = (lead, tool, more) => (0, import_react14.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);
@@ -7568,7 +8065,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_react13.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react13.createElement)(
+ return (0, import_react14.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react14.createElement)(
"button",
{
type: "button",
@@ -7580,15 +8077,15 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
onClick: () => actions.openSession(id),
onContextMenu
},
- (0, import_react13.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
- (0, import_react13.createElement)("span", { className: "bt-tile-name" }, shown),
+ (0, import_react14.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
+ (0, import_react14.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_react13.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null,
- bot.role ? (0, import_react13.createElement)("span", { className: "bt-chip" }, bot.role) : null
+ 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
));
}
- const unreadMark = wide && waiting && !active ? (0, import_react13.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react13.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null;
- const row = (0, import_react13.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)(
"button",
{
type: "button",
@@ -7601,24 +8098,24 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
onClick: () => actions.openSession(id),
onContextMenu
},
- (0, import_react13.createElement)(BotAvatar, { bot, size: 36, state, main }),
- pinned && !wide ? (0, import_react13.createElement)(PinMark) : null,
- wide ? (0, import_react13.createElement)(
+ (0, import_react14.createElement)(BotAvatar, { bot, size: 36, state, main }),
+ pinned && !wide ? (0, import_react14.createElement)(PinMark) : null,
+ wide ? (0, import_react14.createElement)(
"span",
{ className: "bt-row-body" },
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"span",
{ className: "bt-row-name" },
- (0, import_react13.createElement)("span", { className: "bt-row-title" }, shown),
- bot.remote ? (0, import_react13.createElement)(RemoteMark, { remote: bot.remote }) : null,
- pinned ? (0, import_react13.createElement)(PinMark) : null
+ (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
),
- secondLine ? (0, import_react13.createElement)("span", { className: "bt-row-preview" }, secondLine) : null
+ secondLine ? (0, import_react14.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_react13.createElement)("button", {
+ const settings = wide ? (0, import_react14.createElement)("button", {
type: "button",
className: "bt-row-settings",
"aria-label": settingsLabel,
@@ -7629,8 +8126,8 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
keepSidebarStill(event.currentTarget);
actions.openSettings("bots", id);
}
- }, (0, import_react13.createElement)(GearIcon)) : null;
- if (settings) return (0, import_react13.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
+ }, (0, import_react14.createElement)(GearIcon)) : null;
+ if (settings) return (0, import_react14.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
return row;
}
function keepSidebarStill(node) {
@@ -7643,14 +8140,14 @@ function keepSidebarStill(node) {
if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset));
}
function RemoteMark({ remote }) {
- return (0, import_react13.createElement)(
+ return (0, import_react14.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_react13.createElement)(
+ (0, import_react14.createElement)(
"svg",
{ width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
- (0, import_react13.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
- (0, import_react13.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
+ (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" })
)
);
}
@@ -7662,15 +8159,15 @@ function isPastFaceRoom(room2) {
return status === "candidate" && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now();
}
function FaceMark() {
- return (0, import_react13.createElement)(
+ return (0, import_react14.createElement)(
"span",
{ className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") },
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"svg",
{ width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
- (0, import_react13.createElement)("circle", { cx: 9, cy: 8, r: 3 }),
- (0, import_react13.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }),
- (0, import_react13.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" })
+ (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" })
)
);
}
@@ -7695,7 +8192,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_react13.createElement)(
+ return (0, import_react14.createElement)(
"button",
{
type: "button",
@@ -7711,20 +8208,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus,
actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY });
}
},
- (0, import_react13.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
- pinned && !wide ? (0, import_react13.createElement)(PinMark) : null,
- wide ? (0, import_react13.createElement)(
+ (0, import_react14.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
+ pinned && !wide ? (0, import_react14.createElement)(PinMark) : null,
+ wide ? (0, import_react14.createElement)(
"span",
{ className: "bt-row-body" },
- (0, import_react13.createElement)("span", { className: "bt-row-name" }, (0, import_react13.createElement)("span", { className: "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react13.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react13.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react13.createElement)(PinMark) : null),
- workingLine ? (0, import_react13.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react13.createElement)("span", { className: "bt-row-preview" }, preview) : null
+ (0, import_react14.createElement)("span", { className: "bt-row-name" }, (0, import_react14.createElement)("span", { className: "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
) : null,
- wide && alert && !active ? (0, import_react13.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react13.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
);
}
function AgentSessionRow({ session, agent, actions, renaming, index }) {
- const input = (0, import_react13.useRef)(null);
- (0, import_react13.useLayoutEffect)(() => {
+ const input = (0, import_react14.useRef)(null);
+ (0, import_react14.useLayoutEffect)(() => {
if (!renaming) return;
input.current?.focus();
input.current?.select();
@@ -7745,10 +8242,10 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
}
})();
};
- return (0, import_react13.createElement)(
+ return (0, import_react14.createElement)(
"div",
{ className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } },
- (0, import_react13.createElement)("input", {
+ (0, import_react14.createElement)("input", {
ref: input,
defaultValue: session.title,
"aria-label": t("Rename"),
@@ -7761,7 +8258,7 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
})
);
}
- return (0, import_react13.createElement)(
+ return (0, import_react14.createElement)(
"button",
{
type: "button",
@@ -7775,8 +8272,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_react13.createElement)("span", { className: "bt-agent-sub-title" }, session.title),
- (0, import_react13.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) : "")
);
}
function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) {
@@ -7811,10 +8308,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_react13.createElement)(
+ return (0, import_react14.createElement)(
"div",
{ className: "bt-agent" },
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"button",
{
type: "button",
@@ -7829,30 +8326,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY });
}
},
- (0, import_react13.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react13.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })),
- wide ? (0, import_react13.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)(
"span",
{ className: "bt-row-body" },
- (0, import_react13.createElement)("span", { className: "bt-row-name" }, "DSH Agent"),
- preview ? (0, import_react13.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
) : null,
- wide && unread && !active ? (0, import_react13.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
+ wide && unread && !active ? (0, import_react14.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
),
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"div",
{ className: "bt-agent-sub", "data-open": open || void 0 },
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"div",
{ className: "bt-agent-sub-in" },
- (0, import_react13.createElement)(
+ (0, import_react14.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_react13.createElement)(PlusIcon),
- (0, import_react13.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"))
),
- sessions.map((session, index) => (0, import_react13.createElement)(AgentSessionRow, {
+ sessions.map((session, index) => (0, import_react14.createElement)(AgentSessionRow, {
key: session.id,
session,
agent,
@@ -7866,8 +8363,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
}
var GLIDE = springEasing(1e3, 63);
function useListGlide(listRef, layout, key) {
- const tops = (0, import_react13.useRef)({ layout, map: /* @__PURE__ */ new Map() });
- (0, import_react13.useLayoutEffect)(() => {
+ const tops = (0, import_react14.useRef)({ layout, map: /* @__PURE__ */ new Map() });
+ (0, import_react14.useLayoutEffect)(() => {
const list = listRef.current;
if (!list) return;
const next = /* @__PURE__ */ new Map();
@@ -7884,7 +8381,7 @@ function useListGlide(listRef, layout, key) {
tops.current = { layout, map: next };
}, [layout, key]);
}
-var NodeActions = class extends import_react13.Component {
+var NodeActions = class extends import_react14.Component {
constructor(props) {
super(props);
this.state = { failed: false };
@@ -7896,32 +8393,32 @@ var NodeActions = class extends import_react13.Component {
console.error("flybot: nodeActions failed", error);
}
render() {
- return this.state.failed ? null : (0, import_react13.createElement)(this.props.component, { bot: this.props.bot });
+ return this.state.failed ? null : (0, import_react14.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_react13.createElement)(
- import_react13.Fragment,
+ return (0, import_react14.createElement)(
+ import_react14.Fragment,
null,
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"div",
{ className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } },
- children.length ? (0, import_react13.createElement)(
+ children.length ? (0, import_react14.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_react13.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾")
- ) : (0, import_react13.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }),
- (0, import_react13.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }),
- typeof actions === "function" ? (0, import_react13.createElement)(NodeActions, { component: actions, bot }) : null
+ (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
),
- closed ? null : children.map((child) => (0, import_react13.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions }))
+ closed ? null : children.map((child) => (0, import_react14.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_react13.useRef)(null);
+ const listRef = (0, import_react14.useRef)(null);
const roster = useRoster((value) => value);
const markedUnread = useUi((value) => value.unread);
props.useRegistry((value) => value);
@@ -7930,7 +8427,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_react13.useMemo)(() => {
+ const order = (0, import_react14.useMemo)(() => {
const stamps = JSON.parse(updated);
const entries2 = [
// Own Bots, friends' Bots, groups and face rooms share one list. A remote chat
@@ -7945,7 +8442,7 @@ function TeamSidebar(props) {
}, [roster, updated]);
const main = roster.byId[roster.mainBotId];
const structure = useSidebarStructure();
- const [collapsed, setCollapsed] = (0, import_react13.useState)({});
+ const [collapsed, setCollapsed] = (0, import_react14.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;
@@ -7959,41 +8456,42 @@ function TeamSidebar(props) {
event.preventDefault();
actions.openPalette(event.key);
};
- return (0, import_react13.createElement)(
+ return (0, import_react14.createElement)(
"div",
{ className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch },
- wide ? (0, import_react13.createElement)(
+ wide ? (0, import_react14.createElement)(
"div",
{ className: "bt-side-top" },
- (0, import_react13.createElement)("span", { className: "bt-brand" }, (0, import_react13.createElement)(BrandMark, { size: 22 }), (0, import_react13.createElement)(BrandName)),
- (0, import_react13.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
- (0, import_react13.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react13.createElement)(SearchIcon)),
- (0, import_react13.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react13.createElement)(PlusIcon))
- ) : (0, import_react13.createElement)(
- import_react13.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,
null,
- (0, import_react13.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
- (0, import_react13.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react13.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))
),
- wide && roster.readOnly ? (0, import_react13.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
- (0, import_react13.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
- (0, import_react13.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)(
"div",
{ ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") },
- tree ? (0, import_react13.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react13.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react13.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react13.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_react13.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react13.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react13.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
- wide ? (0, import_react13.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react13.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null
+ 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
)
);
}
function UpdateButton({ useUpdate, actions }) {
const update = useUpdate((value) => value);
- const [open, setOpen] = (0, import_react13.useState)(false);
- const [box, setBox] = (0, import_react13.useState)(null);
- const [error, setError] = (0, import_react13.useState)("");
- const [dshError, setDshError] = (0, import_react13.useState)("");
- const button = (0, import_react13.useRef)(null);
- (0, import_react13.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)(() => {
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;
@@ -8036,10 +8534,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_react13.createElement)(
- import_react13.Fragment,
+ return (0, import_react14.createElement)(
+ import_react14.Fragment,
null,
- (0, import_react13.createElement)("button", {
+ (0, import_react14.createElement)("button", {
ref: button,
type: "button",
className: "bt-round bt-update",
@@ -8049,21 +8547,21 @@ function UpdateButton({ useUpdate, actions }) {
"aria-haspopup": "dialog",
"aria-expanded": open,
onClick: toggle
- }, (0, import_react13.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react13.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
- open && box ? (0, import_react_dom3.createPortal)((0, import_react13.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)(
"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_react13.createElement)(
- import_react13.Fragment,
+ update?.available || phase !== "idle" ? (0, import_react14.createElement)(
+ import_react14.Fragment,
null,
- (0, import_react13.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })),
- (0, import_react13.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_react13.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
- (0, import_react13.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)(
"div",
{ className: "bt-update-actions" },
- update.notesUrl ? (0, import_react13.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
- phase === "installed" ? null : (0, import_react13.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", {
type: "button",
className: "bt-update-go",
disabled: phase === "installing",
@@ -8071,17 +8569,17 @@ function UpdateButton({ useUpdate, actions }) {
}, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update"))
)
) : null,
- dshRow ? (0, import_react13.createElement)(
+ dshRow ? (0, import_react14.createElement)(
"div",
{ className: "bt-update-dsh" },
- (0, import_react13.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
- (0, import_react13.createElement)("div", { className: "bt-update-text" }, dshText),
- dshMessage ? (0, import_react13.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
- (0, import_react13.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)(
"div",
{ className: "bt-update-actions" },
- dsh.kind === "app" ? dsh.available ? (0, import_react13.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_react13.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", {
type: "button",
className: "bt-update-go",
disabled: dshPhase === "installing",
@@ -8094,23 +8592,23 @@ function UpdateButton({ useUpdate, actions }) {
}
function ConnectApps({ wide, actions }) {
if (!wide) return null;
- return (0, import_react13.createElement)(
+ return (0, import_react14.createElement)(
"span",
{ className: "bt-connect-wrap" },
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"button",
{ type: "button", className: "bt-connect bt-connect-foot", onClick: () => actions.openConnectors() },
- (0, import_react13.createElement)(PlugIcon),
+ (0, import_react14.createElement)(PlugIcon),
t("Connect apps")
)
);
}
function YouAvatar() {
- return (0, import_react13.createElement)(
+ return (0, import_react14.createElement)(
"span",
{ className: "bt-you-wrap" },
- (0, import_react13.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react13.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })),
- (0, import_react13.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"))
);
}
var openShellSettings = null;
@@ -8142,7 +8640,7 @@ function openHarnessOnboarding(id) {
var MODELS_STEP_ID = "flybot-models";
var MODELS_STEP_ORDER = 1e3;
function ModelsSettingsStep({ explicit, complete, openSection }) {
- (0, import_react13.useLayoutEffect)(() => {
+ (0, import_react14.useLayoutEffect)(() => {
if (explicit) openSection("models");
complete();
}, []);
@@ -8180,32 +8678,32 @@ function pressSettingsShortcut() {
}));
}
function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) {
- const [open, setOpen] = (0, import_react13.useState)(false);
- (0, import_react13.useLayoutEffect)(() => {
+ const [open, setOpen] = (0, import_react14.useState)(false);
+ (0, import_react14.useLayoutEffect)(() => {
if (!seat) return void 0;
actions.seatAccount(true);
return () => actions.seatAccount(false);
}, [seat]);
- const [box, setBox] = (0, import_react13.useState)(null);
- const button = (0, import_react13.useRef)(null);
- (0, import_react13.useEffect)(() => {
+ const [box, setBox] = (0, import_react14.useState)(null);
+ const button = (0, import_react14.useRef)(null);
+ (0, import_react14.useEffect)(() => {
if (settingsOpen) setOpen(false);
}, [settingsOpen]);
- (0, import_react13.useEffect)(() => {
+ (0, import_react14.useEffect)(() => {
if (!openSettings) return void 0;
openShellSettings = openSettings;
return () => {
if (openShellSettings === openSettings) openShellSettings = null;
};
}, [openSettings]);
- (0, import_react13.useEffect)(() => {
+ (0, import_react14.useEffect)(() => {
if (!openOnboarding) return void 0;
openShellOnboarding = openOnboarding;
return () => {
if (openShellOnboarding === openOnboarding) openShellOnboarding = null;
};
}, [openOnboarding]);
- (0, import_react13.useEffect)(() => {
+ (0, import_react14.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;
@@ -8242,23 +8740,23 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi
[t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")]
]
];
- return (0, import_react13.createElement)(
- import_react13.Fragment,
+ return (0, import_react14.createElement)(
+ import_react14.Fragment,
null,
- (0, import_react13.createElement)(
+ (0, import_react14.createElement)(
"button",
{ ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle },
- (0, import_react13.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react13.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 }))
),
- open && box ? (0, import_react_dom3.createPortal)((0, import_react13.createElement)(
+ open && box ? (0, import_react_dom3.createPortal)((0, import_react14.createElement)(
"div",
{ className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box },
- (0, import_react13.createElement)(FlyBotAccountHeader, { actions }),
- groups.map((group, index) => (0, import_react13.createElement)(
- import_react13.Fragment,
+ (0, import_react14.createElement)(FlyBotAccountHeader, { actions }),
+ groups.map((group, index) => (0, import_react14.createElement)(
+ import_react14.Fragment,
{ key: index },
- index > 0 ? (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
- group.map(([label, Icon, run, hint]) => (0, import_react13.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", {
key: label,
type: "button",
role: "menuitem",
@@ -8266,7 +8764,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi
setOpen(false);
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react13.createElement)(Icon), (0, import_react13.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react13.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))
))
), document.body) : null
);
@@ -8276,8 +8774,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_react13.useRef)(null);
- (0, import_react13.useEffect)(() => {
+ const lastBot = (0, import_react14.useRef)(null);
+ (0, import_react14.useEffect)(() => {
if (!roster.ready || !roster.mainBotId) return void 0;
const [id, blank3] = mainView.split("|");
const bot = id === "" ? void 0 : roster.byId[id];
@@ -8298,16 +8796,16 @@ function useLandOnMainBot(roster, useSessions, actions) {
function FooterAccount(props) {
const seated = props.useUi((value) => value.accountSeated === true);
if (seated) return null;
- return (0, import_react13.createElement)("span", { className: "bt-foot-account" }, (0, import_react13.createElement)(AccountLauncher, { ...props, seat: false }));
+ return (0, import_react14.createElement)("span", { className: "bt-foot-account" }, (0, import_react14.createElement)(AccountLauncher, { ...props, seat: false }));
}
function BotReturn({ wide, useSurface, actions }) {
const mode = useSurface((value) => value.mode);
if (mode !== "agent") return null;
- return (0, import_react13.createElement)(
+ return (0, import_react14.createElement)(
"button",
{ type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") },
- (0, import_react13.createElement)(FlyBotMark, { size: 20 }),
- wide ? (0, import_react13.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
+ (0, import_react14.createElement)(FlyBotMark, { size: 20 }),
+ wide ? (0, import_react14.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
);
}
var RETURN_CSS = `
@@ -8321,10 +8819,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_react13.createElement)("span", { className: "bt-brand-mark" }, (0, import_react13.createElement)(FlyBotMark, { size }));
+ return (0, import_react14.createElement)("span", { className: "bt-brand-mark" }, (0, import_react14.createElement)(FlyBotMark, { size }));
}
function BrandName() {
- return (0, import_react13.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot"));
+ return (0, import_react14.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot"));
}
var SIDEBAR_CSS = `
.bt-side-top .bt-brand{cursor:default;padding-left:6px}
@@ -8398,15 +8896,15 @@ var SIDEBAR_CSS = `
`;
// src/client/header.js
-var import_react14 = require("react");
+var import_react15 = require("react");
var import_react_dom4 = require("react-dom");
-var GoArrow = () => (0, import_react14.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_react14.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 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_react14.useState)(false);
- (0, import_react14.useEffect)(() => {
+ const [shown, setShown] = (0, import_react15.useState)(false);
+ (0, import_react15.useEffect)(() => {
const left = seen + MEMORY_NOTICE_MS - Date.now();
setShown(seen > 0 && left > 0);
if (seen === 0 || left <= 0) return void 0;
@@ -8416,16 +8914,16 @@ function useMemoryNotice(ids, useMemoryNews) {
return shown;
}
function MemoryNotice({ shown }) {
- const inner = (0, import_react14.useRef)(null);
- const [width, setWidth] = (0, import_react14.useState)(0);
+ const inner = (0, import_react15.useRef)(null);
+ const [width, setWidth] = (0, import_react15.useState)(0);
const label = t("Memory updated");
- (0, import_react14.useLayoutEffect)(() => {
+ (0, import_react15.useLayoutEffect)(() => {
if (shown) setWidth(inner.current?.scrollWidth ?? 0);
}, [label, shown]);
- return (0, import_react14.createElement)(
+ return (0, import_react15.createElement)(
"span",
{ className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } },
- (0, import_react14.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react14.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react14.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)
);
}
function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) {
@@ -8441,7 +8939,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_react14.useEffect)(() => {
+ (0, import_react15.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");
@@ -8458,10 +8956,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_react14.createElement)(
+ return (0, import_react15.createElement)(
"div",
{ className: "bt-head" },
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)(
"button",
{
type: "button",
@@ -8471,19 +8969,19 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe
"aria-label": memoryNotice ? t("Memory updated. Open {name}'s memory", { name: roster.byId[changed2]?.name ?? name }) : t("View conversation details"),
onClick: () => memoryNotice ? actions.openMemory(changed2) : actions.toggleDetails(sessionId)
},
- bot ? (0, import_react14.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react14.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null,
- (0, import_react14.createElement)("span", { className: "bt-pill-name" }, name || title || t("New chat")),
- (0, import_react14.createElement)(MemoryNotice, { shown: memoryNotice }),
- (0, import_react14.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react14.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: "bt-pill-name" }, 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_react14.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
- (0, import_react14.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }),
- (0, import_react14.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 })
);
}
function useTranscriptView() {
- const [view, setView] = (0, import_react14.useState)(null);
- (0, import_react14.useEffect)(() => {
+ const [view, setView] = (0, import_react15.useState)(null);
+ (0, import_react15.useEffect)(() => {
let frame = 0;
let watched = null;
const observer = new MutationObserver(() => schedule());
@@ -8548,22 +9046,22 @@ function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnCl
});
const view = useTranscriptView();
const unpinned = view?.unpinned === true;
- const [release, setRelease] = (0, import_react14.useState)({ base: null, dismissed: false });
- const [above, setAbove] = (0, import_react14.useState)({ seen: false, dismissed: false });
- (0, import_react14.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)(() => {
setRelease({ base: unpinned ? replies : null, dismissed: false });
}, [unpinned]);
- (0, import_react14.useEffect)(() => {
+ (0, import_react15.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_react14.useEffect)(() => {
+ (0, import_react15.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_react14.useEffect)(() => {
+ (0, import_react15.useEffect)(() => {
const scroller = document.querySelector("[data-conversation-scroll]");
if (!scroller) return void 0;
scroller.toggleAttribute("data-bt-news", showDown);
@@ -8571,16 +9069,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_react14.createElement)(
+ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react15.createElement)(
"div",
{ className: "bt-news", "data-direction": direction, style },
- (0, import_react14.createElement)(
+ (0, import_react15.createElement)(
"button",
{ type: "button", className: "bt-news-jump", onClick: onJump },
- (0, import_react14.createElement)("span", { className: "bt-news-ico" }, (0, import_react14.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
+ (0, import_react15.createElement)("span", { className: "bt-news-ico" }, (0, import_react15.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
label(count)
),
- (0, import_react14.createElement)("button", {
+ (0, import_react15.createElement)("button", {
type: "button",
className: "bt-news-x",
"aria-label": t("Dismiss new messages"),
@@ -8588,7 +9086,7 @@ function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnCl
event.stopPropagation();
onDismiss();
}
- }, (0, import_react14.createElement)(CloseIcon))
+ }, (0, import_react15.createElement)(CloseIcon))
);
const toLatest = () => {
const scroller = document.querySelector("[data-conversation-scroll]");
@@ -8598,8 +9096,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_react14.createElement)(
- import_react14.Fragment,
+ (0, import_react15.createElement)(
+ import_react15.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
@@ -8707,18 +9205,18 @@ function installHeaderEffects({ sessions, roster, actions, doc = globalThis.docu
}
// src/client/composer.js
-var import_react15 = require("react");
+var import_react16 = 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_react15.useState)(false);
- const [note, setNote] = (0, import_react15.useState)("");
- const recognition = (0, import_react15.useRef)(null);
- const latest = (0, import_react15.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);
latest.current = onText;
- (0, import_react15.useEffect)(() => () => recognition.current?.abort(), []);
- (0, import_react15.useEffect)(() => {
+ (0, import_react16.useEffect)(() => () => recognition.current?.abort(), []);
+ (0, import_react16.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
@@ -8772,14 +9270,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_react15.createElement)(
+ return (0, import_react16.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_react15.createElement)(
+ return (0, import_react16.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 })
@@ -8790,7 +9288,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_react15.useEffect)(() => {
+ (0, import_react16.useEffect)(() => {
if (!quote || !inputActions) return;
actions.clearQuote(quote.token);
const excerpt = quote.text.replace(/\s+/g, " ").trim();
@@ -8808,10 +9306,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) {
}, [quote?.token]);
if (sessionId === void 0) return null;
const pluginMic = actions.speech() !== void 0;
- return (0, import_react15.createElement)(
- import_react15.Fragment,
+ return (0, import_react16.createElement)(
+ import_react16.Fragment,
null,
- pluginMic ? null : (0, import_react15.createElement)("button", {
+ pluginMic ? null : (0, import_react16.createElement)("button", {
type: "button",
className: "bt-mic",
"aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"),
@@ -8819,16 +9317,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_react15.createElement)(MicIcon)),
- (0, import_react15.createElement)("button", {
+ }, (0, import_react16.createElement)(MicIcon)),
+ (0, import_react16.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_react15.createElement)(WaveIcon, { size: 18 })),
- dictation.note ? (0, import_react15.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
);
}
function menuIcons(roster) {
@@ -8836,17 +9334,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_react15.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: roster.getSnapshot().byId[id] ?? row.bot, size }));
return marks.get(id);
};
}
function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
const roster = useRoster((value) => value);
- const [open, setOpen] = (0, import_react15.useState)(null);
+ const [open, setOpen] = (0, import_react16.useState)(null);
const [shown, leaving] = useLinger(open, 120);
- const button = (0, import_react15.useRef)(null);
- const menu = (0, import_react15.useRef)(null);
- (0, import_react15.useEffect)(() => {
+ const button = (0, import_react16.useRef)(null);
+ const menu = (0, import_react16.useRef)(null);
+ (0, import_react16.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null);
@@ -8887,10 +9385,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
actions.referenceChat(sessionId, inputActions?.captureInsertion?.());
};
const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat();
- return (0, import_react15.createElement)(
- import_react15.Fragment,
+ return (0, import_react16.createElement)(
+ import_react16.Fragment,
null,
- (0, import_react15.createElement)("button", {
+ (0, import_react16.createElement)("button", {
ref: button,
type: "button",
className: "bt-plus",
@@ -8900,8 +9398,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
"aria-expanded": Boolean(open),
onMouseDown: keepComposerFocus,
onClick: toggle
- }, (0, import_react15.createElement)(PlusIcon)),
- shown ? (0, import_react_dom5.createPortal)((0, import_react15.createElement)(
+ }, (0, import_react16.createElement)(PlusIcon)),
+ shown ? (0, import_react_dom5.createPortal)((0, import_react16.createElement)(
"div",
{
ref: menu,
@@ -8911,9 +9409,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
style: { left: shown.left, bottom: shown.bottom },
"data-leaving": leaving || void 0
},
- (0, import_react15.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react15.createElement)(PaperclipIcon), (0, import_react15.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
- canReference ? (0, import_react15.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react15.createElement)(ChatsIcon), (0, import_react15.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
- (0, import_react15.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react15.createElement)(ClockIcon), (0, import_react15.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")))
), document.body) : null
);
}
@@ -9267,7 +9765,7 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) {
}
// src/client/model-menu.js
-var import_react16 = require("react");
+var import_react17 = require("react");
var import_react_dom6 = require("react-dom");
var MENU_EXIT_MS = 120;
function groupModels(models) {
@@ -9290,8 +9788,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_react16.useState)(void 0);
- (0, import_react16.useEffect)(() => {
+ const [fetched, setFetched] = (0, import_react17.useState)(void 0);
+ (0, import_react17.useEffect)(() => {
setFetched(void 0);
if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0;
let live = true;
@@ -9316,12 +9814,12 @@ function effortRows(reasoning) {
];
}
function EffortRows({ reasoning, current, onPick }) {
- return (0, import_react16.createElement)(
- import_react16.Fragment,
+ return (0, import_react17.createElement)(
+ import_react17.Fragment,
null,
- (0, import_react16.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- (0, import_react16.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
- effortRows(reasoning).map((row) => (0, import_react16.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)(
"button",
{
key: row.effort ?? "default",
@@ -9330,17 +9828,17 @@ function EffortRows({ reasoning, current, onPick }) {
"aria-checked": row.effort === current,
onClick: () => onPick(row.effort)
},
- (0, import_react16.createElement)("span", { className: "bt-menu-label" }, row.label),
- (0, import_react16.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react16.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)
))
);
}
function ModelRows({ roster, current, onPick }) {
- return groupModels(roster.models).map(([provider, list]) => (0, import_react16.createElement)(
- import_react16.Fragment,
+ return groupModels(roster.models).map(([provider, list]) => (0, import_react17.createElement)(
+ import_react17.Fragment,
{ key: list[0].provider ?? provider },
- (0, import_react16.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
- list.map((model) => (0, import_react16.createElement)(
+ (0, import_react17.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
+ list.map((model) => (0, import_react17.createElement)(
"button",
{
key: model.ref,
@@ -9350,20 +9848,20 @@ function ModelRows({ roster, current, onPick }) {
title: model.ref,
onClick: () => onPick(model.ref)
},
- (0, import_react16.createElement)("span", { className: "bt-menu-label" }, model.name),
- model.ref === roster.defaultModel ? (0, import_react16.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null,
- (0, import_react16.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react16.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)
))
));
}
-var Caret = () => (0, import_react16.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_react16.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" }));
function useAnchoredMenu() {
- const [place, setPlace] = (0, import_react16.useState)(null);
+ const [place, setPlace] = (0, import_react17.useState)(null);
const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS);
- const button = (0, import_react16.useRef)(null);
- const menu = (0, import_react16.useRef)(null);
+ const button = (0, import_react17.useRef)(null);
+ const menu = (0, import_react17.useRef)(null);
const close = () => setPlace(null);
- (0, import_react16.useEffect)(() => {
+ (0, import_react17.useEffect)(() => {
if (place === null) return void 0;
const onDown = (event) => {
if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return;
@@ -9426,10 +9924,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_react16.createElement)(
- import_react16.Fragment,
+ return (0, import_react17.createElement)(
+ import_react17.Fragment,
null,
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"button",
{
ref: button,
@@ -9442,38 +9940,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_react16.createElement)(
+ (0, import_react17.createElement)(
"span",
{ className: "bt-model-copy" },
- (0, import_react16.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")),
- shown ? (0, import_react16.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_react16.createElement)(Caret)
+ (0, import_react17.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom6.createPortal)((0, import_react16.createElement)(
+ shownPlace ? (0, import_react_dom6.createPortal)((0, import_react17.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_react16.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react16.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")),
- effortRows(reasoning).length > 0 ? (0, import_react16.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
- (0, import_react16.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- (0, import_react16.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: () => {
close();
openHarnessModels();
- } }, (0, import_react16.createElement)(SlidersIcon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…")))
+ } }, (0, import_react17.createElement)(SlidersIcon), (0, import_react17.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_react16.useState)("");
- const [error, setError] = (0, import_react16.useState)("");
+ const [busy, setBusy] = (0, import_react17.useState)("");
+ const [error, setError] = (0, import_react17.useState)("");
const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready;
- (0, import_react16.useEffect)(() => {
+ (0, import_react17.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_react16.createElement)(RemoteGoneDock, { bot }) : null;
+ if (bot.remote) return bot.remote.gone ? (0, import_react17.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;
@@ -9486,12 +9984,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_react16.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react16.createElement)(KeyHelpLink, { key: "key-help" })] : [
- ...groupModels(models).map(([provider, list]) => (0, import_react16.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)(
"div",
{ key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider },
- (0, import_react16.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
- (0, import_react16.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react16.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", {
key: model.ref,
type: "button",
className: "bt-model-chip",
@@ -9500,46 +9998,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_react16.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)))
)),
- (0, import_react16.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react16.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"))
];
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"div",
{ className: "bt-model-dock" },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"div",
{ className: "bt-astack", "data-lead": "" },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"div",
{ className: "bt-msg" },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"div",
{ className: "bt-msg-line" },
- (0, import_react16.createElement)(BotMark, { bot, size: 22 }),
- (0, import_react16.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_react16.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices),
- error ? (0, import_react16.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
);
}
function RemoteGoneDock({ bot }) {
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"div",
{ className: "bt-model-dock", "data-flybot": "remote-gone" },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"div",
{ className: "bt-astack", "data-lead": "" },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"div",
{ className: "bt-msg" },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"div",
{ className: "bt-msg-line" },
- (0, import_react16.createElement)(BotMark, { bot, size: 22 }),
- (0, import_react16.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_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 }))
)
)
)
@@ -9554,7 +10052,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_react16.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_react17.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 ?? "");
@@ -9578,22 +10076,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_react16.createElement)(
+ return (0, import_react17.createElement)(
"div",
{ className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 },
- (0, import_react16.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
+ (0, import_react17.createElement)(
"div",
{ className: "bt-turn-error-copy" },
- (0, import_react16.createElement)("span", { className: "bt-turn-error-title" }, title),
- (0, import_react16.createElement)("span", { className: "bt-turn-error-message" }, message),
- code ? (0, import_react16.createElement)("code", { className: "bt-turn-error-code" }, code) : null
+ (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
),
- choices.length > 0 ? (0, import_react16.createElement)(
+ choices.length > 0 ? (0, import_react17.createElement)(
"div",
{ className: "bt-turn-error-actions" },
- choices.map(([label, run]) => (0, import_react16.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)),
- deepseek ? (0, import_react16.createElement)(KeyHelpLink, { key: "key-help" }) : null
+ 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
) : null
);
}
@@ -9601,14 +10099,14 @@ function DeepSeekSignIn({ useUi, actions }) {
const open = useUi((state) => state.signIn);
const entry = open ? actions.signInEntry?.() : null;
if (!entry) return null;
- return (0, import_react16.createElement)(SignInHost, { key: open, entry, actions });
+ return (0, import_react17.createElement)(SignInHost, { key: open, entry, actions });
}
function SignInHost({ entry, actions }) {
- const [ops] = (0, import_react16.useState)(() => entry.inject?.() ?? {});
+ const [ops] = (0, import_react17.useState)(() => entry.inject?.() ?? {});
const close = () => actions.closeSignIn();
- const useHook = (source) => (selector) => (0, import_react16.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot()));
- const [hooks] = (0, import_react16.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) }));
- return (0, import_react16.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, {
...ops,
...hooks,
t: actions.accountT(),
@@ -9667,7 +10165,7 @@ var MODEL_CSS = `
`;
// src/client/model-test.js
-var import_react17 = require("react");
+var import_react18 = 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.",
@@ -9692,12 +10190,12 @@ function testOutcome(result, modelName) {
}
var testModel = (id) => call("test-model", { id });
function ModelTestRow({ bot, modelName, test = testModel }) {
- const [state, setState] = (0, import_react17.useState)({ busy: false, outcome: void 0 });
- const live = (0, import_react17.useRef)(true);
- (0, import_react17.useEffect)(() => () => {
+ const [state, setState] = (0, import_react18.useState)({ busy: false, outcome: void 0 });
+ const live = (0, import_react18.useRef)(true);
+ (0, import_react18.useEffect)(() => () => {
live.current = false;
}, []);
- (0, import_react17.useEffect)(() => {
+ (0, import_react18.useEffect)(() => {
setState({ busy: false, outcome: void 0 });
}, [bot.id, bot.model]);
const start = () => {
@@ -9711,21 +10209,21 @@ function ModelTestRow({ bot, modelName, test = testModel }) {
});
};
const { busy, outcome } = state;
- return (0, import_react17.createElement)(
+ return (0, import_react18.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-model-test" },
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react17.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")),
- (0, import_react17.createElement)(
+ (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)(
"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_react17.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
+ outcome?.code ? (0, import_react18.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
)
),
- (0, import_react17.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection"))
+ (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"))
);
}
var MODEL_TEST_CSS = `
@@ -9737,17 +10235,17 @@ var MODEL_TEST_CSS = `
`;
// src/client/folder-menu.js
-var import_react18 = require("react");
+var import_react19 = require("react");
var import_react_dom7 = require("react-dom");
var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path);
-var Tick = ({ on }) => (0, import_react18.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react18.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);
function FolderMenu({ value, group = false, actions, run, onPick, wide = false }) {
const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu();
- const [info, setInfo] = (0, import_react18.useState)(null);
- const [view, setView] = (0, import_react18.useState)("list");
- const [draft, setDraft] = (0, import_react18.useState)("");
- const [listing, setListing] = (0, import_react18.useState)(null);
- const [problem, setProblem] = (0, import_react18.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 fallback = group ? t("Each Bot's own folder") : t("Team workspace");
const toggle = () => {
if (isOpen) {
@@ -9780,35 +10278,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_react18.createElement)(
+ const listView = (0, import_react19.createElement)(
"div",
{ key: "list", className: "bt-folder-view" },
- (0, import_react18.createElement)(
+ (0, import_react19.createElement)(
"button",
{ type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") },
- (0, import_react18.createElement)("span", { className: "bt-menu-label" }, fallback),
- (0, import_react18.createElement)(Tick, { on: !value })
+ (0, import_react19.createElement)("span", { className: "bt-menu-label" }, fallback),
+ (0, import_react19.createElement)(Tick, { on: !value })
),
- folders.length > 0 ? (0, import_react18.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null,
- folders.map((folder) => (0, import_react18.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)(
"button",
{ key: folder.path, type: "button", role: "menuitemradio", "aria-checked": folder.path === value, title: folder.path, onClick: () => choose(folder.path) },
- (0, import_react18.createElement)("span", { className: "bt-folder-copy" }, (0, import_react18.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react18.createElement)("span", { className: "bt-folder-path" }, folder.path)),
- (0, import_react18.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_react18.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- info?.picker === "native" ? (0, import_react18.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react18.createElement)(FolderIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null,
- info?.picker === "browse" ? (0, import_react18.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react18.createElement)(FolderIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null,
- (0, import_react18.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: () => {
setDraft(value ?? "");
setView("type");
- } }, (0, import_react18.createElement)(PencilIcon), (0, import_react18.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…")))
);
- const typeView = (0, import_react18.createElement)(
+ const typeView = (0, import_react19.createElement)(
"div",
{ key: "type", className: "bt-folder-view" },
- (0, import_react18.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react18.createElement)(ChevronLeftIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
- (0, import_react18.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", {
className: "bt-folder-input",
autoFocus: true,
value: draft,
@@ -9820,33 +10318,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false }
if (event.key === "Enter" && !event.nativeEvent.isComposing) typed();
}
}),
- (0, import_react18.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"))
);
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_react18.createElement)(
+ const browseView = listing ? (0, import_react19.createElement)(
"div",
{ key: `browse:${listing.path}`, className: "bt-folder-view" },
- (0, import_react18.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react18.createElement)(ChevronLeftIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
- (0, import_react18.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path),
- parent ? (0, import_react18.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react18.createElement)(FolderIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null,
- inside.map((entry) => (0, import_react18.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)(
"button",
{ key: entry.path, type: "button", role: "menuitem", onClick: () => browse(entry.path) },
- (0, import_react18.createElement)(FolderIcon),
- (0, import_react18.createElement)("span", { className: "bt-menu-label" }, entry.name),
- (0, import_react18.createElement)(ChevronRightIcon)
+ (0, import_react19.createElement)(FolderIcon),
+ (0, import_react19.createElement)("span", { className: "bt-menu-label" }, entry.name),
+ (0, import_react19.createElement)(ChevronRightIcon)
)),
- inside.length === 0 ? (0, import_react18.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null,
- listing.truncated ? (0, import_react18.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null,
- (0, import_react18.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- (0, import_react18.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"))
) : null;
const shownView = view === "type" ? typeView : view === "browse" && browseView ? browseView : listView;
- return (0, import_react18.createElement)(
- import_react18.Fragment,
+ return (0, import_react19.createElement)(
+ import_react19.Fragment,
null,
- (0, import_react18.createElement)(
+ (0, import_react19.createElement)(
"button",
{
ref: button,
@@ -9859,19 +10357,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false }
title: value,
onClick: toggle
},
- (0, import_react18.createElement)(
+ (0, import_react19.createElement)(
"span",
{ className: "bt-model-copy" },
- (0, import_react18.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback),
- value ? (0, import_react18.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_react18.createElement)(Caret)
+ (0, import_react19.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react18.createElement)(
+ shownPlace ? (0, import_react_dom7.createPortal)((0, import_react19.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_react18.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
+ problem ? (0, import_react19.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
), document.body) : null
);
}
@@ -9891,8 +10389,8 @@ var FOLDER_CSS = `
`;
// src/client/schedules.js
-var import_react19 = require("react");
-var POLL_MS3 = 3e4;
+var import_react20 = require("react");
+var POLL_MS4 = 3e4;
var shared = null;
var listeners = /* @__PURE__ */ new Set();
function publish(view) {
@@ -9900,8 +10398,8 @@ function publish(view) {
for (const listener of listeners) listener(view);
}
function useSchedules(actions) {
- const [view, setView] = (0, import_react19.useState)(shared);
- (0, import_react19.useEffect)(() => {
+ const [view, setView] = (0, import_react20.useState)(shared);
+ (0, import_react20.useEffect)(() => {
listeners.add(setView);
let stopped = false;
let timer;
@@ -9909,7 +10407,7 @@ function useSchedules(actions) {
if (!stopped) publish(next);
}).catch(() => {
}).finally(() => {
- if (!stopped) timer = setTimeout(load, POLL_MS3);
+ if (!stopped) timer = setTimeout(load, POLL_MS4);
});
load();
return () => {
@@ -9973,9 +10471,9 @@ function lastRunText(run) {
}
}
function TaskRow({ task, actions, onView, onEdit, showBot, roster }) {
- const [confirming, setConfirming] = (0, import_react19.useState)(false);
- const [busy, setBusy] = (0, import_react19.useState)(false);
- (0, import_react19.useEffect)(() => {
+ const [confirming, setConfirming] = (0, import_react20.useState)(false);
+ const [busy, setBusy] = (0, import_react20.useState)(false);
+ (0, import_react20.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
@@ -9996,7 +10494,7 @@ function TaskRow({ task, actions, onView, onEdit, showBot, roster }) {
onView(next);
}, () => setBusy(false));
};
- const [retryError, setRetryError] = (0, import_react19.useState)("");
+ const [retryError, setRetryError] = (0, import_react20.useState)("");
const retry = () => {
setBusy(true);
setRetryError("");
@@ -10012,24 +10510,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_react19.createElement)(
+ return (0, import_react20.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_react19.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
- (0, import_react19.createElement)(
+ showBot ? (0, import_react20.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
+ (0, import_react20.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react19.createElement)("span", { className: "bt-task-title" }, task.title),
- (0, import_react19.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_react19.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null,
- last ? (0, import_react19.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null,
- retryError ? (0, import_react19.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : 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_react19.createElement)("span", { className: "bt-task-next" }, nextText(task)),
- failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react19.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_react19.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react19.createElement)(PencilIcon)) : null,
- stoppable ? (0, import_react19.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null,
- (0, import_react19.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", {
type: "button",
className: "bt-mini bt-task-remove",
"data-confirm": confirming || void 0,
@@ -10037,7 +10535,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_react19.createElement)(TrashIcon))
+ }, confirming ? t("Delete") : (0, import_react20.createElement)(TrashIcon))
);
}
var pad = (number) => String(number).padStart(2, "0");
@@ -10070,17 +10568,17 @@ function formStart(task) {
}
function TaskForm({ roster, actions, start, task, onDone, onCancel }) {
const first = formStart(task);
- const [target, setTarget] = (0, import_react19.useState)(task ? task.botId ?? task.roomId : start ?? roster.bots[0]?.id ?? "");
- const [title, setTitle] = (0, import_react19.useState)(first.title);
- const [prompt, setPrompt] = (0, import_react19.useState)(first.prompt);
- const [kind, setKind] = (0, import_react19.useState)(first.kind);
- const [at, setAt] = (0, import_react19.useState)(first.at);
- const [time, setTime] = (0, import_react19.useState)(first.time);
- const [weekdays, setWeekdays] = (0, import_react19.useState)(first.weekdays);
- const [count, setCount] = (0, import_react19.useState)(first.count);
- const [unit, setUnit] = (0, import_react19.useState)(first.unit);
- const [busy, setBusy] = (0, import_react19.useState)(false);
- const [error, setError] = (0, import_react19.useState)("");
+ 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 timing = () => {
if (kind === "cron") return { kind: "keep" };
if (kind === "at") return { kind, at: new Date(at).toISOString() };
@@ -10107,69 +10605,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_react19.createElement)(
+ return (0, import_react20.createElement)(
"form",
{ className: "bt-set-card bt-task-form", onSubmit: submit },
- (0, import_react19.createElement)(
+ (0, import_react20.createElement)(
"label",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react19.createElement)("span", { className: "bt-set-copy" }, (0, import_react19.createElement)("span", { className: "bt-bs-label" }, t("Send to"))),
- (0, import_react19.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)(
"select",
{ className: "bt-select bt-task-bot", value: target, onChange: (event) => setTarget(event.target.value) },
- (0, import_react19.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react19.createElement)("option", { key: bot.id, value: bot.id }, bot.name))),
- roster.rooms.length > 0 ? (0, import_react19.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react19.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_react19.createElement)(
+ (0, import_react20.createElement)(
"label",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react19.createElement)("span", { className: "bt-set-copy" }, (0, import_react19.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
- (0, import_react19.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_react19.createElement)(
+ (0, import_react20.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react19.createElement)("span", { className: "bt-set-copy" }, (0, import_react19.createElement)("span", { className: "bt-bs-label" }, t("What to do"))),
- (0, import_react19.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_react19.createElement)(
+ (0, import_react20.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react19.createElement)("span", { className: "bt-set-copy" }, (0, import_react19.createElement)("span", { className: "bt-bs-label" }, t("When"))),
- (0, import_react19.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react19.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))),
- (0, import_react19.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)(
"div",
{ key: kind, className: "bt-task-when bt-veil" },
- kind === "cron" ? (0, import_react19.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null,
- kind === "at" ? (0, import_react19.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_react19.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react19.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", {
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_react19.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_react19.createElement)(
- import_react19.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,
null,
- (0, import_react19.createElement)("span", { className: "bt-task-every" }, t("Every")),
- (0, import_react19.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_react19.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)(
"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_react19.createElement)("option", { key: id, value: id }, t(label)))
+ UNITS.map(([id, , label]) => (0, import_react20.createElement)("option", { key: id, value: id }, t(label)))
)
) : null
)
),
- error ? (0, import_react19.createElement)("div", { className: "bt-set-row" }, (0, import_react19.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
- (0, import_react19.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)(
"div",
{ className: "bt-set-row bt-task-foot" },
- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
- (0, import_react19.createElement)(
+ (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
+ (0, import_react20.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")
@@ -10182,21 +10680,21 @@ function openPlugins(actions) {
actions.selectPanel("plugins");
}
function AutomationOff({ actions }) {
- return (0, import_react19.createElement)(
+ return (0, import_react20.createElement)(
"div",
{ className: "bt-set-card bt-task-off" },
- (0, import_react19.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react19.createElement)(ClockIcon)),
- (0, import_react19.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")),
- (0, import_react19.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")),
- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
+ (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"))
);
}
function SchedulesPage({ roster, actions, start }) {
const view = useSchedules(actions);
- const [adding, setAdding] = (0, import_react19.useState)(Boolean(start));
- const [editing, setEditing] = (0, import_react19.useState)(null);
- if (view === null) return (0, import_react19.createElement)("div", { className: "bt-set-hint" }, t("Loading…"));
- if (!view.available) return (0, import_react19.createElement)(AutomationOff, { 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 shown = (next) => {
publish(next);
setAdding(false);
@@ -10207,51 +10705,51 @@ function SchedulesPage({ roster, actions, start }) {
setEditing(task);
};
const byChat = [
- ...roster.bots.map((bot) => [bot.id, (0, import_react19.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_react19.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)])
].filter(([, , , tasks]) => tasks.length > 0);
return [
- adding ? (0, import_react19.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react19.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => {
+ 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: () => {
setEditing(null);
setAdding(true);
- } }, (0, import_react19.createElement)(PlusIcon), t("New task"))),
- byChat.length === 0 && !adding ? (0, import_react19.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react19.createElement)("div", { className: "bt-set-row" }, (0, import_react19.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react19.createElement)(
+ } }, (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)(
"section",
{ key: id, className: "bt-set-section" },
- (0, import_react19.createElement)("h3", { className: "bt-task-head" }, avatar, name),
- (0, import_react19.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) })
+ (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) })
))
];
}
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_react19.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit }));
- return (0, import_react19.createElement)(
- import_react19.Fragment,
+ 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,
null,
- open ? (0, import_react19.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null,
- rows.length + extra.length > 0 ? (0, import_react19.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : 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
);
}
function BotSchedules({ bot, roster, actions }) {
const view = useSchedules(actions);
- const [editing, setEditing] = (0, import_react19.useState)(null);
+ const [editing, setEditing] = (0, import_react20.useState)(null);
if (view === null) return null;
const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : [];
- return (0, import_react19.createElement)(
+ return (0, import_react20.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react19.createElement)("h3", null, t("Scheduled tasks")),
- !view.available ? (0, import_react19.createElement)(
+ (0, import_react20.createElement)("h3", null, t("Scheduled tasks")),
+ !view.available ? (0, import_react20.createElement)(
"div",
{ className: "bt-set-card" },
- (0, import_react19.createElement)(
+ (0, import_react20.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react19.createElement)("span", { className: "bt-set-copy" }, (0, import_react19.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))),
- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
+ (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_react19.createElement)(TaskList, {
+ ) : (0, import_react20.createElement)(TaskList, {
tasks,
editing,
roster,
@@ -10262,23 +10760,23 @@ function BotSchedules({ bot, roster, actions }) {
setEditing(null);
},
onCancel: () => setEditing(null),
- extra: [(0, import_react19.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react19.createElement)(PlusIcon), t("New task"))]
+ 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"))]
})
);
}
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_react19.createElement)(
+ return (0, import_react20.createElement)(
"div",
null,
- (0, import_react19.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")),
- tasks.length === 0 ? (0, import_react19.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_react19.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react19.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)(
"button",
{ key: task.id, type: "button", className: "bt-task-line", onClick: () => actions.openSettings("schedules") },
- (0, import_react19.createElement)(ClockIcon),
- (0, import_react19.createElement)("span", { className: "bt-task-line-title" }, task.title),
- (0, import_react19.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))
)))
);
}
@@ -10328,7 +10826,7 @@ var SCHEDULE_CSS = `
`;
// src/client/capability-settings.js
-var import_react20 = require("react");
+var import_react21 = require("react");
var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] });
var parseMap = (text, label) => {
let value;
@@ -10342,11 +10840,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_react20.createElement)(
+ return (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(label))),
- (0, import_react20.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", {
className: multiline ? "bt-input bt-input-area" : "bt-input",
value,
disabled,
@@ -10358,7 +10856,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl
);
}
function CapabilityForm({ draft, setDraft, items, actions, onClose }) {
- const [error, setError] = (0, import_react20.useState)("");
+ const [error, setError] = (0, import_react21.useState)("");
const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value }));
const submit = async (event) => {
event.preventDefault();
@@ -10389,95 +10887,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_react20.createElement)(
+ return (0, import_react21.createElement)(
"form",
{ className: "bt-connector-form", onSubmit: submit },
- (0, import_react20.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }),
- (0, import_react20.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)(
"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("Type"))),
- (0, import_react20.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)(
"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_react20.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")))
)
),
- (0, import_react20.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
- draft.kind === "mcp" ? (0, import_react20.createElement)(
+ (0, import_react21.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
+ draft.kind === "mcp" ? (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("Connection"))),
- (0, import_react20.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)(
"select",
{ className: "bt-input", value: draft.transport, onChange: (event) => set("transport", event.target.value) },
- (0, import_react20.createElement)("option", { value: "stdio" }, t("Local command")),
- (0, import_react20.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"))
)
) : null,
- draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react20.createElement)(
+ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react21.createElement)(
"div",
null,
- (0, import_react20.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }),
- (0, import_react20.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }),
- (0, import_react20.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"}') })
) : null,
- draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react20.createElement)(
+ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react21.createElement)(
"div",
null,
- (0, import_react20.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }),
- (0, import_react20.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"}') })
) : null,
- draft.kind === "skill" ? (0, import_react20.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null,
- draft.kind === "pack" ? (0, import_react20.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)(
"div",
{ className: "bt-set-card" },
- available.length === 0 ? (0, import_react20.createElement)("div", { className: "bt-set-row" }, (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react20.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)(
"label",
{ key: item.name, 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" }, item.name)),
- (0, import_react20.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)) })
))
) : null,
- error ? (0, import_react20.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
- (0, import_react20.createElement)(
+ error ? (0, import_react21.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
+ (0, import_react21.createElement)(
"div",
{ className: "bt-connector-form-row" },
- (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")),
- (0, import_react20.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"))
)
);
}
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_react20.createElement)(
+ return (0, import_react21.createElement)(
"div",
{ className: "bt-connector" },
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"div",
{ className: "bt-connector-head" },
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"span",
{ className: "bt-connector-text" },
- (0, import_react20.createElement)("span", { className: "bt-connector-name" }, item.name),
- (0, import_react20.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_react20.createElement)(
+ (0, import_react21.createElement)(
"span",
{ className: "bt-connector-actions" },
- item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null,
- (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")),
- (0, import_react20.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"))
)
)
);
}
function CapabilitiesPage({ roster, actions, run }) {
const items = [...roster.capabilities ?? []].sort((a, b) => a.name.localeCompare(b.name));
- const [draft, setDraft] = (0, import_react20.useState)(null);
- const [confirming, setConfirming] = (0, import_react20.useState)("");
- (0, import_react20.useEffect)(() => {
+ const [draft, setDraft] = (0, import_react21.useState)(null);
+ const [confirming, setConfirming] = (0, import_react21.useState)("");
+ (0, import_react21.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(""), 4e3);
return () => clearTimeout(timer);
@@ -10500,20 +10998,20 @@ function CapabilitiesPage({ roster, actions, run }) {
setConfirming("");
void run(actions.deleteCapability(item.name));
};
- return (0, import_react20.createElement)(
+ return (0, import_react21.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react20.createElement)("h3", null, t("Shared capabilities")),
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)("h3", null, t("Shared capabilities")),
+ (0, import_react21.createElement)(
"div",
{ className: "bt-actions" },
- (0, import_react20.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"))
),
- draft ? (0, import_react20.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
- (0, import_react20.createElement)(
+ draft ? (0, import_react21.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
+ (0, import_react21.createElement)(
"div",
{ className: "bt-set-card bt-connectors-card" },
- items.length === 0 ? (0, import_react20.createElement)("div", { className: "bt-set-row" }, (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react20.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, {
key: item.name,
item,
confirming: confirming === item.name,
@@ -10529,10 +11027,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_react20.createElement)(
+ return (0, import_react21.createElement)(
Card2,
{ title: t("Capabilities") },
- items.map((item) => (0, import_react20.createElement)(SwitchRow2, {
+ items.map((item) => (0, import_react21.createElement)(SwitchRow2, {
key: item.name,
label: item.name,
hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0,
@@ -10543,7 +11041,7 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow
}
// src/client/connector-bots.js
-var import_react21 = require("react");
+var import_react22 = 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));
@@ -10559,38 +11057,38 @@ 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_react21.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_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)) });
}
function ConnectorBots({ connector, roster, actions, run, SwitchRow: SwitchRow2 }) {
const bots = ownBots(roster);
- return (0, import_react21.createElement)(
+ return (0, import_react22.createElement)(
"div",
{ className: "bt-connector-bots" },
- bots.length === 0 ? (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react21.createElement)(
+ bots.length === 0 ? (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react22.createElement)(
"div",
{ key: bot.id, className: "bt-connector-bot" },
- (0, import_react21.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
+ (0, import_react22.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
)),
- (0, import_react21.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_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."))
);
}
function BotConnectorsCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) {
const connected = connectedConnectors(roster);
if (connected.length === 0) return null;
- return (0, import_react21.createElement)(
+ return (0, import_react22.createElement)(
Card2,
{ title: t("Connectors") },
- connected.map((connector) => (0, import_react21.createElement)(
+ connected.map((connector) => (0, import_react22.createElement)(
"div",
{ key: connector.id, className: "bt-connector-bot" },
- (0, import_react21.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
+ (0, import_react22.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
))
);
}
// src/client/memory-panel.js
-var import_react22 = require("react");
-var POLL_MS4 = 1500;
+var import_react23 = require("react");
+var POLL_MS5 = 1500;
var MENU_EXIT_MS2 = 120;
var counts = /* @__PURE__ */ new Map();
var countListeners = /* @__PURE__ */ new Set();
@@ -10600,8 +11098,8 @@ function noteCount(botId, count) {
for (const listener of countListeners) listener();
}
function useMemoryCount(botId, actions) {
- const [count, setCount] = (0, import_react22.useState)(counts.get(botId) ?? null);
- (0, import_react22.useEffect)(() => {
+ const [count, setCount] = (0, import_react23.useState)(counts.get(botId) ?? null);
+ (0, import_react23.useEffect)(() => {
const listener = () => setCount(counts.get(botId) ?? null);
countListeners.add(listener);
listener();
@@ -10620,16 +11118,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_react22.createElement)(
+ return (0, import_react23.createElement)(
"div",
null,
- (0, import_react22.createElement)("div", { className: "bt-section-title" }, t("Memory")),
- (0, import_react22.createElement)(
+ (0, import_react23.createElement)("div", { className: "bt-section-title" }, t("Memory")),
+ (0, import_react23.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_react22.createElement)(MemoryIcon),
- (0, import_react22.createElement)("span", { className: "bt-mem-row-copy" }, label),
- (0, import_react22.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react22.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))
)
);
}
@@ -10644,45 +11142,45 @@ function ago(at, now) {
return format.format(-Math.round(hours / 24), "day");
}
function Skeleton({ label }) {
- return (0, import_react22.createElement)(
+ return (0, import_react23.createElement)(
"div",
{ className: "bt-mem-skel", "aria-busy": true },
- [0, 1].map((block) => (0, import_react22.createElement)(
+ [0, 1].map((block) => (0, import_react23.createElement)(
"div",
{ key: block, className: "bt-mem-skel-block" },
- (0, import_react22.createElement)("span", { className: "bt-mem-skel-head" }),
- (0, import_react22.createElement)("span", null),
- (0, import_react22.createElement)("span", null),
- (0, import_react22.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%" } })
)),
- label ? (0, import_react22.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
+ label ? (0, import_react23.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
);
}
function Summary({ bot, view, regenerate }) {
if (view.entries.length === 0) {
- return (0, import_react22.createElement)(
+ return (0, import_react23.createElement)(
"div",
{ className: "bt-mem-empty" },
- (0, import_react22.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react22.createElement)(MemoryIcon)),
- (0, import_react22.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })),
- (0, import_react22.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 }))
);
}
- const failed = view.error && !view.summarizing ? (0, import_react22.createElement)(
+ const failed = view.error && !view.summarizing ? (0, import_react23.createElement)(
"div",
{ className: "bt-mem-failed", role: "alert" },
- (0, import_react22.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_react22.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"))
) : null;
- if (!view.summary) return (0, import_react22.createElement)(import_react22.Fragment, null, failed, failed ? null : (0, import_react22.createElement)(Skeleton, { label: t("Writing the summary…") }));
- return (0, import_react22.createElement)(
- import_react22.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,
null,
failed,
- (0, import_react22.createElement)(
+ (0, import_react23.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_react22.createElement)("h3", { key: index }, block.text) : (0, import_react22.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))
)
);
}
@@ -10694,9 +11192,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => {
return groups;
}, []);
function EntryRow({ entry, remove }) {
- const [confirming, setConfirming] = (0, import_react22.useState)(false);
- const [busy, setBusy] = (0, import_react22.useState)(false);
- (0, import_react22.useEffect)(() => {
+ const [confirming, setConfirming] = (0, import_react23.useState)(false);
+ const [busy, setBusy] = (0, import_react23.useState)(false);
+ (0, import_react23.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
@@ -10711,16 +11209,16 @@ function EntryRow({ entry, remove }) {
setBusy(true);
if (!await remove(entry)) setBusy(false);
};
- return (0, import_react22.createElement)(
+ return (0, import_react23.createElement)(
"li",
{ className: "bt-mem-entry", "data-busy": busy || void 0 },
- (0, import_react22.createElement)(
+ (0, import_react23.createElement)(
"div",
{ className: "bt-mem-entry-copy" },
- (0, import_react22.createElement)("span", { className: "bt-mem-entry-text" }, entry.text),
- meta ? (0, import_react22.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_react22.createElement)("button", {
+ (0, import_react23.createElement)("button", {
type: "button",
className: "bt-mem-remove",
"data-confirm": confirming || void 0,
@@ -10728,7 +11226,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_react22.createElement)(TrashIcon))
+ }, confirming ? t("Remove") : (0, import_react23.createElement)(TrashIcon))
);
}
function Entries({ bot, view, remove }) {
@@ -10737,25 +11235,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_react22.createElement)(
+ return (0, import_react23.createElement)(
"div",
{ className: "bt-mem-entries" },
- sections.map((section) => (0, import_react22.createElement)(
+ sections.map((section) => (0, import_react23.createElement)(
"section",
{ key: section.scope, className: "bt-mem-section" },
- (0, import_react22.createElement)(
+ (0, import_react23.createElement)(
"div",
{ className: "bt-mem-section-head" },
- (0, import_react22.createElement)("h3", null, section.title),
- section.hint ? (0, import_react22.createElement)("span", null, section.hint) : null,
- (0, import_react22.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()) }))
),
- groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react22.createElement)(
- import_react22.Fragment,
+ groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react23.createElement)(
+ import_react23.Fragment,
{ key: group.key },
- group.topic ? (0, import_react22.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null,
- (0, import_react22.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react22.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove })))
- )) : (0, import_react22.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"))
))
);
}
@@ -10767,52 +11265,52 @@ 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_react22.createElement)(
+ return (0, import_react23.createElement)(
"div",
{ className: "bt-mem-reply", role: "status" },
- (0, import_react22.createElement)(
+ (0, import_react23.createElement)(
"div",
{ className: "bt-mem-reply-head" },
- (0, import_react22.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked),
- answer.pending ? null : (0, import_react22.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react22.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))
),
- answer.pending ? (0, import_react22.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react22.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react22.createElement)(
- import_react22.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,
null,
- answer.reply ? (0, import_react22.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null,
- answer.changes.length > 0 ? (0, import_react22.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react22.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)(
"li",
{ key: index, "data-ok": change.ok || void 0 },
- change.ok ? (0, import_react22.createElement)(CheckIcon) : (0, import_react22.createElement)(CloseIcon),
- (0, import_react22.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))
))) : null
)
);
}
function useMemoryView(botId, actions) {
- const [view, setView] = (0, import_react22.useState)(null);
- const [error, setError] = (0, import_react22.useState)("");
- const timer = (0, import_react22.useRef)(0);
- const latest = (0, import_react22.useRef)(0);
- const alive = (0, import_react22.useRef)(true);
- const show = (0, import_react22.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) => {
setView(next);
setError("");
noteCount(botId, next.entries.length);
}, [botId]);
- const load = (0, import_react22.useCallback)(async (flags = {}) => {
+ const load = (0, import_react23.useCallback)(async (flags = {}) => {
clearTimeout(timer.current);
const ticket = ++latest.current;
try {
const next = await actions.memoryView(botId, flags);
if (!alive.current || ticket !== latest.current) return;
show(next);
- if (next.summarizing) timer.current = setTimeout(() => void load({ summarize: flags.summarize || flags.regenerate }), POLL_MS4);
+ if (next.summarizing) timer.current = setTimeout(() => void load({ summarize: flags.summarize || flags.regenerate }), POLL_MS5);
} catch (failure) {
if (alive.current && ticket === latest.current) setError(failure?.message ?? String(failure));
}
}, [botId]);
- (0, import_react22.useEffect)(() => {
+ (0, import_react23.useEffect)(() => {
alive.current = true;
void load({ summarize: true });
return () => {
@@ -10823,8 +11321,8 @@ function useMemoryView(botId, actions) {
return { view, error, load, show };
}
function useNow3(every) {
- const [now, setNow] = (0, import_react22.useState)(Date.now());
- (0, import_react22.useEffect)(() => {
+ const [now, setNow] = (0, import_react23.useState)(Date.now());
+ (0, import_react23.useEffect)(() => {
const id = setInterval(() => setNow(Date.now()), every);
return () => clearInterval(id);
}, [every]);
@@ -10832,18 +11330,18 @@ function useNow3(every) {
}
function MemoryDialog({ bot, actions, leaving = false }) {
const { view, error, load, show } = useMemoryView(bot.id, actions);
- const [page, setPage] = (0, import_react22.useState)("summary");
- const [menu, setMenu] = (0, import_react22.useState)(false);
+ const [page, setPage] = (0, import_react23.useState)("summary");
+ const [menu, setMenu] = (0, import_react23.useState)(false);
const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2);
- const [text, setText] = (0, import_react22.useState)("");
- const [answer, setAnswer] = (0, import_react22.useState)(null);
- const menuRef = (0, import_react22.useRef)(null);
- const dotsRef = (0, import_react22.useRef)(null);
- const inputRef = (0, import_react22.useRef)(null);
+ 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 now = useNow3(3e4);
- const close = (0, import_react22.useCallback)(() => actions.closeMemory(), []);
+ const close = (0, import_react23.useCallback)(() => actions.closeMemory(), []);
useEscape(close);
- (0, import_react22.useEffect)(() => {
+ (0, import_react23.useEffect)(() => {
if (!menu) return void 0;
const onDown = (event) => {
if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return;
@@ -10905,52 +11403,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_react22.createElement)(
+ return (0, import_react23.createElement)(
"div",
{ className: "bt-mem-layer", "data-leaving": leaving || void 0 },
- (0, import_react22.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
- (0, import_react22.createElement)(
+ (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_react22.createElement)(
+ (0, import_react23.createElement)(
"div",
{ className: "bt-mem-head" },
- page === "entries" ? (0, import_react22.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react22.createElement)(ChevronLeftIcon)) : null,
- (0, import_react22.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
- (0, import_react22.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
- (0, import_react22.createElement)("span", { className: "bt-mem-head-gap" }),
- (0, import_react22.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_react22.createElement)(DotsIcon)),
- (0, import_react22.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react22.createElement)(CloseIcon)),
- menuShown ? (0, import_react22.createElement)(
+ 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_react22.createElement)(
+ (0, import_react23.createElement)(
"button",
{ type: "button", role: "menuitem", onClick: openEntries },
- (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("All entries")),
- (0, import_react22.createElement)("span", { className: "bt-menu-hint" }, String(count))
+ (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_react22.createElement)(
+ (0, import_react23.createElement)(
"button",
{ type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate },
- (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary"))
+ (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary"))
)
) : null
),
- (0, import_react22.createElement)(
+ (0, import_react23.createElement)(
"div",
{ className: "bt-mem-body" },
- error ? (0, import_react22.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null,
- view === null ? error ? null : (0, import_react22.createElement)(Skeleton) : page === "entries" ? (0, import_react22.createElement)(Entries, { bot, view, remove }) : (0, import_react22.createElement)(Summary, { bot, view, regenerate })
+ 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_react22.createElement)(
+ (0, import_react23.createElement)(
"div",
{ className: "bt-mem-foot" },
- answer ? (0, import_react22.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
- (0, import_react22.createElement)(
+ answer ? (0, import_react23.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
+ (0, import_react23.createElement)(
"form",
{ className: "bt-mem-ask", onSubmit: ask },
- (0, import_react22.createElement)("input", {
+ (0, import_react23.createElement)("input", {
ref: inputRef,
className: "bt-mem-input",
value: text,
@@ -10960,10 +11458,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_react22.createElement)(
+ (0, import_react23.createElement)(
"button",
{ type: "submit", className: "bt-mem-send", disabled: busy || text.trim() === "", "aria-label": t("Send") },
- busy ? (0, import_react22.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react22.createElement)(ArrowUpIcon)
+ busy ? (0, import_react23.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react23.createElement)(ArrowUpIcon)
)
)
)
@@ -10972,7 +11470,7 @@ function MemoryDialog({ bot, actions, leaving = false }) {
}
// src/client/standing-approvals.js
-var import_react23 = require("react");
+var import_react24 = require("react");
var standingOf = (target, roster) => {
const me = roster?.self?.memberId;
return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me);
@@ -10981,19 +11479,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_react23.createElement)(
+ return (0, import_react24.createElement)(
Card2,
{ title: t("Always allowed") },
- grants.map((grant) => (0, import_react23.createElement)(
+ grants.map((grant) => (0, import_react24.createElement)(
"div",
{ key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind },
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind),
- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, hint)
+ (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_react23.createElement)("button", {
+ (0, import_react24.createElement)("button", {
type: "button",
className: "bt-soft bt-soft-sm",
"aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }),
@@ -11004,7 +11502,7 @@ function StandingApprovalsCard({ scope, target, roster, actions, run, Card: Card
}
// src/client/bot-settings.js
-var import_react24 = require("react");
+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"));
@@ -11028,8 +11526,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_react24.useState)(null);
- const reverting = (0, import_react24.useRef)(false);
+ const [draft, setDraft] = (0, import_react25.useState)(null);
+ const reverting = (0, import_react25.useRef)(false);
const commit = () => {
const next = draft?.trim();
setDraft(null);
@@ -11040,7 +11538,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_react24.createElement)(multiline ? "textarea" : "input", {
+ return (0, import_react25.createElement)(multiline ? "textarea" : "input", {
className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input",
value: draft ?? value ?? "",
placeholder,
@@ -11061,29 +11559,29 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu
});
}
function Row({ label, hint, stack = false, children }) {
- return (0, import_react24.createElement)(
+ 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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react24.createElement)("span", { className: "bt-set-hint" }, hint) : null),
+ (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_react24.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react24.createElement)("h3", null, title) : null, (0, import_react24.createElement)("div", { className: "bt-set-card" }, 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_react24.createElement)(
+ 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_react24.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true })
+ (0, import_react25.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true })
);
}
function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) {
- return (0, import_react24.createElement)(
+ return (0, import_react25.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react24.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react24.createElement)("span", { className: "bt-set-hint" }, hint) : null),
- (0, import_react24.createElement)(Switch, { on, label, disabled, onChange })
+ (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) {
@@ -11094,22 +11592,22 @@ function modelHint(bot, roster) {
return t("Applies from the next message.");
}
function ShapeRing({ shape, size }) {
- const maskId = (0, import_react24.useRef)("");
+ 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_react24.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } });
+ 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_react24.createElement)(
- import_react24.Fragment,
+ return (0, import_react25.createElement)(
+ import_react25.Fragment,
null,
- (0, import_react24.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react24.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react24.createElement)("button", {
+ (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",
@@ -11117,8 +11615,8 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) {
title: t(shapeOf(id).label),
"aria-pressed": !hasPhoto && look.shape === id,
onClick: () => onShape(id)
- }, (0, import_react24.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react24.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))),
- (0, import_react24.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react24.createElement)("button", {
+ }, (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",
@@ -11126,19 +11624,19 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) {
title: t(capitalize(color)),
"aria-pressed": look.color === color,
onClick: () => onColor(color)
- }, (0, import_react24.createElement)("span", { style: { background: paintCss(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_react24.useState)(look.image ? "upload" : "bot");
- const [over, setOver] = (0, import_react24.useState)(false);
- const ref = (0, import_react24.useRef)(null);
- const fileRef = (0, import_react24.useRef)(null);
+ 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_react24.useEffect)(() => {
+ (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();
@@ -11156,7 +11654,7 @@ function AvatarEditor({ bot, actions, run, onClose }) {
window.removeEventListener("keydown", onKey, true);
};
}, [onClose]);
- (0, import_react24.useEffect)(() => {
+ (0, import_react25.useEffect)(() => {
if (tab !== "upload") return void 0;
const onPaste = (event) => {
const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/"));
@@ -11170,32 +11668,32 @@ function AvatarEditor({ bot, actions, run, onClose }) {
}, [tab, bot]);
const hasPhoto = Boolean(look.image);
const glide = useGlide(tab);
- return (0, import_react24.createElement)(
+ return (0, import_react25.createElement)(
"div",
{ ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") },
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"div",
{ className: "bt-avedit-top" },
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"div",
{ ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" },
- (0, import_react24.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react24.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)("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_react24.createElement)("button", {
+ (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_react24.createElement)(CharacterGrid, {
+ 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_react24.createElement)(
+ tab === "upload" ? (0, import_react25.createElement)(
"div",
{
className: "bt-dropzone",
@@ -11211,15 +11709,15 @@ function AvatarEditor({ bot, actions, run, onClose }) {
upload(event.dataTransfer.files?.[0]);
}
},
- hasPhoto ? (0, import_react24.createElement)("img", { src: look.image, alt: "" }) : null,
- (0, import_react24.createElement)("div", null, t("Drag, drop, or paste an image")),
- (0, import_react24.createElement)(
+ 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_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")),
- hasPhoto ? (0, import_react24.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)("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_react24.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
+ (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);
@@ -11230,22 +11728,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_react24.createElement)(
+ return (0, import_react25.createElement)(
Card,
{ title: t("Memory") },
- (0, import_react24.createElement)(
+ (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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, label)),
- (0, import_react24.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react24.createElement)(ChevronRightIcon))
+ (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_react24.useState)(false);
- const [confirming, setConfirming] = (0, import_react24.useState)(false);
- const closeEditor = (0, import_react24.useCallback)(() => setEditing(false), []);
- (0, import_react24.useEffect)(() => {
+ 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);
@@ -11260,116 +11758,116 @@ function BotSettingsForm({ bot, roster, actions, run }) {
if (confirming) void run(actions.deleteBot(bot.id));
else setConfirming(true);
};
- (0, import_react24.useEffect)(() => {
+ (0, import_react25.useEffect)(() => {
void actions.refreshModels?.();
}, [bot.id]);
- return (0, import_react24.createElement)(
+ return (0, import_react25.createElement)(
"div",
{ className: "bt-bs" },
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"div",
{ className: "bt-bs-hero" },
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"div",
{ className: "bt-avatar-anchor" },
- (0, import_react24.createElement)(
+ (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_react24.createElement)(BotMark, { bot, size: 64, live: true }),
- (0, import_react24.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react24.createElement)(PencilIcon))
+ (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_react24.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
+ editing ? (0, import_react25.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
),
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"div",
{ className: "bt-bs-who" },
- (0, import_react24.createElement)("span", { className: "bt-bs-name" }, bot.name),
- (0, import_react24.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
+ (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_react24.createElement)(
+ (0, import_react25.createElement)(
Card,
{ title: t("Profile") },
- (0, import_react24.createElement)(Row, { label: t("Name") }, (0, import_react24.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })),
- (0, import_react24.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react24.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) }))
+ (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_react24.createElement)(
+ (0, import_react25.createElement)(
Card,
{ title: t("Model") },
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))),
- (0, import_react24.createElement)(ModelMenu, { bot, roster, actions, run })
+ (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 })
),
- bot.model ? (0, import_react24.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null
+ bot.model ? (0, import_react25.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null
),
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
Card,
{ title: t("Working folder") },
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
- (0, import_react24.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) })
+ (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_react24.createElement)(
+ (0, import_react25.createElement)(
Card,
{ title: t("Instructions") },
- (0, import_react24.createElement)(
+ (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_react24.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) })
+ (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 }) })
)
),
// Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406).
- (0, import_react24.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react24.createElement)(Card, { title: t("Sharing") }, children) }),
- (0, import_react24.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }),
- (0, import_react24.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }),
- roster.memory === false ? null : (0, import_react24.createElement)(MemoryCard, { bot, actions }),
- (0, import_react24.createElement)(BotSchedules, { bot, roster, actions }),
- (0, import_react24.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }),
- (0, import_react24.createElement)(
+ (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_react24.createElement)(
+ onlyMain ? (0, import_react25.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react24.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)("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_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change"))
- ) : (0, import_react24.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_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_react24.createElement)(
+ isTile ? null : (0, import_react25.createElement)(
Card,
{ title: t("Sidebar") },
- (0, import_react24.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_react24.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)(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_react24.createElement)(
+ (0, import_react25.createElement)(
Card,
{ title: t("More") },
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"button",
{ type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate },
- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))),
- (0, import_react24.createElement)("span", { className: "bt-chevron" }, (0, import_react24.createElement)(ChevronRightIcon))
+ (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_react24.createElement)(
+ 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_react24.createElement)(
+ (0, import_react25.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react24.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")),
- (0, import_react24.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-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_react24.createElement)("span", { className: "bt-chevron" }, (0, import_react24.createElement)(ChevronRightIcon))
+ (0, import_react25.createElement)("span", { className: "bt-chevron" }, (0, import_react25.createElement)(ChevronRightIcon))
)
)
);
@@ -11410,7 +11908,7 @@ ${MODEL_TEST_CSS}
`;
// src/client/group-settings.js
-var import_react25 = require("react");
+var import_react26 = 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."]
@@ -11427,31 +11925,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, 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_react25.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_react25.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_react25.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name];
- return index === 0 ? marked : [(0, import_react25.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked];
+ 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];
});
- return (0, import_react25.createElement)(
+ return (0, import_react26.createElement)(
"div",
{ className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"span",
{ className: "bt-gs-who bt-face-person" },
- (0, import_react25.createElement)(PersonMark, { size, name: person.name }),
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(PersonMark, { size, name: person.name }),
+ (0, import_react26.createElement)(
"span",
{ className: "bt-face-person-text" },
- (0, import_react25.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
- (0, import_react25.createElement)("span", { className: "bt-face-bots" }, bots)
+ (0, import_react26.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
+ (0, import_react26.createElement)("span", { className: "bt-face-bots" }, bots)
)
)
);
}
function FaceLead({ room: room2, roster, pick: pick2 }) {
const lead = room2.face?.lead;
- const [open, setOpen] = (0, import_react25.useState)(false);
- const [busy, setBusy] = (0, import_react25.useState)(false);
- const [error, setError] = (0, import_react25.useState)("");
+ const [open, setOpen] = (0, import_react26.useState)(false);
+ const [busy, setBusy] = (0, import_react26.useState)(false);
+ const [error, setError] = (0, import_react26.useState)("");
const seats = faceSeats(room2, roster);
const current = seats.find((seat) => isLeadSeat(seat, lead?.handle));
if (!current) return null;
@@ -11468,30 +11966,30 @@ function FaceLead({ room: room2, roster, pick: pick2 }) {
setBusy(false);
}
};
- return (0, import_react25.createElement)(
+ return (0, import_react26.createElement)(
"div",
{ className: "bt-face-lead", "data-flybot": "face-lead" },
- (0, import_react25.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
+ (0, import_react26.createElement)(
"div",
{ className: "bt-set-row" },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react25.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)),
- (0, import_react25.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_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."))
),
- canPick ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null
+ canPick ? (0, import_react26.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_react25.createElement)(
+ open && canPick ? seats.map((seat) => (0, import_react26.createElement)(
"div",
{ key: seat.key, className: "bt-set-row" },
- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)),
- seat === current ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react25.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead"))
+ (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"))
)) : null,
- open && canPick && lead.picked ? (0, import_react25.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_react25.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")),
- error ? (0, import_react25.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : 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 seatedCount(room2) {
@@ -11507,9 +12005,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_react25.useState)(false);
- const [confirming, setConfirming] = (0, import_react25.useState)(false);
- (0, import_react25.useEffect)(() => {
+ const [adding, setAdding] = (0, import_react26.useState)(false);
+ const [confirming, setConfirming] = (0, import_react26.useState)(false);
+ (0, import_react26.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 4e3);
return () => clearTimeout(timer);
@@ -11547,41 +12045,41 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
if (done) onDeleted?.();
});
};
- 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)(RoomAvatar, { room: room2, roster, size: 64 }),
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
+ (0, import_react26.createElement)(
"div",
{ className: "bt-bs-who" },
- (0, import_react25.createElement)("span", { className: "bt-bs-name" }, room2.name),
- (0, import_react25.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
+ (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_react25.createElement)(
+ (0, import_react26.createElement)(
Card,
{ title: t("Profile") },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
Row,
{ label: t("Name"), hint: t("Leave it empty to name the group after its members.") },
- (0, import_react25.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)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) })
),
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
Row,
{ label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true },
- (0, import_react25.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)(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_react25.createElement)(
+ (0, import_react26.createElement)(
Card,
{ title: t("Who answers") },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react25.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react25.createElement)("button", {
+ (0, import_react26.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react26.createElement)("button", {
key: id,
type: "button",
"aria-pressed": mode === id,
@@ -11591,104 +12089,104 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
if (mode !== id) void update({ mode: id });
}
}, t(label)))),
- (0, import_react25.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
+ (0, import_react26.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
)
),
- (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: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ 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: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) })
)
),
- face ? (0, import_react25.createElement)(
+ face ? (0, import_react26.createElement)(
Card,
{ title: t("Members · {count}", { count: people2.length }) },
- people2.map((person) => (0, import_react25.createElement)(FacePersonRow, { key: person.key, person, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
+ people2.map((person) => (0, import_react26.createElement)(FacePersonRow, { key: person.key, person, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
) : null,
- face ? null : (0, import_react25.createElement)(
+ face ? null : (0, import_react26.createElement)(
Card,
{ title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) },
- humans.map((person) => (0, import_react25.createElement)(
+ humans.map((person) => (0, import_react26.createElement)(
"div",
{ key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"span",
{ className: "bt-gs-who" },
- (0, import_react25.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id),
- (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Human")),
- person.role === "admin" ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Admin")) : null
+ (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
),
- humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react25.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_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
)),
members.map((member) => {
const leads = room2.admin === member.id;
- return (0, import_react25.createElement)(
+ return (0, import_react26.createElement)(
"div",
{ key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"button",
{ type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) },
- (0, import_react25.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
- (0, import_react25.createElement)("span", { className: "bt-row-title" }, member.name),
- leads ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Admin")) : null,
- (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Chief")) : null
+ (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
),
- leads ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react25.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")),
- members.length > 2 ? (0, import_react25.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_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
);
}),
- remotes.map((person) => (0, import_react25.createElement)(
+ remotes.map((person) => (0, import_react26.createElement)(
"div",
{ key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"span",
{ className: "bt-gs-who" },
- (0, import_react25.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
- (0, import_react25.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)),
+ (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)),
// #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_react25.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_react26.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_react25.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_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" ? null : adding ? [
- ...outsidePeople.map((person) => (0, import_react25.createElement)(
+ ...outsidePeople.map((person) => (0, import_react26.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_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)),
- (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Add person"))
+ (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"))
)),
- ...outside.map((bot) => (0, import_react25.createElement)(
+ ...outside.map((bot) => (0, import_react26.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_react25.createElement)(BotMark, { bot, size: 22 }),
- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, bot.name)),
- (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Add"))
+ (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"))
)),
- ...outsideRemotes.map((bot) => (0, import_react25.createElement)(
+ ...outsideRemotes.map((bot) => (0, import_react26.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_react25.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_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))),
- (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot"))
+ (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_react25.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_react25.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react25.createElement)(PlusIcon), t("Add member")) : null,
- (0, import_react25.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react25.createElement)(
+ (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)(
"span",
{ className: "bt-set-hint" },
// The host checks for two members on every change, even a rename.
@@ -11696,37 +12194,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_react25.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
+ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react26.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
),
- (0, import_react25.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
+ (0, import_react26.createElement)(
Card,
{ title: t("Sidebar") },
- (0, import_react25.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_react25.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)(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_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 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: room2.name }) : t("Delete group")),
- (0, import_react25.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-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_react25.createElement)("span", { className: "bt-chevron" }, (0, import_react25.createElement)(ChevronRightIcon))
+ (0, import_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.createElement)(ChevronRightIcon))
)
)
);
}
function NewGroupForm({ roster, actions, run, onCreated, onCancel }) {
- const [admin, setAdmin] = (0, import_react25.useState)(null);
- const [members, setMembers] = (0, import_react25.useState)([]);
- const [name, setName] = (0, import_react25.useState)("");
- const [busy, setBusy] = (0, import_react25.useState)(false);
+ 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 toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]);
const pickAdmin = (id) => {
setAdmin(id);
@@ -11742,48 +12240,48 @@ function NewGroupForm({ roster, actions, run, onCreated, onCancel }) {
if (done && room2) onCreated(room2);
else setBusy(false);
};
- const chip = (bot, on, onClick, role) => (0, import_react25.createElement)("button", {
+ const chip = (bot, on, onClick, role) => (0, import_react26.createElement)("button", {
key: bot.id,
type: "button",
role,
className: "bt-ng-chip",
"aria-checked": on,
onClick
- }, (0, import_react25.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react25.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react25.createElement)(AdminStar) : null);
- return (0, import_react25.createElement)(
+ }, (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",
{ className: "bt-set-card bt-ng" },
- (0, import_react25.createElement)(
+ (0, import_react26.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react25.createElement)(AdminStar), t("Choose the group admin"))),
- (0, import_react25.createElement)(
+ (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",
{ 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_react25.createElement)(
+ admin === null ? null : (0, import_react26.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" },
- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))),
- (0, import_react25.createElement)(
+ (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-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_react25.createElement)(
+ admin === null || members.length === 0 ? null : (0, import_react26.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-ng-step" },
- (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
- (0, import_react25.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)("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_react25.createElement)(
+ (0, import_react26.createElement)(
"div",
{ className: "bt-set-row bt-task-foot" },
- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
- (0, import_react25.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_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"))
)
);
}
@@ -11817,13 +12315,13 @@ button.bt-face-bot:hover{color:var(--bt-ink);text-decoration:underline;text-unde
`;
// src/client/palette.js
-var import_react26 = require("react");
+var import_react27 = 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_react26.useState)(start ?? "");
- const [cursor, setCursor] = (0, import_react26.useState)(0);
- const [leaving, setLeaving] = (0, import_react26.useState)(false);
+ 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 wanted = query.trim().toLowerCase();
const close = () => {
if (leaving) return;
@@ -11859,7 +12357,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_react26.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }),
+ avatar: (0, import_react27.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,
@@ -11870,7 +12368,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) {
})),
...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({
key: room2.id,
- avatar: (0, import_react26.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
+ avatar: (0, import_react27.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,
@@ -11883,7 +12381,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_react26.createElement)("span", { className: "bt-palette-agent" }, (0, import_react26.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
+ avatar: (0, import_react27.createElement)("span", { className: "bt-palette-agent" }, (0, import_react27.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
title: "DSH Agent",
subtitle: t("Plain dsh session"),
run: () => actions.openAgent(agent.id)
@@ -11896,16 +12394,16 @@ function CommandPalette({ start, token, roster, actions, useSessions }) {
key: `${id}:${turn}`,
title: bot?.name ?? room2?.name ?? t("Bot"),
subtitle: snippet,
- avatar: bot ? (0, import_react26.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react26.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
+ avatar: bot ? (0, import_react27.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react27.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_react26.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") },
- { key: "cmd:bot", icon: (0, import_react26.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") },
- { key: "cmd:group", icon: (0, import_react26.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") },
- { key: "cmd:settings", icon: (0, import_react26.createElement)(GearIcon), title: t("Settings"), run: () => actions.openSettings("general") }
+ { 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);
@@ -11915,7 +12413,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) {
${shown}`, { axis: "y", ...QUICK_EDGES });
const listRef = glide.box;
useFlip(listRef, shown, "[data-flip]");
- (0, import_react26.useEffect)(() => {
+ (0, import_react27.useEffect)(() => {
listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" });
}, [active, query]);
const choose = (row) => {
@@ -11924,18 +12422,18 @@ ${shown}`, { axis: "y", ...QUICK_EDGES });
close();
};
let index = -1;
- return (0, import_react26.createElement)(
+ return (0, import_react27.createElement)(
"div",
{ className: "bt-palette-layer", "data-leaving": leaving || void 0 },
- (0, import_react26.createElement)("div", { className: "bt-backdrop", onMouseDown: close }),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)("div", { className: "bt-backdrop", onMouseDown: close }),
+ (0, import_react27.createElement)(
"div",
{ className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-palette-head" },
- (0, import_react26.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react26.createElement)(SearchIcon)),
- (0, import_react26.createElement)("input", {
+ (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"),
@@ -11961,29 +12459,29 @@ ${shown}`, { axis: "y", ...QUICK_EDGES });
}
}
}),
- query ? (0, import_react26.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
+ query ? (0, import_react27.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
setQuery("");
setCursor(0);
- } }, (0, import_react26.createElement)(CloseIcon)) : null
+ } }, (0, import_react27.createElement)(CloseIcon)) : null
),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") },
- (0, import_react26.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- rows.length === 0 ? (0, import_react26.createElement)(
+ (0, import_react27.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ rows.length === 0 ? (0, import_react27.createElement)(
"div",
{ className: "bt-palette-empty" },
- (0, import_react26.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react26.createElement)(SearchIcon)),
- (0, import_react26.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")),
- (0, import_react26.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message"))
- ) : sections.map(([title, items]) => (0, import_react26.createElement)(
- import_react26.Fragment,
+ (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,
{ key: title },
- (0, import_react26.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
+ (0, import_react27.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
items.map((row) => {
index += 1;
const at = index;
- return (0, import_react26.createElement)(
+ return (0, import_react27.createElement)(
"button",
{
key: row.key,
@@ -11997,14 +12495,14 @@ ${shown}`, { axis: "y", ...QUICK_EDGES });
},
onClick: () => choose(row)
},
- (0, import_react26.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react26.createElement)("span", { className: "bt-palette-cmd" }, row.icon)),
- (0, import_react26.createElement)(
+ (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)(
"span",
{ className: "bt-palette-text" },
- (0, import_react26.createElement)("span", { className: "bt-palette-title" }, row.title),
- row.subtitle ? (0, import_react26.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null
+ (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
),
- row.badge ? (0, import_react26.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null
+ row.badge ? (0, import_react27.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null
);
})
))
@@ -12046,7 +12544,7 @@ async function afterCreate({ id, text, actions }) {
}
// src/client/create-bot.js
-var import_react27 = require("react");
+var import_react28 = require("react");
var pick = (list) => list[Math.floor(Math.random() * list.length)];
var randomLook = (avoid = {}) => {
const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape);
@@ -12054,17 +12552,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_react27.useState)(() => randomLook());
- const [image, setImage] = (0, import_react27.useState)(null);
- const [name, setName] = (0, import_react27.useState)(start?.name ?? "");
- const [model, setModel] = (0, import_react27.useState)(() => startModel(roster));
- const [effort, setEffort] = (0, import_react27.useState)(void 0);
- const [busy, setBusy] = (0, import_react27.useState)(false);
- const [error, setError] = (0, import_react27.useState)("");
- const fileRef = (0, import_react27.useRef)(null);
- const frame = (0, import_react27.useRef)(null);
+ 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);
useEscape(onBack);
- (0, import_react27.useEffect)(() => {
+ (0, import_react28.useEffect)(() => {
if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" });
}, []);
const fallbackName = freshBotName(roster, t("New Bot"));
@@ -12089,25 +12587,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_react27.createElement)(
+ const card = (0, import_react28.createElement)(
"div",
{ className: "bt-create-card" },
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)(
"div",
{ className: "bt-create-preview" },
// The key remounts the mark, so every new pick plays the pop.
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)(
"span",
{ key: `${look.shape}:${look.color}:${image ? "photo" : ""}`, className: "bt-create-pop" },
- (0, import_react27.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_react27.createElement)("span", { className: "bt-create-name" }, finalName)
+ (0, import_react28.createElement)("span", { className: "bt-create-name" }, finalName)
),
- (0, import_react27.createElement)("div", { className: "bt-create-step" }, t("Look")),
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)("div", { className: "bt-create-step" }, t("Look")),
+ (0, import_react28.createElement)(
"div",
{ className: "bt-create-looks" },
- (0, import_react27.createElement)(CharacterGrid, {
+ (0, import_react28.createElement)(CharacterGrid, {
look,
hasPhoto: Boolean(image),
onShape: (shape) => {
@@ -12119,27 +12617,27 @@ function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inlin
setLook((current) => ({ ...current, color }));
}
}),
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)(
"div",
{ className: "bt-actions bt-create-lookbar" },
- (0, import_react27.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: () => {
setImage(null);
setLook((current) => randomLook(current));
} }, t("Shuffle")),
- (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")),
- image ? (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null,
- (0, import_react27.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
+ (0, import_react28.createElement)("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) => {
const file = event.target.files?.[0];
event.target.value = "";
upload(file);
} })
)
),
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)(
"label",
{ className: "bt-field" },
- (0, import_react27.createElement)("span", { className: "bt-field-label" }, t("Name")),
- (0, import_react27.createElement)("input", {
+ (0, import_react28.createElement)("span", { className: "bt-field-label" }, t("Name")),
+ (0, import_react28.createElement)("input", {
className: "bt-input",
autoFocus: !inline,
value: name,
@@ -12154,59 +12652,59 @@ function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inlin
}
}
}),
- problem ? (0, import_react27.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react27.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
),
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)(
"div",
{ className: "bt-field" },
- (0, import_react27.createElement)("span", { className: "bt-field-label" }, t("Model")),
- (0, import_react27.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) => {
setModel(ref);
setEffort(void 0);
}, onPickEffort: setEffort }),
- (0, import_react27.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."))
),
- error ? (0, import_react27.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
- (0, import_react27.createElement)(
+ error ? (0, import_react28.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
+ (0, import_react28.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_react27.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null,
- (0, import_react27.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_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"))
)
);
- if (inline) return (0, import_react27.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card);
- return (0, import_react27.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)(
"div",
{ className: "bt-create", role: "dialog", "aria-label": t("Create Bot") },
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)(
"div",
{ className: "bt-create-head" },
- canGoBack ? (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react27.createElement)(ChevronLeftIcon)) : (0, import_react27.createElement)("span", { className: "bt-create-gap" }),
- (0, import_react27.createElement)("span", { className: "bt-create-title" }, t("Create Bot")),
- (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react27.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))
),
- (0, import_react27.createElement)("div", { className: "bt-create-body" }, card)
+ (0, import_react28.createElement)("div", { className: "bt-create-body" }, card)
);
}
// src/client/new-chat.js
-var import_react28 = require("react");
+var import_react29 = require("react");
function NewChat({ mode, roster, actions }) {
- const [query, setQuery] = (0, import_react28.useState)("");
- const [picked, setPicked] = (0, import_react28.useState)([]);
- const [group, setGroup] = (0, import_react28.useState)(mode === "group");
- const [compose, setCompose] = (0, import_react28.useState)(mode !== "new");
- const [admin, setAdmin] = (0, import_react28.useState)(null);
- const [message, setMessage] = (0, import_react28.useState)("");
- const [busy, setBusy] = (0, import_react28.useState)(false);
- const [error, setError] = (0, import_react28.useState)("");
- const [fix, setFix] = (0, import_react28.useState)(null);
- const [fixNote, setFixNote] = (0, import_react28.useState)("");
- const [inviteLink, setInviteLink] = (0, import_react28.useState)("");
- const [cursor, setCursor] = (0, import_react28.useState)(0);
- const [draft, setDraft] = (0, import_react28.useState)(mode === "create" ? { name: "", fromList: false } : null);
- const inputRef = (0, import_react28.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 close = () => actions.closeOverlay();
useEscape(close);
const paneLeft = usePaneLeft();
@@ -12337,14 +12835,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_react28.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => {
+ options.push({ key: "chat", icon: (0, import_react29.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_react28.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_react28.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => {
+ 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: () => {
setGroup(true);
setCompose(true);
setCursor(0);
@@ -12353,7 +12851,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_react28.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react28.createElement)(PeopleIcon) }) : (0, import_react28.createElement)(PeopleIcon),
+ 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),
label: t(action.label),
hint: action.hint ? t(action.hint) : void 0,
run: () => {
@@ -12365,7 +12863,7 @@ function NewChat({ mode, roster, actions }) {
});
}
if ((roster.agents ?? []).length === 0) {
- options.push({ key: "agent", icon: (0, import_react28.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_react29.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => {
await actions.addAgent();
close();
}) });
@@ -12376,19 +12874,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_react28.createElement)(PlusIcon), label: t("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 });
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_react28.useRef)(null);
+ const drop = (0, import_react29.useRef)(null);
useMorph(drop, listed);
if (draft) {
- return (0, import_react28.createElement)(
+ return (0, import_react29.createElement)(
"div",
{ className: "bt-pane", style: { left: paneLeft } },
- (0, import_react28.createElement)(CreateBot, {
+ (0, import_react29.createElement)(CreateBot, {
roster,
actions,
start: draft,
@@ -12408,7 +12906,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_react28.createElement)(
+ return (0, import_react29.createElement)(
"div",
{
className: "bt-pane",
@@ -12418,31 +12916,31 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
if (event.target === event.currentTarget) close();
} : void 0
},
- (0, import_react28.createElement)(
+ (0, import_react29.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_react28.createElement)(
+ menuOnly ? null : (0, import_react29.createElement)(
"div",
{ className: "bt-to" },
- (0, import_react28.createElement)("span", { className: "bt-to-label" }, t("To:")),
- adminBot ? (0, import_react28.createElement)(
+ (0, import_react29.createElement)("span", { className: "bt-to-label" }, t("To:")),
+ adminBot ? (0, import_react29.createElement)(
"span",
{ key: adminBot.id, className: "bt-token bt-token-admin", title: t("Admin") },
- (0, import_react28.createElement)(BotMark, { bot: adminBot, size: 16 }),
+ (0, import_react29.createElement)(BotMark, { bot: adminBot, size: 16 }),
adminBot.name,
- (0, import_react28.createElement)(AdminStar),
- (0, import_react28.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) }, "×")
) : null,
- choosingAdmin ? (0, import_react28.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react28.createElement)(AdminStar)) : null,
- pickedBots.map((bot) => (0, import_react28.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)(
"span",
{ key: bot.id, className: "bt-token" },
- (0, import_react28.createElement)(BotMark, { bot, size: 16 }),
+ (0, import_react29.createElement)(BotMark, { bot, size: 16 }),
bot.name,
- (0, import_react28.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_react28.createElement)("input", {
+ (0, import_react29.createElement)("input", {
ref: inputRef,
autoFocus: true,
value: query,
@@ -12466,20 +12964,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
} else if (event.key === "Backspace" && query === "") unpick();
}
}),
- group && admin !== null ? (0, import_react28.createElement)("button", {
+ group && admin !== null ? (0, import_react29.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_react28.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_react28.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react28.createElement)(
+ (0, import_react29.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react29.createElement)(
"div",
{ ref: glide.box, role: "listbox", "aria-label": t("Recipients"), className: "bt-drop-list" },
- (0, import_react28.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- options.length === 0 ? (0, import_react28.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_react28.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)(
"button",
{
key: option.key,
@@ -12491,36 +12989,36 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
onMouseEnter: () => setCursor(index),
onClick: option.run
},
- option.bot ? (0, import_react28.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react28.createElement)("span", { className: "bt-option-icon" }, option.icon),
+ option.bot ? (0, import_react29.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react29.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_react28.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react28.createElement)("span", { className: "bt-option-copy" }, (0, import_react28.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react28.createElement)("span", { className: "bt-option-sub" }, option.hint)),
- option.bot && option.hint && index === active ? (0, import_react28.createElement)("span", { className: "bt-option-hint" }, option.hint) : null
+ 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
))
)),
- menuOnly ? null : (0, import_react28.createElement)("div", { className: "bt-nc-spacer" }),
- error && fix ? (0, import_react28.createElement)(
+ menuOnly ? null : (0, import_react29.createElement)("div", { className: "bt-nc-spacer" }),
+ error && fix ? (0, import_react29.createElement)(
"div",
{ className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" },
- (0, import_react28.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_react28.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_react28.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => {
+ (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: () => {
void copyInvite(bot);
} }, t("Copy invite link for {name}", { name: bot.name })) : null,
- (0, import_react28.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_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 }))
])),
- fixNote ? (0, import_react28.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null,
- inviteLink ? (0, import_react28.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_react28.createElement)("div", { className: "bt-error" }, error) : null,
- menuOnly ? null : (0, import_react28.createElement)(
+ 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)(
"div",
{ className: "bt-nc-compose" },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ className: "bt-nc-card" },
- (0, import_react28.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react28.createElement)(PlusIcon)),
- (0, import_react28.createElement)("textarea", {
+ (0, import_react29.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react29.createElement)(PlusIcon)),
+ (0, import_react29.createElement)("textarea", {
rows: 1,
value: message,
placeholder,
@@ -12533,7 +13031,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
}
}
}),
- (0, import_react28.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react28.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))
)
)
)
@@ -12541,18 +13039,18 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
}
// src/client/exchange.js
-var import_react29 = require("react");
+var import_react30 = require("react");
var SHEET_EXIT_MS = 220;
function ExchangeDialog({ pair, roster, actions }) {
- const [entries2, setEntries] = (0, import_react29.useState)(null);
- const [leaving, setLeaving] = (0, import_react29.useState)(false);
+ const [entries2, setEntries] = (0, import_react30.useState)(null);
+ const [leaving, setLeaving] = (0, import_react30.useState)(false);
const close = () => {
if (leaving) return;
setLeaving(true);
setTimeout(() => actions.closeOverlay(), SHEET_EXIT_MS);
};
useEscape(close);
- (0, import_react29.useEffect)(() => {
+ (0, import_react30.useEffect)(() => {
let stop = false;
let timer;
let seen = null;
@@ -12576,48 +13074,48 @@ function ExchangeDialog({ pair, roster, actions }) {
const right = roster.byId[pair[1]];
let lastDay = 0;
const paneLeft = usePaneLeft();
- return (0, import_react29.createElement)(
+ return (0, import_react30.createElement)(
"div",
{ className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"div",
{ className: "bt-dialog", role: "dialog", "aria-label": t("Bot exchange") },
- (0, import_react29.createElement)("div", { className: "bt-head" }, (0, import_react29.createElement)(
+ (0, import_react30.createElement)("div", { className: "bt-head" }, (0, import_react30.createElement)(
"span",
{ className: "bt-pill", style: { cursor: "default" } },
- left ? (0, import_react29.createElement)(BotMark, { bot: left, size: 22 }) : null,
+ left ? (0, import_react30.createElement)(BotMark, { bot: left, size: 22 }) : null,
left?.name ?? "?",
- (0, import_react29.createElement)("span", { className: "bt-swap" }, "⇄"),
- right ? (0, import_react29.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,
right?.name ?? "?"
)),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"div",
{ className: "bt-dialog-log", role: "log", "aria-label": t("Bot exchange transcript") },
- entries2 === null ? (0, import_react29.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react29.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) => {
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_react29.createElement)(
- import_react29.Fragment,
+ return (0, import_react30.createElement)(
+ import_react30.Fragment,
{ key: entry.id },
- showDay ? (0, import_react29.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
- (0, import_react29.createElement)(
+ showDay ? (0, import_react30.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
+ (0, import_react30.createElement)(
"div",
{ className: "bt-group-msg" },
- (0, import_react29.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name),
- (0, import_react29.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react29.createElement)("span", { className: "bt-lead" }, (0, import_react29.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_react29.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react29.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")))
)
);
}
// src/client/voice.js
-var import_react30 = require("react");
+var import_react31 = 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",
@@ -12629,8 +13127,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_react30.useState)(0);
- (0, import_react30.useEffect)(() => {
+ const [level, setLevel] = (0, import_react31.useState)(0);
+ (0, import_react31.useEffect)(() => {
if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0;
let stream = null;
let audio = null;
@@ -12680,18 +13178,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_react30.useState)(() => actions.speech());
- const [phase, setPhase] = (0, import_react30.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
- const [catalog, setCatalog] = (0, import_react30.useState)(null);
- const [heard, setHeard] = (0, import_react30.useState)("");
- const [said, setSaid] = (0, import_react30.useState)("");
- const [note, setNote] = (0, import_react30.useState)("");
- const [serviceLevel, setServiceLevel] = (0, import_react30.useState)(0);
- const [cycle, setCycle] = (0, import_react30.useState)(0);
- const waiting = (0, import_react30.useRef)(null);
- const reply = (0, import_react30.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);
reply.current = lastReply;
- const turn = (0, import_react30.useRef)(0);
+ const turn = (0, import_react31.useRef)(0);
const paneLeft = usePaneLeft();
const close = () => {
turn.current += 1;
@@ -12711,12 +13209,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
dictation.stop();
submit(text);
});
- (0, import_react30.useEffect)(() => {
+ (0, import_react31.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
}, [note]);
- (0, import_react30.useEffect)(() => {
+ (0, import_react31.useEffect)(() => {
if (!speech) return void 0;
let live = true;
speechCatalog(speech).then(({ catalog: next, usable }) => {
@@ -12731,16 +13229,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
};
}, [speech]);
const listen = phase === "listening";
- (0, import_react30.useEffect)(() => {
+ (0, import_react31.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_react30.useEffect)(() => {
+ (0, import_react31.useEffect)(() => {
if (phase !== "listening") dictation.stop();
}, [phase]);
- (0, import_react30.useEffect)(() => {
+ (0, import_react31.useEffect)(() => {
if (!speech || !catalog || !listen) return void 0;
const own = turn.current;
let capture = null;
@@ -12802,7 +13300,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
setServiceLevel(0);
};
}, [speech, catalog, listen, cycle]);
- (0, import_react30.useEffect)(() => {
+ (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;
@@ -12822,7 +13320,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
window.speechSynthesis.cancel();
window.speechSynthesis.speak(utterance);
}, [phase, running, lastReply]);
- (0, import_react30.useEffect)(() => () => window.speechSynthesis?.cancel(), []);
+ (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";
@@ -12839,44 +13337,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_react30.createElement)(
+ return (0, import_react31.createElement)(
"div",
{ className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
- (0, import_react30.createElement)(
+ (0, import_react31.createElement)(
"div",
{ className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase },
- (0, import_react30.createElement)(
+ (0, import_react31.createElement)(
"div",
{ className: "bt-voice-orb", style: { "--bt-level": level } },
- (0, import_react30.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
- (0, import_react30.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
- bot ? (0, import_react30.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
+ (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_react30.createElement)("div", { className: "bt-voice-name" }, name),
- (0, import_react30.createElement)(
+ (0, import_react31.createElement)("div", { className: "bt-voice-name" }, name),
+ (0, import_react31.createElement)(
"div",
{ className: "bt-voice-status", role: "status" },
- phase === "thinking" ? (0, import_react30.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react30.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
+ 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_react30.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
- openSetup ? (0, import_react30.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
+ (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_react30.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
+ shownNote ? (0, import_react31.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
),
- (0, import_react30.createElement)(
+ (0, import_react31.createElement)(
"div",
{ className: "bt-voice-bar" },
- (0, import_react30.createElement)("button", {
+ (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_react30.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react30.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
- (0, import_react30.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react30.createElement)(CloseIcon))
+ }, (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))
)
);
}
@@ -13014,7 +13512,7 @@ function percent(part, whole) {
}
// src/client/usage-page.js
-var import_react31 = require("react");
+var import_react32 = require("react");
var HEAT_WEEKS = 53;
var TOP_SERIES = 7;
var RING_R = 42;
@@ -13061,9 +13559,9 @@ function titleOf(range, bucket) {
return dates({ year: "numeric", month: "long" }).format(bucket.start);
}
function useTween(value) {
- const [shown, setShown] = (0, import_react31.useState)(0);
- const from = (0, import_react31.useRef)(0);
- (0, import_react31.useEffect)(() => {
+ const [shown, setShown] = (0, import_react32.useState)(0);
+ const from = (0, import_react32.useRef)(0);
+ (0, import_react32.useEffect)(() => {
if (reducedMotion() || typeof requestAnimationFrame !== "function") {
from.current = value;
setShown(value);
@@ -13083,9 +13581,9 @@ function useTween(value) {
}, [value]);
return shown;
}
-var glyphBox = (size, icon3) => (0, import_react31.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react31.createElement)(icon3));
+var glyphBox = (size, icon3) => (0, import_react32.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react32.createElement)(icon3));
function ModelGlyph({ name, paint, size }) {
- return (0, import_react31.createElement)("span", {
+ return (0, import_react32.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))` }
@@ -13102,7 +13600,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_react31.createElement)(BotAvatar, { bot, size, badge: false, live: false })
+ glyph: (size) => (0, import_react32.createElement)(BotAvatar, { bot, size, badge: false, live: false })
};
}
if (owner.kind === "room") {
@@ -13112,7 +13610,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_react31.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
+ glyph: (size) => room2 ? (0, import_react32.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
};
}
if (owner.kind === "agent") {
@@ -13134,30 +13632,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_react31.createElement)(
+ return (0, import_react32.createElement)(
"div",
{ ref: glide.box, className: "bt-us-tabs", role: "group", "aria-label": label },
- (0, import_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- options.map(([id, text, icon3]) => (0, import_react31.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react31.createElement)(icon3) : 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))
);
}
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_react31.createElement)("div", { className: "bt-us-stat" }, (0, import_react31.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react31.createElement)("span", { className: "bt-us-stat-value" }, value));
- return (0, import_react31.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)(
"section",
{ className: "bt-us-stats", style: { "--i": 0 } },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-us-stat bt-us-stat-main" },
- (0, import_react31.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
+ (0, import_react32.createElement)(
"span",
{ className: "bt-us-stat-value" },
compact(shown),
- (0, import_react31.createElement)("small", null, t("tokens")),
- change === null ? null : (0, import_react31.createElement)(
+ (0, import_react32.createElement)("small", null, t("tokens")),
+ change === null ? null : (0, import_react32.createElement)(
"span",
{ className: "bt-us-delta", title: compareText(range) },
`${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%`
@@ -13175,83 +13673,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_react31.useState)(null);
+ const [hover, setHover] = (0, import_react32.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_react31.createElement)(
+ return (0, import_react32.createElement)(
"section",
{ className: "bt-us-card bt-us-chart", style: { "--i": 1 } },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-us-card-head" },
- (0, import_react31.createElement)("h3", null, t("Tokens over time"), (0, import_react31.createElement)("span", null, unitLabel(range.unit))),
- onBy ? (0, import_react31.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_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-us-chart-body" },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-us-plot", "data-hover": hover === null ? void 0 : "", onMouseLeave: () => setHover(null) },
- [1, 0.5].map((level) => (0, import_react31.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react31.createElement)("span", null, compact(top * level)))),
- (0, import_react31.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react31.createElement)("span", null, "0")),
- (0, import_react31.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)(
"div",
{ className: "bt-us-cols" },
- columns.map((column, at) => (0, import_react31.createElement)("div", {
+ columns.map((column, at) => (0, import_react32.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_react31.createElement)(
+ }, column.total > 0 ? (0, import_react32.createElement)(
"div",
{ className: "bt-us-stack", style: { height: `${column.total / top * 100}%`, "--i": at } },
- column.parts.map(([key, value]) => value > 0 ? (0, import_react31.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null)
- ) : (0, import_react31.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" })))
),
- tip ? (0, import_react31.createElement)(
+ tip ? (0, import_react32.createElement)(
"div",
{ className: "bt-us-tip", style: tipSide },
- (0, import_react31.createElement)("div", { className: "bt-us-tip-title" }, tip.title),
- (0, import_react31.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_react31.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)(
"div",
{ key, className: "bt-us-tip-row" },
- (0, import_react31.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }),
- (0, import_react31.createElement)("span", null, tipOf.get(key)?.name),
- (0, import_react31.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))
))
) : null
),
- (0, import_react31.createElement)(
+ (0, import_react32.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_react31.createElement)(
+ columns.map((column, at) => (0, import_react32.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_react31.createElement)(
+ series.length > 1 || series[0]?.pick ? (0, import_react32.createElement)(
"div",
{ className: "bt-us-legend" },
- series.map((entry) => (0, import_react31.createElement)(entry.pick ? "button" : "span", {
+ series.map((entry) => (0, import_react32.createElement)(entry.pick ? "button" : "span", {
key: entry.key,
className: "bt-us-key",
...entry.pick ? { type: "button", onClick: () => onPick(entry.pick) } : {}
- }, (0, import_react31.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))
) : 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_react31.createElement)(
+ return (0, import_react32.createElement)(
onClick ? "button" : "div",
{
className: "bt-us-row",
@@ -13260,69 +13758,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_react31.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
- (0, import_react31.createElement)(
+ glyph ? (0, import_react32.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
+ (0, import_react32.createElement)(
"span",
{ className: "bt-us-row-body" },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"span",
{ className: "bt-us-row-line" },
- (0, import_react31.createElement)("span", { className: "bt-us-row-name" }, name),
- sub ? (0, import_react31.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_react31.createElement)("span", { className: "bt-us-track" }, (0, import_react31.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_react31.createElement)("span", { className: "bt-us-row-num" }, (0, import_react31.createElement)("b", null, compact(sum.total)), (0, import_react31.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)}%`))
);
}
function ListCard({ title, count, index, children }) {
- return (0, import_react31.createElement)(
+ return (0, import_react32.createElement)(
"section",
{ className: "bt-us-card", style: { "--i": index } },
- (0, import_react31.createElement)("div", { className: "bt-us-card-head" }, (0, import_react31.createElement)("h3", null, title, count === void 0 ? null : (0, import_react31.createElement)("span", null, count))),
- (0, import_react31.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)
);
}
function Profile({ face, share, range }) {
const arc = Math.max(0, Math.min(1, share)) * RING2;
- return (0, import_react31.createElement)(
+ return (0, import_react32.createElement)(
"section",
{ className: "bt-us-profile" },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"span",
{ className: "bt-us-ring" },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"svg",
{ viewBox: "0 0 96 96", "aria-hidden": true },
- face.stops ? (0, import_react31.createElement)("defs", null, (0, import_react31.createElement)(
+ face.stops ? (0, import_react32.createElement)("defs", null, (0, import_react32.createElement)(
"linearGradient",
{ id: "bt-us-ring-paint", x1: 0, y1: 1, x2: 1, y2: 0 },
- face.stops.map(([offset, color]) => (0, import_react31.createElement)("stop", { key: offset, offset, stopColor: color }))
+ face.stops.map(([offset, color]) => (0, import_react32.createElement)("stop", { key: offset, offset, stopColor: color }))
)) : null,
- (0, import_react31.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }),
- arc > 0 ? (0, import_react31.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)("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_react31.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_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-us-profile-copy" },
- (0, import_react31.createElement)("h3", null, face.name),
- face.sub ? (0, import_react31.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null,
- (0, import_react31.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) }))
)
);
}
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_react31.useRef)(null);
+ const box = (0, import_react32.useRef)(null);
const todayMs = hourMs(today);
const thisMonth = monthOf(todayMs);
- const [right, setRight] = (0, import_react31.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today))));
- const [anchor, setAnchor] = (0, import_react31.useState)(null);
- const [hover, setHover] = (0, import_react31.useState)(null);
- (0, import_react31.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 onDown = (event) => {
if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose();
};
@@ -13348,17 +13846,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
const [from, to] = [anchor, key].sort();
onPick(from, to);
};
- const weekdays = (0, import_react31.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 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_react31.createElement)("span", { key: `b${at}` }));
+ const cells = Array.from({ length: lead }, (_, at) => (0, import_react32.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_react31.createElement)("button", {
+ cells.push((0, import_react32.createElement)("button", {
key,
type: "button",
className: "bt-us-day",
@@ -13374,15 +13872,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
"data-has": flag(active.has(key)),
onMouseEnter: () => setHover(key),
onClick: () => press(key)
- }, (0, import_react31.createElement)("b", null, day)));
+ }, (0, import_react32.createElement)("b", null, day)));
}
- const nav = side === "left" ? (0, import_react31.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_react31.createElement)(ChevronLeftIcon)) : (0, import_react31.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_react31.createElement)(ChevronRightIcon));
- return (0, import_react31.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)(
"div",
{ key: start, className: "bt-us-month" },
- (0, import_react31.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react31.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))),
- (0, import_react31.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react31.createElement)("span", { key: at }, name))),
- (0, import_react31.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)
);
};
const shortcuts = [
@@ -13391,30 +13889,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_react31.createElement)(
+ return (0, import_react32.createElement)(
"div",
{ ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-us-pick-quick" },
- shortcuts.map(([id, label, from, to]) => (0, import_react31.createElement)("button", {
+ shortcuts.map(([id, label, from, to]) => (0, import_react32.createElement)("button", {
key: id,
type: "button",
"aria-pressed": span?.from === from && span?.to === to,
onClick: () => onPick(from, to)
}, label))
),
- (0, import_react31.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
+ (0, import_react32.createElement)(
"div",
{ className: "bt-us-pick-foot", role: "status" },
- (0, import_react31.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")),
- low === null ? null : (0, import_react31.createElement)("span", null, (0, import_react31.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)}`)
)
);
}
function Heat({ view, keep, paint, index }) {
- const [hover, setHover] = (0, import_react31.useState)(null);
+ const [hover, setHover] = (0, import_react32.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);
@@ -13430,13 +13928,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_react31.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)));
}
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_react31.createElement)("span", {
+ cells.push((0, import_react32.createElement)("span", {
key: ms,
className: "bt-us-cell",
style: { "--w": week },
@@ -13444,7 +13942,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_react31.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));
}
}
const tipSide = hover ? {
@@ -13453,61 +13951,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_react31.createElement)(
+ return (0, import_react32.createElement)(
"section",
{ className: "bt-us-card bt-us-heat", style: { "--i": index } },
- (0, import_react31.createElement)("div", { className: "bt-us-card-head" }, (0, import_react31.createElement)("h3", null, t("Activity")), (0, import_react31.createElement)("span", { className: "bt-us-head-note" }, summary)),
- (0, import_react31.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months),
- (0, import_react31.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)(
"div",
{ className: "bt-us-heat-plot", onMouseLeave: () => setHover(null) },
- (0, import_react31.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
- hover ? (0, import_react31.createElement)(
+ (0, import_react32.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
+ hover ? (0, import_react32.createElement)(
"div",
{ className: "bt-us-tip bt-us-heat-tip", style: tipSide },
- (0, import_react31.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)),
- (0, import_react31.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) }))
) : null
),
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-us-heat-scale", "aria-hidden": true },
t("Light"),
- levels.map((level) => (0, import_react31.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react31.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) } }))),
t("Heavy")
)
);
}
function Skeleton2() {
- return (0, import_react31.createElement)(
+ return (0, import_react32.createElement)(
"div",
{ className: "bt-us", "aria-busy": true },
- (0, import_react31.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
+ (0, import_react32.createElement)(
"div",
{ className: "bt-us-sheet bt-us-skel-sheet" },
- (0, import_react31.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
- (0, import_react31.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
- (0, import_react31.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)(
"div",
{ className: "bt-us-pair" },
- (0, import_react31.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }),
- (0, import_react31.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 } })
)
)
);
}
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_react31.useState)(null);
- const [error, setError] = (0, import_react31.useState)("");
- const [rangeId, setRangeId] = (0, import_react31.useState)("7d");
- const [span, setSpan] = (0, import_react31.useState)(null);
- const [picking, setPicking] = (0, import_react31.useState)(false);
- const [by, setBy] = (0, import_react31.useState)("bot");
- const tabs = (0, import_react31.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 focus = focusOf(focusId);
- (0, import_react31.useEffect)(() => {
+ (0, import_react32.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(() => {
@@ -13517,7 +14015,7 @@ function UsagePage({ roster, actions, focusId }) {
const value = await actions.usage();
await resized;
if (stopped) return;
- (0, import_react31.startTransition)(() => setView(value));
+ (0, import_react32.startTransition)(() => setView(value));
setError("");
timer = setTimeout(load, value.scanning ? 3e3 : 2e4);
} catch (failure) {
@@ -13534,7 +14032,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_react31.useMemo)(() => {
+ const shaped = (0, import_react32.useMemo)(() => {
if (!view) return null;
const faces2 = view.owners.map((owner) => ownerFace(owner, roster));
const lifetime = /* @__PURE__ */ new Map();
@@ -13543,7 +14041,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_react31.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 }) };
});
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;
@@ -13591,23 +14089,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_react31.useRef)(null);
- (0, import_react31.useEffect)(() => {
+ const root = (0, import_react32.useRef)(null);
+ (0, import_react32.useEffect)(() => {
root.current?.closest(".bt-settings-scroll")?.scrollTo?.({ top: 0 });
}, [focusId]);
if (!view) {
- return error ? (0, import_react31.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react31.createElement)(Skeleton2);
+ return error ? (0, import_react32.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react32.createElement)(Skeleton2);
}
if (view.rows.length === 0) {
- return (0, import_react31.createElement)(
+ return (0, import_react32.createElement)(
"div",
{ className: "bt-us" },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-us-empty" },
- (0, import_react31.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }),
- (0, import_react31.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")),
- (0, import_react31.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."))
)
);
}
@@ -13632,7 +14130,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_react31.createElement)(Row2, {
+ const ownerRow = ([at, sum], index, total = whole) => (0, import_react32.createElement)(Row2, {
key: `o${at}`,
kind: faces[at].kind,
glyph: faces[at].glyph(32),
@@ -13644,7 +14142,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_react31.createElement)(Row2, {
+ const modelRow = ([at, sum], index) => (0, import_react32.createElement)(Row2, {
key: `m${at}`,
kind: "model",
glyph: models[at].glyph(32),
@@ -13656,30 +14154,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_react31.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."));
let body;
if (face) {
const kinds = ranked(part.kinds);
const usedModels = ranked(part.models);
const owners = ranked(part.owners);
body = [
- (0, import_react31.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
+ (0, import_react32.createElement)(
"div",
{ key: "sheet", className: "bt-us-sheet" },
- (0, import_react31.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
- (0, import_react31.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
- (0, import_react31.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)(
"div",
{ key: "pair", className: "bt-us-pair" },
- ownerAt >= 0 ? (0, import_react31.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react31.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_react31.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)(
ListCard,
{ title: t("What for"), index: 3 },
- kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react31.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
)
),
- (0, import_react31.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 })
)
];
} else {
@@ -13688,39 +14186,39 @@ function UsagePage({ roster, actions, focusId }) {
const rest = owners.filter(([at]) => !isBot(at));
const usedModels = ranked(all.models);
body = [
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ key: "sheet", className: "bt-us-sheet" },
- (0, import_react31.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
- (0, import_react31.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
- (0, import_react31.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)(
"div",
{ key: "pair", className: "bt-us-pair" },
- (0, import_react31.createElement)(
+ (0, import_react32.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_react31.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,
rest.map((entry, index) => ownerRow(entry, bots.length + index))
),
- (0, import_react31.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_react31.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 })
)
];
}
- return (0, import_react31.createElement)(
+ return (0, import_react32.createElement)(
"div",
{ ref: root, className: "bt-us", key: focusId ?? "all" },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-us-top" },
- (0, import_react31.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }),
- view.scanning ? (0, import_react31.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null,
- picking ? (0, import_react31.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
),
- error ? (0, import_react31.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,
body
);
}
@@ -13877,7 +14375,7 @@ button.bt-us-row:hover{background:var(--bt-hover)}
`;
// src/client/connectors.js
-var import_react32 = require("react");
+var import_react33 = require("react");
var TOKEN_URL = "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/settings/personal-access-tokens/new";
var COMPOSIO_KEY_URL = "https://platform.composio.dev/";
var APPS = [
@@ -13910,10 +14408,10 @@ var APPS = [
["youtube", "YouTube"]
];
var appName = (slug) => APPS.find(([id]) => id === slug)?.[1] ?? slug;
-var GitHubMark = () => (0, import_react32.createElement)(
+var GitHubMark = () => (0, import_react33.createElement)(
"svg",
{ width: 20, height: 20, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": true },
- (0, import_react32.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" })
);
var MARKS = { github: GitHubMark, composio: ConnectorIcon };
var ABOUT = {
@@ -13930,20 +14428,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_react32.createElement)(
+ return (0, import_react33.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_react32.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
+ label ? (0, import_react33.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
);
}
function AppPicker({ apps, onChange, search, statuses = {} }) {
- const [query, setQuery] = (0, import_react32.useState)("");
- const [found, setFound] = (0, import_react32.useState)(null);
- const [failed, setFailed] = (0, import_react32.useState)(null);
- const names = (0, import_react32.useRef)(new Map(APPS));
- const asked = (0, import_react32.useRef)(0);
- (0, import_react32.useEffect)(() => {
+ 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 text = query.trim();
if (text === "" || search === void 0) {
setFound(null);
@@ -13970,13 +14468,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_react32.createElement)(
+ return (0, import_react33.createElement)(
"div",
{ className: "bt-connector-apps" },
- apps.length > 0 ? (0, import_react32.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react32.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null,
- (0, import_react32.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_react32.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null,
- (0, import_react32.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react32.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react32.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => {
+ 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: () => {
names.current.set(app.slug, app.name);
onChange([...apps, app.slug]);
} }, app.name)))
@@ -13984,21 +14482,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_react32.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react32.createElement)(
+ return (0, import_react33.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react33.createElement)(
"li",
{ key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" },
- (0, import_react32.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)),
- (0, import_react32.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "")
+ (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]]?.() ?? "")
)));
}
function ConnectorRow({ connector, roster, actions }) {
- const [form, setForm] = (0, import_react32.useState)(false);
- const [bots, setBots] = (0, import_react32.useState)(false);
- const [token, setToken] = (0, import_react32.useState)("");
- const [apps, setApps] = (0, import_react32.useState)(() => connector.apps ?? []);
- const [picking, setPicking] = (0, import_react32.useState)(false);
- const [busy, setBusy] = (0, import_react32.useState)(false);
- const [error, setError] = (0, import_react32.useState)(null);
+ 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 run = (task, after) => {
setBusy(true);
setError(null);
@@ -14007,9 +14505,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_react32.useState)({});
+ const [statuses, setStatuses] = (0, import_react33.useState)({});
const appsKey = (connector.apps ?? []).join(",");
- (0, import_react32.useEffect)(() => {
+ (0, import_react33.useEffect)(() => {
if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) {
setStatuses({});
return void 0;
@@ -14024,7 +14522,7 @@ function ConnectorRow({ connector, roster, actions }) {
live = false;
};
}, [gateway, connector.state, appsKey, picking]);
- const search = (0, import_react32.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]);
+ const search = (0, import_react33.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) {
@@ -14046,88 +14544,88 @@ function ConnectorRow({ connector, roster, actions }) {
};
const on = connector.state !== "off";
const Mark = MARKS[connector.id];
- const row = (0, import_react32.useRef)(null);
+ const row = (0, import_react33.useRef)(null);
useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}`);
- return (0, import_react32.createElement)(
+ return (0, import_react33.createElement)(
"div",
{ ref: row, className: "bt-connector", "data-state": connector.state },
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ className: "bt-connector-head" },
- (0, import_react32.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react32.createElement)(Mark) : null),
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react33.createElement)(Mark) : null),
+ (0, import_react33.createElement)(
"span",
{ className: "bt-connector-text" },
- (0, import_react32.createElement)("span", { className: "bt-connector-name" }, connector.name),
- (0, import_react32.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_react32.createElement)(
+ (0, import_react33.createElement)(
"span",
{ className: "bt-connector-actions" },
- connector.state === "error" ? (0, import_react32.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_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
+ 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: () => {
setApps(connector.apps ?? []);
setPicking(true);
} }, t("Choose apps")) : null,
- on ? (0, import_react32.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_react32.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_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect"))
+ 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 && gateway && !picking ? (0, import_react32.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null,
- on ? (0, import_react32.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null,
- on && bots ? (0, import_react32.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null,
- !on && form ? (0, import_react32.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)(
"div",
{ className: "bt-connector-form" },
- (0, import_react32.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react32.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })),
- gateway ? (0, import_react32.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null,
- (0, import_react32.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)(
"div",
{ className: "bt-connector-form-row" },
- (0, import_react32.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_react32.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: () => {
setForm(false);
setToken("");
setError(null);
} }, t("Cancel")),
- (0, import_react32.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_react32.createElement)(
+ on && gateway && picking ? (0, import_react33.createElement)(
"div",
{ className: "bt-connector-form" },
- (0, import_react32.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }),
- (0, import_react32.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_react32.createElement)(
+ (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)(
"div",
{ className: "bt-connector-form-row" },
- (0, import_react32.createElement)("span", { className: "bt-connector-link" }),
- (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
+ (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: () => {
setPicking(false);
setError(null);
} }, t("Cancel")),
- (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save"))
+ (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_react32.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null
+ error ? (0, import_react33.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null
);
}
function ConnectorsPage({ roster, actions }) {
const list = roster?.connectors ?? [];
- return (0, import_react32.createElement)(
+ return (0, import_react33.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react32.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react32.createElement)("div", { className: "bt-set-row" }, (0, import_react32.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react32.createElement)(ConnectorRow, { key: connector.id, connector, roster, 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 })))
);
}
// src/client/feedback.js
-var import_react33 = require("react");
-var FeedbackIcon = () => (0, import_react33.createElement)(
+var import_react34 = require("react");
+var FeedbackIcon = () => (0, import_react34.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_react33.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_react33.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" })
);
function download(name, text) {
const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" }));
@@ -14150,16 +14648,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_react33.useState)("");
- const [report, setReport] = (0, import_react33.useState)(null);
- const [preview, setPreview] = (0, import_react33.useState)(false);
- const [note, setNote] = (0, import_react33.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 fetchReport = () => actions.diagnostics({ client: clientFacts(), clientErrors: clientErrors() }).then((next) => {
setReport(next);
return next;
});
const load = () => fetchReport().then(() => void 0);
- (0, import_react33.useEffect)(() => {
+ (0, import_react34.useEffect)(() => {
void run(load());
}, []);
const copyReport = () => run(fetchReport().then(async (next) => {
@@ -14167,7 +14665,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_react33.useState)(null);
+ const [issueLink, setIssueLink] = (0, import_react34.useState)(null);
const openIssue = () => {
const win = window.open("", "_blank");
if (win !== null) win.opener = null;
@@ -14195,59 +14693,59 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) {
const total = (report?.total ?? 0) + browserErrors.length;
const shown = entries2.slice(0, 8);
return [
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)(
Section,
{ key: "report", title: t("Report a problem") },
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)(
"div",
{ className: "bt-set-row bt-set-paste bt-fb-what" },
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)(
"label",
{ className: "bt-set-copy", htmlFor: "bt-fb-what" },
- (0, import_react33.createElement)("span", null, t("What happened?")),
- (0, import_react33.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_react33.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_react33.createElement)(
+ (0, import_react34.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_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")),
- (0, import_react33.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"))
),
- preview ? (0, import_react33.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react33.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null,
- (0, import_react33.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)(
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_react33.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
+ (0, import_react34.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
)
),
- note ? (0, import_react33.createElement)(
+ note ? (0, import_react34.createElement)(
"div",
{ key: "note", className: "bt-note bt-fb-note", role: "status" },
note,
- issueLink ? [" ", (0, import_react33.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
) : null,
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)(
Section,
{ key: "errors", title: report ? t("Recent errors · {count}", { count: total }) : t("Recent errors") },
- shown.length === 0 ? (0, import_react33.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_react33.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)(
"div",
{ key: `${entry.time}-${index}`, className: "bt-set-row bt-fb-entry", title: entry.text },
- (0, import_react33.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
+ (0, import_react34.createElement)(
"div",
{ className: "bt-set-copy" },
- (0, import_react33.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)),
- (0, import_react33.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].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(" · "))
),
- entry.code ? (0, import_react33.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
)),
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)(
Row3,
{ label: t("Error log"), hint: t("Kept on this computer across restarts, up to about 1 MB.") },
- (0, import_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")),
- (0, import_react33.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"))
)
)
];
@@ -14255,8 +14753,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_react33.useState)(false);
- (0, import_react33.useEffect)(() => {
+ const [leaving, setLeaving] = (0, import_react34.useState)(false);
+ (0, import_react34.useEffect)(() => {
if (!toast) return void 0;
setLeaving(false);
const fade = setTimeout(() => setLeaving(true), TOAST_MS);
@@ -14268,15 +14766,15 @@ function ErrorToast({ useUi, actions }) {
}, [toast?.token]);
if (!toast) return null;
const info = toast.tone === "info";
- return (0, import_react33.createElement)(
+ return (0, import_react34.createElement)(
"div",
{ className: "bt-toast", role: info ? "status" : "alert", "data-tone": toast.tone, "data-leaving": leaving || void 0 },
- (0, import_react33.createElement)("span", { className: "bt-toast-text" }, toast.text),
- info ? null : (0, import_react33.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => {
+ (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: () => {
actions.dismissToast(toast.token);
actions.openSettings("feedback");
} }, t("Details")),
- (0, import_react33.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react33.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))
);
}
var FEEDBACK_CSS = `
@@ -14307,7 +14805,7 @@ var FEEDBACK_CSS = `
`;
// src/client/about.js
-var import_react34 = require("react");
+var import_react35 = 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…");
@@ -14342,9 +14840,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_react34.useState)(false);
- const [stars, setStars] = (0, import_react34.useState)(null);
- (0, import_react34.useEffect)(() => {
+ const [checking, setChecking] = (0, import_react35.useState)(false);
+ const [stars, setStars] = (0, import_react35.useState)(null);
+ (0, import_react35.useEffect)(() => {
let live = true;
actions.repoStars().then((count) => {
if (live && Number.isInteger(count)) setStars(count);
@@ -14365,29 +14863,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_react34.createElement)(
+ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react35.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_react34.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_react34.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_react34.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)(
"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_react34.createElement)(
+ (0, import_react35.createElement)(
"div",
{ key: "hero", className: "bt-about-hero" },
- (0, import_react34.createElement)("span", { className: "bt-about-mark" }, (0, import_react34.createElement)(FlyBotMark, { size: 52 })),
- (0, import_react34.createElement)(
+ (0, import_react35.createElement)("span", { className: "bt-about-mark" }, (0, import_react35.createElement)(FlyBotMark, { size: 52 })),
+ (0, import_react35.createElement)(
"div",
{ className: "bt-about-copy" },
- (0, import_react34.createElement)("span", { className: "bt-about-name" }, t("FlyBot")),
- update.current ? (0, import_react34.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : 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
),
- repo ? (0, import_react34.createElement)(
+ repo ? (0, import_react35.createElement)(
"a",
{
className: "bt-star",
@@ -14397,23 +14895,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_react34.createElement)("span", { className: "bt-star-main" }, (0, import_react34.createElement)("span", { className: "bt-star-icon" }, (0, import_react34.createElement)(StarLineIcon)), "Star"),
- stars === null ? null : (0, import_react34.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))
) : null
),
- repo ? (0, import_react34.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_react34.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)(
Section,
{ key: "updates", title: t("Updates") },
- (0, import_react34.createElement)(Row3, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react34.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null),
- dsh?.current ? (0, import_react34.createElement)(Row3, { label: t("DeepSeek Harness (underlying)"), 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_react34.createElement)(
+ (0, import_react35.createElement)(
Section,
{ key: "links", title: t("Links") },
- repo ? (0, import_react34.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react34.createElement)("a", linkProps(repo), (0, import_react34.createElement)(GitHubMark), t("Open"))) : null,
- update.notesUrl ? (0, import_react34.createElement)(Row3, { label: t("Release notes") }, (0, import_react34.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
- (0, import_react34.createElement)(Row3, { label: t("Report a problem") }, (0, import_react34.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")))
)
];
}
@@ -14442,19 +14940,19 @@ var ABOUT_CSS = `
`;
// src/client/settings.js
-var import_react35 = require("react");
+var import_react36 = 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_react35.createElement)("section", { className: "bt-set-section" }, (0, import_react35.createElement)("h3", null, title), (0, import_react35.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));
}
function SetRow({ label, hint, children }) {
- return (0, import_react35.createElement)(
+ return (0, import_react36.createElement)(
"div",
{ className: "bt-set-row" },
- (0, import_react35.createElement)("div", { className: "bt-set-copy" }, (0, import_react35.createElement)("span", null, label), hint ? (0, import_react35.createElement)("span", { className: "bt-set-hint" }, hint) : null),
- children ? (0, import_react35.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
);
}
var NOTIFY_KINDS = [
@@ -14466,35 +14964,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_react35.useState)(prefs.accent ?? "#4d6bfe");
- const timer = (0, import_react35.useRef)(0);
- (0, import_react35.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 pickCustom = (value) => {
setCustom(value);
clearTimeout(timer.current);
timer.current = setTimeout(() => run(actions.setPrefs({ accent: value })), 350);
};
return [
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
SetSection,
{ key: "look", title: t("Appearance") },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
SetRow,
{ label: t("Theme"), hint: t("Colors for the whole team, on every device.") },
- (0, import_react35.createElement)(
+ (0, import_react36.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_react35.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)))
)
),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
SetRow,
{ label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"div",
{ className: "bt-swatches" },
- (0, import_react35.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_react35.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", {
key: ink,
type: "button",
className: "bt-swatch",
@@ -14504,26 +15002,26 @@ function GeneralPage({ roster, actions, run }) {
style: { background: INKS[ink][0] },
onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] }))
})),
- (0, import_react35.createElement)(
+ (0, import_react36.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_react35.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) })
)
)
)
),
// One switch for every system notification (#353), then one per kind (#469).
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
SetSection,
{ key: "notify", title: t("Notifications") },
- (0, import_react35.createElement)(SwitchRow, {
+ (0, import_react36.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_react35.createElement)(SwitchRow, {
+ NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react36.createElement)(SwitchRow, {
key: kind,
label: t(label),
hint: hint ? t(hint) : void 0,
@@ -14539,52 +15037,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_react35.createElement)(
+ return (0, import_react36.createElement)(
SetSection,
{ title: t("Main Bot") },
- (0, import_react35.createElement)(
+ (0, import_react36.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_react35.createElement)("select", {
+ (0, import_react36.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_react35.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react35.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)))
),
- others.map((bot) => (0, import_react35.createElement)(
+ others.map((bot) => (0, import_react36.createElement)(
SetRow,
{ key: bot.id, label: bot.name, hint: t("Also a Main Bot.") },
- (0, import_react35.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_react35.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"))
)),
- candidates.length > 0 ? (0, import_react35.createElement)(
+ candidates.length > 0 ? (0, import_react36.createElement)(
SetRow,
{ label: t("Add a Main Bot"), hint: t("Main Bots can create and change Bots, and run every group chat.") },
- (0, import_react35.createElement)("select", {
+ (0, import_react36.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_react35.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react35.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)))
) : 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_react35.createElement)(
+ return (0, import_react36.createElement)(
SetSection,
{ title: t("Animation") },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
SetRow,
{ label: t("Bot animation") },
- (0, import_react35.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react35.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", {
key: id,
type: "button",
"aria-pressed": motion === id,
@@ -14596,69 +15094,69 @@ function MotionSection({ roster, actions, run }) {
);
}
function BotsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react35.useState)(false);
+ const [creating, setCreating] = (0, import_react36.useState)(false);
return [
- (0, import_react35.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
- (0, import_react35.createElement)(MotionSection, { key: "motion", roster, actions, run }),
- creating ? (0, import_react35.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react35.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)(
"div",
{ key: "new", className: "bt-actions" },
- (0, import_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react35.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_react35.createElement)(
+ (0, import_react36.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_react35.createElement)(
+ roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react36.createElement)(
"button",
{ key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react35.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
- (0, import_react35.createElement)("span", { className: "bt-row-title" }, bot.name),
- bot.role ? (0, import_react35.createElement)("span", { className: "bt-tag" }, bot.role) : null,
- isMainOf(roster, bot.id) ? (0, import_react35.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
- bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react35.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_react35.createElement)("span", { className: "bt-chevron" }, (0, import_react35.createElement)(ChevronRightIcon))
+ (0, import_react36.createElement)("span", { className: "bt-chevron" }, (0, import_react36.createElement)(ChevronRightIcon))
))
)
];
}
function GroupsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react35.useState)(false);
+ const [creating, setCreating] = (0, import_react36.useState)(false);
return [
- creating ? (0, import_react35.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react35.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)(
"div",
{ key: "new", className: "bt-actions" },
- (0, import_react35.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_react35.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_react35.createElement)(
+ (0, import_react36.createElement)(
SetSection,
{ key: "groups", title: t("Group chats · {count}", { count: roster.rooms.length }) },
- roster.rooms.length === 0 ? (0, import_react35.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) => {
const admin = roster.byId[room2.admin];
- return (0, import_react35.createElement)(
+ return (0, import_react36.createElement)(
"button",
{ key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react35.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
- (0, import_react35.createElement)("span", { className: "bt-row-title" }, room2.name),
- (0, import_react35.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })),
- admin ? (0, import_react35.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react35.createElement)(AdminStar), admin.name) : null,
- room2.hidden ? (0, import_react35.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_react35.createElement)("span", { className: "bt-chevron" }, (0, import_react35.createElement)(ChevronRightIcon))
+ (0, import_react36.createElement)("span", { className: "bt-chevron" }, (0, import_react36.createElement)(ChevronRightIcon))
);
})
)
];
}
function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) {
- const [error, setError] = (0, import_react35.useState)("");
- const close = (0, import_react35.useCallback)(() => actions.closeSettings(), []);
+ const [error, setError] = (0, import_react36.useState)("");
+ const close = (0, import_react36.useCallback)(() => actions.closeSettings(), []);
useEscape(close);
const run = (promise) => Promise.resolve(promise).then(() => {
setError("");
@@ -14679,34 +15177,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_react35.useRef)({ key: pageKey, depth, move: "up" });
+ const last = (0, import_react36.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_react35.useState)(shown === "usage");
+ const [usageOpened, setUsageOpened] = (0, import_react36.useState)(shown === "usage");
if (shown === "usage" && !usageOpened) setUsageOpened(true);
- const errorLine = error ? (0, import_react35.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
- return (0, import_react35.createElement)(
+ const errorLine = error ? (0, import_react36.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
+ return (0, import_react36.createElement)(
"div",
{ className: "bt-settings-layer", "data-leaving": leaving || void 0 },
- (0, import_react35.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
+ (0, import_react36.createElement)(
"div",
{ className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("FlyBot settings"), "data-wide": shown === "usage" ? "" : void 0 },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"nav",
{ ref: nav.box, className: "bt-settings-nav", "aria-label": t("Settings pages") },
- (0, import_react35.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
- pages.map(([key, text]) => (0, import_react35.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", {
key,
type: "button",
className: "bt-settings-item",
"aria-current": shown === key ? "page" : void 0,
onClick: () => actions.openSettings(key)
- }, (0, import_react35.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text))),
+ }, (0, import_react36.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_react35.createElement)("button", {
+ (0, import_react36.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"),
@@ -14715,51 +15213,51 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false
close();
openHarnessSettings();
}
- }, (0, import_react35.createElement)(SlidersIcon), t("Harness settings"), (0, import_react35.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
+ }, (0, import_react36.createElement)(SlidersIcon), t("Harness settings"), (0, import_react36.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"div",
{ className: "bt-settings-main" },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"div",
{ className: "bt-settings-head" },
- back ? (0, import_react35.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react35.createElement)(ChevronLeftIcon)) : null,
- (0, import_react35.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)
),
- shown === "usage" ? null : (0, import_react35.createElement)(
+ shown === "usage" ? null : (0, import_react36.createElement)(
"div",
{ key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move },
errorLine,
- shown === "general" ? (0, import_react35.createElement)(GeneralPage, { roster, actions, run }) : null,
- bot ? (0, import_react35.createElement)("div", { className: "bt-settings-form" }, (0, import_react35.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
- shown === "bots" && !bot ? (0, import_react35.createElement)(BotsPage, { roster, actions, run }) : null,
- room2 ? (0, import_react35.createElement)("div", { className: "bt-settings-form" }, (0, import_react35.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
- shown === "groups" && !room2 ? (0, import_react35.createElement)(GroupsPage, { roster, actions, run }) : null,
- shown === "connectors" ? (0, import_react35.createElement)(ConnectorsPage, { roster, actions }) : null,
- pluginPage ? (0, import_react35.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null,
- shown === "capabilities" ? (0, import_react35.createElement)(CapabilitiesPage, { roster, actions, run }) : null,
- shown === "schedules" ? (0, import_react35.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
- shown === "secrets" ? (0, import_react35.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
- shown === "feedback" ? (0, import_react35.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
- shown === "about" ? (0, import_react35.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
),
- usageOpened ? (0, import_react35.createElement)(
+ usageOpened ? (0, import_react36.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_react35.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
+ (0, import_react36.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
) : null
),
- (0, import_react35.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react35.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))
)
);
}
// src/client/context-menu.js
-var import_react36 = require("react");
+var import_react37 = require("react");
function ContextMenu({ menu, roster, actions, leaving = false }) {
useEscape(() => actions.closeMenu());
- (0, import_react36.useEffect)(() => {
+ (0, import_react37.useEffect)(() => {
const onDown = (event) => {
if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu();
};
@@ -14767,14 +15265,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
return () => window.removeEventListener("mousedown", onDown);
}, []);
const structure = useSidebarStructure();
- const ref = (0, import_react36.useRef)(null);
- const [place, setPlace] = (0, import_react36.useState)({ left: menu.x, top: menu.y });
- (0, import_react36.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 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_react36.createElement)(AgentMenu, { menu, roster, actions, leaving });
+ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react37.createElement)(AgentMenu, { menu, roster, actions, leaving });
const bot = roster.byId[menu.id];
const room2 = roster.roomsById[menu.id];
const entry = bot ?? room2;
@@ -14816,14 +15314,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
]
];
const shown = groups.map((group) => group.filter(Boolean)).filter((group) => group.length > 0);
- return (0, import_react36.createElement)(
+ return (0, import_react37.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_react36.createElement)(
- import_react36.Fragment,
+ shown.map((group, index) => (0, import_react37.createElement)(
+ import_react37.Fragment,
{ key: index },
- index > 0 ? (0, import_react36.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
- group.map(([label, Icon, run, danger]) => (0, import_react36.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", {
key: label,
type: "button",
role: "menuitem",
@@ -14832,14 +15330,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
actions.closeMenu();
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react36.createElement)(Icon), label))
+ }, (0, import_react37.createElement)(Icon), label))
))
);
}
function AgentMenu({ menu, roster, actions, leaving }) {
- const ref = (0, import_react36.useRef)(null);
- const [place, setPlace] = (0, import_react36.useState)({ left: menu.x, top: menu.y });
- (0, import_react36.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 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)) });
@@ -14861,14 +15359,14 @@ function AgentMenu({ menu, roster, actions, leaving }) {
[t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true]
]
];
- return (0, import_react36.createElement)(
+ return (0, import_react37.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_react36.createElement)(
- import_react36.Fragment,
+ groups.map((group, index) => (0, import_react37.createElement)(
+ import_react37.Fragment,
{ key: index },
- index > 0 ? (0, import_react36.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
- group.map(([label, Icon, run, danger]) => (0, import_react36.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", {
key: label,
type: "button",
role: "menuitem",
@@ -14877,7 +15375,7 @@ function AgentMenu({ menu, roster, actions, leaving }) {
actions.closeMenu();
void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error));
}
- }, (0, import_react36.createElement)(Icon), label))
+ }, (0, import_react37.createElement)(Icon), label))
))
);
}
@@ -14963,7 +15461,7 @@ function migrateStorage(storage = globalThis.localStorage) {
}
// src/client/details-drawer.js
-var import_react37 = require("react");
+var import_react38 = 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 "";
@@ -14988,10 +15486,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_react37.useState)(null);
+ const [pending, setPending] = (0, import_react38.useState)(null);
const catalog = roster.permissions;
const saved = bot.permission ?? catalog?.defaultPreset;
- (0, import_react37.useEffect)(() => {
+ (0, import_react38.useEffect)(() => {
if (pending === saved) setPending(null);
}, [pending, saved]);
if (!catalog?.options?.length) return null;
@@ -15001,14 +15499,14 @@ function PermissionRow({ bot, roster, actions, run }) {
setPending(value);
void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null));
};
- return (0, import_react37.createElement)(
+ return (0, import_react38.createElement)(
"div",
{ className: "bt-drawer-permission" },
- (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
+ (0, import_react38.createElement)(
Segmented,
{ label: t("Permissions"), value: current },
- catalog.options.map((option) => (0, import_react37.createElement)("button", {
+ catalog.options.map((option) => (0, import_react38.createElement)("button", {
key: option.value,
type: "button",
"aria-pressed": option.value === current,
@@ -15022,42 +15520,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_react37.useEffect)(() => {
+ (0, import_react38.useEffect)(() => {
actions.browserEnsure?.(bot.id);
}, [bot.id]);
if (roster.browser !== true || !actions.browserShow) {
- return (0, import_react37.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."));
}
if (state.ids.length === 0) {
- return (0, import_react37.createElement)(
+ return (0, import_react38.createElement)(
"div",
{ className: "bt-browser-empty-mark" },
- (0, import_react37.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
- (0, import_react37.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
- (0, import_react37.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_react37.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"))
);
}
- return (0, import_react37.createElement)(
+ return (0, import_react38.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_react37.createElement)(
+ return (0, import_react38.createElement)(
"div",
{ key: tabId, className: "bt-bcard", "data-active": state.active === tabId || void 0, style: { animationDelay: `${index * 30}ms` } },
- (0, import_react37.createElement)(
+ (0, import_react38.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_react37.createElement)(
+ page.excerpt ? (0, import_react38.createElement)(
"span",
{ className: "bt-bcard-preview" },
- (0, import_react37.createElement)("span", { className: "bt-bcard-ptitle" }, label),
- (0, import_react37.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
- ) : (0, import_react37.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react37.createElement)(GlobeIcon), (0, import_react37.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_react37.createElement)("button", {
+ (0, import_react38.createElement)("button", {
type: "button",
className: "bt-bcard-x",
"aria-label": t("Close tab"),
@@ -15066,36 +15564,36 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow
void actions.browserCloseTab(bot.id, tabId);
}
}, "×"),
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"div",
{ className: "bt-bcard-row" },
- (0, import_react37.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
- (0, import_react37.createElement)("span", { className: "bt-bcard-title" }, label),
- page.url ? (0, import_react37.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_react37.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react37.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"))
);
}
function About({ text, title = "About" }) {
- const [open, setOpen] = (0, import_react37.useState)(false);
+ const [open, setOpen] = (0, import_react38.useState)(false);
const long = text.split("\n").length > 5 || text.length > 240;
- const body = (0, import_react37.useRef)(null);
+ const body = (0, import_react38.useRef)(null);
useMorph(body, open);
- return (0, import_react37.createElement)(
+ return (0, import_react38.createElement)(
"div",
null,
- (0, import_react37.createElement)("div", { className: "bt-section-title" }, t(title)),
- (0, import_react37.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text),
- long ? (0, import_react37.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : 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
);
}
function SharedRoomApprovals({ room: room2 }) {
- const [items, setItems] = (0, import_react37.useState)(room2.pendingApprovals ?? []);
- const [busy, setBusy] = (0, import_react37.useState)("");
+ const [items, setItems] = (0, import_react38.useState)(room2.pendingApprovals ?? []);
+ const [busy, setBusy] = (0, import_react38.useState)("");
const face = room2.origin === "face-to-face";
- (0, import_react37.useEffect)(() => {
+ (0, import_react38.useEffect)(() => {
const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length;
if (haveItems) setItems(room2.pendingApprovals);
if (room2.viewerRole !== "invited-owner" && !face) return void 0;
@@ -15121,28 +15619,28 @@ function SharedRoomApprovals({ room: room2 }) {
setBusy("");
}
};
- return (0, import_react37.createElement)(
+ return (0, import_react38.createElement)(
"div",
{ key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" },
- (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react37.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))),
- items.map((item) => (0, import_react37.createElement)(
+ (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)(
"div",
{ key: item.id, className: "bt-set-row" },
- (0, import_react37.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")),
- (0, import_react37.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")),
- (0, import_react37.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject"))
+ (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"))
))
);
}
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_react37.createElement)(
+ return (0, import_react38.createElement)(
"div",
null,
- (0, import_react37.createElement)(FaceLead, { room: room2, roster, pick: pick2 }),
- (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })),
- (0, import_react37.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react37.createElement)(FacePersonRow, {
+ (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, {
key: person.key,
person,
className: "bt-member",
@@ -15165,79 +15663,79 @@ function GroupOverview({ room: room2, roster, actions, usage, useWork, openSetti
};
const face = room2.origin === "face-to-face";
return [
- (0, import_react37.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
- invited && room2.sharedFrom && !face ? (0, import_react37.createElement)(
+ (0, import_react38.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
+ invited && room2.sharedFrom && !face ? (0, import_react38.createElement)(
"div",
{ key: "from", className: "bt-shared-from" },
- (0, import_react37.createElement)("div", { className: "bt-section-title" }, title),
- (0, import_react37.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_react37.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null,
- (0, import_react37.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot"))
- ) : face ? null : room2.notice ? (0, import_react37.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react37.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
- face ? null : (0, import_react37.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
- face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react37.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react37.createElement)(
+ (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)(
"div",
{ key: "members" },
- (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })),
- (0, import_react37.createElement)("div", { className: "bt-members", role: "list" }, [
- ...humans.map((person) => (0, import_react37.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)(
"div",
{ key: person.id, role: "listitem" },
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react37.createElement)("span", { className: "bt-member-name" }, (0, import_react37.createElement)("span", null, person.displayName || person.id)),
- (0, import_react37.createElement)("span", { className: "bt-chip" }, t("Human")),
- person.role === "admin" ? (0, import_react37.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
+ (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
)
)),
- ...remotes.map((person) => (0, import_react37.createElement)(
+ ...remotes.map((person) => (0, import_react38.createElement)(
"div",
{ key: person.handle || person.id, role: "listitem" },
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"span",
{ className: "bt-member-name" },
- (0, import_react37.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
- (0, import_react37.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person))
+ (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))
),
- face ? null : (0, import_react37.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
+ face ? null : (0, import_react38.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
)
)),
...members.map((member) => {
const entry = entries2.find((item) => item.id === member.id);
- return (0, import_react37.createElement)(
+ return (0, import_react38.createElement)(
"div",
{ key: member.id, role: "listitem" },
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"button",
{ type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) },
- (0, import_react37.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
- (0, import_react37.createElement)("span", null, member.name)
+ (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)
),
- room2.admin === member.id ? (0, import_react37.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
+ room2.admin === member.id ? (0, import_react38.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
),
- entry ? (0, import_react37.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
+ entry ? (0, import_react38.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
);
})
])
),
- face ? null : (0, import_react37.createElement)(
+ face ? null : (0, import_react38.createElement)(
"div",
{ key: "mode" },
- (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
- (0, import_react37.createElement)("div", { className: "bt-kv" }, (0, import_react37.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")))
),
- face || !usage ? null : (0, import_react37.createElement)(
+ face || !usage ? null : (0, import_react38.createElement)(
"div",
{ key: "usage" },
- (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Usage")),
- (0, import_react37.createElement)("div", { className: "bt-kv" }, (0, import_react37.createElement)("span", null, room2.name), (0, import_react37.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, room2.name), (0, import_react38.createElement)("span", null, usage))
)
];
}
@@ -15245,15 +15743,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_react37.useState)("details");
+ const [tab, setTab] = (0, import_react38.useState)("details");
const room2 = roster.roomsById[sessionId];
- const [view, setView] = (0, import_react37.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
- const [editing, setEditing] = (0, import_react37.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
- const [draftName, setDraftName] = (0, import_react37.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
- const [copied, setCopied] = (0, import_react37.useState)(false);
- const [error, setError] = (0, import_react37.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 glide = useGlide(tab);
- const last = (0, import_react37.useRef)({ view, tab, move: void 0 });
+ const last = (0, import_react38.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;
@@ -15278,43 +15776,43 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
setTimeout(() => setCopied(false), 1600);
});
};
- const errorLine = error ? (0, import_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
+ const errorLine = error ? (0, import_react38.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
if ((bot || room2) && view === "settings" && !bot?.remote) {
return [
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"div",
{ key: "head", className: "bt-subhead", "data-move": move },
- (0, import_react37.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react37.createElement)(ChevronLeftIcon)),
- (0, import_react37.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
- hasStrip ? null : (0, import_react37.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react37.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_react37.createElement)(
+ (0, import_react38.createElement)(
"div",
{ key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move },
errorLine,
- bot ? (0, import_react37.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react37.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 })
)
];
}
- const top = (0, import_react37.createElement)(
+ const top = (0, import_react38.createElement)(
"div",
{ className: "bt-drawer-top", "data-move": side },
- bot || room2 ? (0, import_react37.createElement)("button", {
+ bot || room2 ? (0, import_react38.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_react37.createElement)(GearIcon)) : null,
- bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react37.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_react37.createElement)(GlobeIcon)) : null,
- bot || room2 ? (0, import_react37.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_react37.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react37.createElement)(ShareIcon)) : null,
- hasStrip ? null : (0, import_react37.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react37.createElement)(CloseIcon))
+ }, (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))
);
if (!bot && !room2) {
return [
- (0, import_react37.createElement)(import_react37.Fragment, { key: "top" }, top),
- (0, import_react37.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react37.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.")))
];
}
const current = bot?.name ?? room2.name;
@@ -15328,12 +15826,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_react37.createElement)(import_react37.Fragment, { key: "top" }, top),
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(import_react38.Fragment, { key: "top" }, top),
+ (0, import_react38.createElement)(
"div",
{ key: "id", className: "bt-drawer-id", "data-move": side },
- bot ? (0, import_react37.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react37.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
- bot?.remote ? (0, import_react37.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react37.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react37.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", {
className: "bt-drawer-name-input",
autoFocus: true,
value: draftName,
@@ -15348,59 +15846,59 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
setEditing(false);
}
}
- }) : (0, import_react37.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: () => {
setDraftName(room2 && !room2.named ? "" : current);
setEditing(true);
} }, current),
- subtitle ? (0, import_react37.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react37.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : 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
),
- bot?.remote ? null : (0, import_react37.createElement)(
+ bot?.remote ? null : (0, import_react38.createElement)(
"div",
{ key: "tabs", ref: glide.box, className: "bt-tabs", role: "tablist", "data-move": side },
- (0, import_react37.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- tabs.map((key) => (0, import_react37.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_react37.createElement)(
+ (0, import_react38.createElement)(
"div",
{ key: `body:${tab}`, className: "bt-drawer-body", "data-move": move },
errorLine,
- room2 && tab === "details" ? (0, import_react37.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
+ room2 && tab === "details" ? (0, import_react38.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
bot && bot.remote && tab === "details" ? [
- bot.remote.handle ? (0, import_react37.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null,
- (0, import_react37.createElement)(FlyBotDetails, { key: "social", bot, roster, actions })
+ 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 })
] : null,
bot && !bot.remote && tab === "details" ? [
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"div",
{ key: "model", className: "bt-drawer-model" },
- (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Model")),
- (0, import_react37.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
- modelOf(bot, roster) ? null : (0, import_react37.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_react37.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
+ (0, import_react38.createElement)(
"div",
{ key: "folder", className: "bt-drawer-model" },
- (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Working folder")),
- (0, import_react37.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 })) })
),
- bot.instructions ? (0, import_react37.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react37.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
- roster.memory === false ? null : (0, import_react37.createElement)(MemoryRow, { key: "memory", bot, actions }),
- usage ? (0, import_react37.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)(
"div",
{ key: "usage" },
- (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Usage")),
- (0, import_react37.createElement)("div", { className: "bt-kv" }, (0, import_react37.createElement)("span", null, bot.name), (0, import_react37.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))
) : null,
- (0, import_react37.createElement)(ScheduleSummary, { key: "routines", bot, actions })
+ (0, import_react38.createElement)(ScheduleSummary, { key: "routines", bot, actions })
] : null,
- bot && !bot.remote && tab === "browser" ? (0, import_react37.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
)
];
}
// src/client/browser-pane.js
-var import_react38 = require("react");
+var import_react39 = require("react");
var import_react_dom8 = require("react-dom");
var PANEL_LINGER_MS = 300;
var WIDTH_KEY = STORAGE.browserWidth;
@@ -15410,10 +15908,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_react38.createElement)(
+ return (0, import_react39.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_react38.createElement)("path", { key: index, d }))
+ paths.map((d, index) => (0, import_react39.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"]);
@@ -15425,9 +15923,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_react38.useState)(null);
- if (!src || failed === src) return (0, import_react38.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react38.createElement)(GlobeIcon));
- return (0, import_react38.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) });
}
var savedWidth = () => {
try {
@@ -15486,11 +15984,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_react38.useRef)(null);
- const ind = (0, import_react38.useRef)(null);
- const spot = (0, import_react38.useRef)(null);
- const under = (0, import_react38.useRef)(null);
- (0, import_react38.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 box = wrap.current;
const capsule = ind.current;
if (!box || !capsule) return void 0;
@@ -15519,7 +16017,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
trail.cancel();
};
});
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
const box = wrap.current;
if (!box) return void 0;
const observer = new ResizeObserver(() => {
@@ -15533,13 +16031,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
observer.observe(box);
return () => observer.disconnect();
}, []);
- return (0, import_react38.createElement)(
+ return (0, import_react39.createElement)(
"div",
{ className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") },
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ className: "bt-ptabs-in", ref: wrap },
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"button",
{
type: "button",
@@ -15549,13 +16047,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
title: bot.name,
onClick: () => onView("details")
},
- (0, import_react38.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
- view === "details" ? (0, import_react38.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
),
ids.map((tabId) => {
const page = pages[tabId] ?? EMPTY_PAGE;
const on = view === "browser" && tabId === active;
- return (0, import_react38.createElement)(
+ return (0, import_react39.createElement)(
"button",
{
key: tabId,
@@ -15576,9 +16074,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
}
}
},
- (0, import_react38.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
- (0, import_react38.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
- (0, import_react38.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", {
className: "bt-ptab-x",
role: "button",
"aria-label": t("Close tab"),
@@ -15590,20 +16088,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
}, "×")
);
}),
- (0, import_react38.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 })
),
// New tab and Close stay outside the scrolling tabs, so a narrow panel keeps both.
- (0, import_react38.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react38.createElement)(PlusTabIcon)),
- (0, import_react38.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_react38.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))
);
}
function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) {
const page = useBrowserPages((pages) => pages[tabId ?? ""]) ?? EMPTY_PAGE;
- const frame = (0, import_react38.useRef)(null);
- const input = (0, import_react38.useRef)(null);
- const [draft, setDraft] = (0, import_react38.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 showGuest = landed && !leaving && Boolean(page.url) && !page.error;
- (0, import_react38.useLayoutEffect)(() => {
+ (0, import_react39.useLayoutEffect)(() => {
const element = frame.current;
if (!element || leaving || !tabId) {
void actions.browserShow(null);
@@ -15629,7 +16127,7 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
window.removeEventListener("resize", schedule);
};
}, [tabId, showGuest, leaving]);
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
setDraft(null);
if (landed && !page.url) input.current?.focus();
}, [tabId, landed]);
@@ -15645,27 +16143,27 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
};
const editing = draft !== null;
const secure = /^https:/i.test(page.url);
- return (0, import_react38.createElement)(
+ return (0, import_react39.createElement)(
"div",
{ className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) },
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ className: "bt-browser-bar" },
- (0, import_react38.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_react38.createElement)(BackIcon)),
- (0, import_react38.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_react38.createElement)(ForwardIcon)),
- (0, import_react38.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", {
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_react38.createElement)(page.loading ? StopIcon : ReloadIcon)),
- (0, import_react38.createElement)(
+ }, (0, import_react39.createElement)(page.loading ? StopIcon : ReloadIcon)),
+ (0, import_react39.createElement)(
"form",
{ className: "bt-browser-address", onSubmit: go, "data-editing": editing || void 0 },
- (0, import_react38.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react38.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
- (0, import_react38.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", {
ref: input,
type: "text",
spellCheck: false,
@@ -15689,25 +16187,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
}
})
),
- page.loading ? (0, import_react38.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
),
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 },
- page.error ? (0, import_react38.createElement)(
+ page.error ? (0, import_react39.createElement)(
"div",
{ className: "bt-browser-empty" },
- (0, import_react38.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
- (0, import_react38.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
- page.url ? (0, import_react38.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: () => {
void actions.browserNavigate(tabId, page.url);
} }, t("Try again")) : null
- ) : !page.url ? (0, import_react38.createElement)(
+ ) : !page.url ? (0, import_react39.createElement)(
"div",
{ className: "bt-browser-empty" },
- (0, import_react38.createElement)(BotMark, { bot, size: 48, 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-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 }))
) : null
)
);
@@ -15719,41 +16217,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_react38.useRef)(null);
- const fly = (0, import_react38.useRef)(null);
- const [landed, setLanded] = (0, import_react38.useState)(false);
- const [settling, setSettling] = (0, import_react38.useState)(false);
- const [width, setWidth] = (0, import_react38.useState)(savedWidth);
- const [viewport, setViewport] = (0, import_react38.useState)(() => window.innerWidth);
- const [dragging, setDragging] = (0, import_react38.useState)(false);
- const [resizing, setResizing] = (0, import_react38.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 instant = dragging || resizing;
- const [reading, setReading] = (0, import_react38.useState)(false);
+ const [reading, setReading] = (0, import_react39.useState)(false);
const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport));
- (0, import_react38.useLayoutEffect)(() => {
+ (0, import_react39.useLayoutEffect)(() => {
if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH);
}, [view, width]);
- (0, import_react38.useLayoutEffect)(() => {
+ (0, import_react39.useLayoutEffect)(() => {
chatRoom.hold(instant);
}, [instant]);
- const [last, setLast] = (0, import_react38.useState)({ view, width: panelWidth });
+ const [last, setLast] = (0, import_react39.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_react38.useRef)(view);
- (0, import_react38.useLayoutEffect)(() => {
+ const shownView = (0, import_react39.useRef)(view);
+ (0, import_react39.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_react38.useEffect)(() => () => {
+ (0, import_react39.useEffect)(() => () => {
chatRoom.set(null);
chatRoom.hold(false);
}, []);
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
let timer;
const onResize = () => {
setResizing(true);
@@ -15767,28 +16265,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
window.removeEventListener("resize", onResize);
};
}, []);
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
if (!settling) return void 0;
const timer = setTimeout(() => setSettling(false), SPRING_SHAPE.duration + 40);
return () => clearTimeout(timer);
}, [settling, view]);
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
const timer = setTimeout(() => setLanded(true), 360);
return () => clearTimeout(timer);
}, []);
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
if (canBrowse) actions.browserEnsure(bot.id);
}, [bot?.id, canBrowse]);
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details");
}, [view, active, tabState.ids.length]);
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
if (view !== "browser" || leaving) void actions.browserShow?.(null);
}, [view, leaving]);
- (0, import_react38.useEffect)(() => () => {
+ (0, import_react39.useEffect)(() => () => {
void actions.browserShow?.(null);
}, []);
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0;
setReading(true);
const timer = setTimeout(() => setReading(false), READ_FLASH_MS);
@@ -15825,7 +16323,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
return actions[name](...args);
};
}
- (0, import_react38.useLayoutEffect)(() => {
+ (0, import_react39.useLayoutEffect)(() => {
const move = fly.current;
if (!move) return;
fly.current = null;
@@ -15908,7 +16406,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_react38.createElement)(
+ return (0, import_react39.createElement)(
"aside",
{
ref: root,
@@ -15925,15 +16423,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
if (event.target === event.currentTarget) setLanded(true);
}
},
- leaving ? null : (0, import_react38.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 }),
// 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_react38.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
- hasStrip ? (0, import_react38.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
- view === "browser" && bot ? (0, import_react38.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react38.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)(
"div",
{ key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" },
- (0, import_react38.createElement)(DetailsDrawer, {
+ (0, import_react39.createElement)(DetailsDrawer, {
sessionId: pane.id,
startRenaming: pane.renaming,
startTab: pane.tab,
@@ -16030,7 +16528,7 @@ var BROWSER_CSS = `
`;
// src/client/remote-bot-settings.js
-var import_react39 = require("react");
+var import_react40 = 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}
@@ -16054,8 +16552,8 @@ var REMOTE_BOT_SETTINGS_CSS = `
function RemoteBotSettingsSheet({ bot, roster, actions, leaving = false }) {
const close = () => actions.closeSettings();
useEscape(close);
- const dialog = (0, import_react39.useRef)(null);
- (0, import_react39.useEffect)(() => {
+ const dialog = (0, import_react40.useRef)(null);
+ (0, import_react40.useEffect)(() => {
const before = document.activeElement;
dialog.current?.focus?.({ preventScroll: true });
return () => {
@@ -16070,37 +16568,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_react39.createElement)(
+ return (0, import_react40.createElement)(
"div",
{ className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" },
- (0, import_react39.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
- (0, import_react39.createElement)(
+ (0, import_react40.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
+ (0, import_react40.createElement)(
"div",
{ className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown },
- (0, import_react39.createElement)(
+ (0, import_react40.createElement)(
"div",
{ className: "bt-remote-sheet-head" },
- (0, import_react39.createElement)(BotMark, { bot, size: 40, live: true }),
- (0, import_react39.createElement)(
+ (0, import_react40.createElement)(BotMark, { bot, size: 40, live: true }),
+ (0, import_react40.createElement)(
"div",
{ className: "bt-remote-sheet-who" },
- (0, import_react39.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name),
- (0, import_react39.createElement)("span", { className: "bt-remote-sheet-sub" }, 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_react39.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react39.createElement)(CloseIcon))
+ (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_react39.createElement)(
+ (0, import_react40.createElement)(
"div",
{ className: "bt-remote-sheet-body" },
- bot.remote.handle ? (0, import_react39.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null,
- (0, import_react39.createElement)(FlyBotDetails, { bot, roster, actions })
+ 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 })
)
)
);
}
// src/client/overlays.js
-var import_react40 = require("react");
+var import_react41 = require("react");
var import_react_dom9 = require("react-dom");
var DIALOG_EXIT_MS = 160;
var MENU_EXIT_MS3 = 120;
@@ -16109,15 +16607,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_react40.useRef)(current);
- (0, import_react40.useEffect)(() => {
+ const previous = (0, import_react41.useRef)(current);
+ (0, import_react41.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_react40.useState)(open);
- (0, import_react40.useEffect)(() => {
+ const [kept, setKept] = (0, import_react41.useState)(open);
+ (0, import_react41.useEffect)(() => {
if (open) {
setKept(open);
return void 0;
@@ -16132,19 +16630,21 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity
const [memory, memoryLeaving] = useLinger(ui2.memory && roster.byId[ui2.memory] ? ui2.memory : null, DIALOG_EXIT_MS);
const [menu, menuLeaving] = useLinger(ui2.menu, MENU_EXIT_MS3);
const [inbox, inboxLeaving] = useLinger(ui2.inbox ?? null, DIALOG_EXIT_MS);
- return (0, import_react40.createElement)(
- import_react40.Fragment,
+ const [board, boardLeaving] = useLinger(ui2.board ?? null, DIALOG_EXIT_MS);
+ return (0, import_react41.createElement)(
+ import_react41.Fragment,
null,
- pane ? (0, import_react_dom9.createPortal)((0, import_react40.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_react40.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null,
- ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react40.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_react40.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null,
- ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react40.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
- menu ? (0, import_react_dom9.createPortal)((0, import_react40.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_react40.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
- remoteBotSettings ? (0, import_react_dom9.createPortal)((0, import_react40.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_react40.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
- inbox ? (0, import_react_dom9.createPortal)((0, import_react40.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), 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
);
}
@@ -17044,7 +17544,7 @@ function writeMode(mode, storage = globalThis.localStorage) {
}
// src/client/user-message.js
-var import_react41 = require("react");
+var import_react42 = require("react");
var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
var canDrawUserMessages = () => typeof ui.projectUserText === "function";
function contentParts(content) {
@@ -17125,38 +17625,38 @@ function userTextParts(text, bots, referenceLabels = []) {
return parts;
}
function UserText({ text, bots, referenceLabels, skillNames, references, actions }) {
- const chip = (part) => [(0, import_react41.createElement)(BotMark, { bot: part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`];
+ const chip = (part) => [(0, import_react42.createElement)(BotMark, { bot: part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`];
return userTextParts(text, bots, referenceLabels).map((part, index) => {
- if (!part.bot) return (0, import_react41.createElement)(import_react41.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
+ if (!part.bot) return (0, import_react42.createElement)(import_react42.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
const title = `@${part.bot.name}`;
- return typeof part.bot.id === "string" ? (0, import_react41.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react41.createElement)("span", { key: index, className: "bt-umention", title }, chip(part));
+ 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));
});
}
function Attachments({ attachments, renderMessageImages }) {
const compact2 = attachments.length > 1;
- return (0, import_react41.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => {
+ return (0, import_react42.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => {
if (block.type === "image") {
- return renderMessageImages ? (0, import_react41.createElement)(import_react41.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;
}
const file = block.attachment;
const meta = [ui.fileExtension?.(file.name)?.toUpperCase().slice(0, 8), ui.fileSizeText?.(file.bytes)].filter(Boolean).join(" · ");
- return (0, import_react41.createElement)(
+ return (0, import_react42.createElement)(
"span",
{ key: index, className: "bt-ufile", title: file.name },
- ui.FileTypeIcon ? (0, import_react41.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null,
- (0, import_react41.createElement)("span", { className: "bt-ufile-text" }, (0, import_react41.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react41.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)
);
}));
}
function CopyButton({ text }) {
- const [copied, setCopied] = (0, import_react41.useState)(false);
+ const [copied, setCopied] = (0, import_react42.useState)(false);
const copy2 = () => {
void navigator.clipboard?.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1e3);
});
};
- return (0, import_react41.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_react41.createElement)(CheckIcon) : (0, import_react41.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));
}
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 }) {
@@ -17173,7 +17673,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_react41.createElement)(
+ return (0, import_react42.createElement)(
"div",
{
className: incoming ? "bt-umsg bt-umsg-other" : "bt-umsg",
@@ -17183,22 +17683,22 @@ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageIma
"data-author-kind": author?.authorKind,
"data-author-name": author?.name
},
- person ? (0, import_react41.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react41.createElement)(PersonMark, { name: author.name })) : null,
- faceMine ? (0, import_react41.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react41.createElement)(PersonMark, { name: myName || void 0 })) : null,
- (0, import_react41.createElement)(
+ 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)(
"div",
{ className: "bt-ustack" },
- faceMine && myName ? (0, import_react41.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react41.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react41.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null,
- attachments.length > 0 ? (0, import_react41.createElement)(Attachments, { attachments, renderMessageImages }) : null,
- text !== "" || rest.length > 0 ? (0, import_react41.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)(
"div",
{ className: "bt-ubody", title: clock3(data.time) },
- (0, import_react41.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_react41.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)
) : null,
- referenceLabels.length > 0 ? (0, import_react41.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
),
- text === "" ? null : (0, import_react41.createElement)("div", { className: "bt-utools" }, (0, import_react41.createElement)(CopyButton, { text }))
+ text === "" ? null : (0, import_react42.createElement)("div", { className: "bt-utools" }, (0, import_react42.createElement)(CopyButton, { text }))
);
}
var USER_MESSAGE_CSS = `
@@ -17296,7 +17796,7 @@ function noticeApprovals({ work, roster, shownSession, openSession, since: since
}
// src/client/stats-dock.js
-var import_react42 = require("react");
+var import_react43 = require("react");
var STATS_DOCK = "conversation.composer.dock";
var STATS_ID = "stats";
var STATS_IDS = [STATS_ID, "activity"];
@@ -17312,10 +17812,10 @@ 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_react42.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]);
+ const t2 = (0, import_react43.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_react42.createElement)(entry.component, { ...props, t: t2 });
+ return (0, import_react43.createElement)(entry.component, { ...props, t: t2 });
}
return { component: StatsDock, inject: (...args) => shell()?.inject?.(...args) ?? {} };
}
@@ -17826,11 +18326,17 @@ function apply(ctx) {
},
// The Agent inbox (#349) opens over the conversation; Open on a card closes it first.
openInbox: () => {
- ui2.set((state) => ({ ...state, inbox: true, menu: null, palette: null, newChat: null }));
+ ui2.set((state) => ({ ...state, inbox: true, board: null, menu: null, palette: null, newChat: null }));
},
closeInbox: () => {
ui2.set((state) => state.inbox ? { ...state, inbox: null } : state);
},
+ openBoard: () => {
+ ui2.set((state) => ({ ...state, board: true, inbox: null, menu: null, palette: null, newChat: null }));
+ },
+ closeBoard: () => {
+ ui2.set((state) => state.board ? { ...state, board: null } : state);
+ },
openConnectors: () => {
ui2.set((state) => ({ ...state, settings: { page: "connectors", id: null }, menu: null, palette: null, newChat: null }));
},
@@ -17840,7 +18346,7 @@ function apply(ctx) {
if (mode !== "bot" && mode !== "agent") return;
if (surface.getSnapshot().mode === mode) return;
if (mode === "agent") {
- ui2.set((state) => ({ ...state, details: null, view: "details", settings: null, remoteBotSettings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, inbox: null, toast: null }));
+ ui2.set((state) => ({ ...state, details: null, view: "details", settings: null, remoteBotSettings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, inbox: null, board: null, toast: null }));
}
writeMode(mode);
surface.set((state) => ({ ...state, mode }));
@@ -17949,7 +18455,9 @@ function apply(ctx) {
approveCapability: (name) => after(call("capability-approve", { name })),
setBotConnector: (botId, connector, enabled) => after(call("bot-connectors", { botId, connector, enabled })),
setBotCapability: (botId, name, attached) => after(call("bot-capability", { botId, name, attached })),
- tasks: () => call("tasks"),
+ tasks: (payload) => call("tasks", payload ?? {}),
+ taskUpdate: (payload) => call("task-update", payload),
+ taskCreate: (payload) => call("task-create", payload),
schedules: () => call("schedules"),
scheduleCreate: (task) => call("schedule-create", task),
scheduleUpdate: (task) => call("schedule-update", task),
@@ -18183,6 +18691,7 @@ function apply(ctx) {
installCompiledStyles(USAGE_CSS, "usage"),
installCompiledStyles(SCHEDULE_CSS, "schedules"),
installCompiledStyles(INBOX_CSS, "inbox"),
+ installCompiledStyles(KANBAN_CSS, "kanban"),
installCompiledStyles(FEEDBACK_CSS, "feedback"),
installCompiledStyles(ABOUT_CSS, "about"),
installCompiledStyles(FLYBOT_CSS, "flybot"),
diff --git a/core/src/client/i18n.js b/core/src/client/i18n.js
index c44246ba4..1893708e5 100644
--- a/core/src/client/i18n.js
+++ b/core/src/client/i18n.js
@@ -605,6 +605,29 @@ const ZH = {
'Latest: {text}': '最新:{text}',
'{count} tool calls': '{count} 次工具调用',
'Untitled task': '未命名任务',
+ // Bot task board (#1058)
+ Tasks: '任务',
+ 'Close tasks': '关闭任务',
+ 'Task board': '任务板',
+ 'Not started': '未开始',
+ 'Groups you admin': '你管理的群',
+ 'No tasks yet.': '还没有任务。',
+ 'Nothing in this column.': '这一列是空的。',
+ Highest: '最高',
+ High: '较高',
+ Low: '较低',
+ Claimed: '已认领',
+ Blocked: '受阻',
+ Start: '开始',
+ Complete: '完成',
+ 'Start task': '开始任务',
+ 'Complete task': '完成任务',
+ 'Add task': '添加任务',
+ 'Task title': '任务标题',
+ Progress: '进度',
+ Group: '群',
+ '{percent}%': '{percent}%',
+ '{done}/{total}': '{done}/{total}',
'for {time}': '已 {time}',
'just started': '刚开始',
'Open {name}': '打开 {name}',
diff --git a/core/src/client/index.js b/core/src/client/index.js
index 53e4fdcdf..c9d609f8e 100644
--- a/core/src/client/index.js
+++ b/core/src/client/index.js
@@ -30,6 +30,7 @@ import { FOLDER_CSS } from './folder-menu.js'
import { USAGE_CSS } from './usage-page.js'
import { SCHEDULE_CSS } from './schedules.js'
import { INBOX_CSS } from './inbox.js'
+import { KANBAN_CSS } from './kanban.js'
import { MOBILE_CSS } from './mobile.js'
import { PANE_MIN_WIDTH_CSS } from './panes.js'
import { STORAGE, migrateStorage } from './storage.js'
@@ -485,8 +486,10 @@ export function apply(ctx) {
openMemory: (botId) => { ui.set(state => ({ ...state, memory: botId, menu: null, palette: null })) },
closeMemory: () => { ui.set(state => (state.memory ? { ...state, memory: null } : state)) },
// The Agent inbox (#349) opens over the conversation; Open on a card closes it first.
- openInbox: () => { ui.set(state => ({ ...state, inbox: true, menu: null, palette: null, newChat: null })) },
+ openInbox: () => { ui.set(state => ({ ...state, inbox: true, board: null, menu: null, palette: null, newChat: null })) },
closeInbox: () => { ui.set(state => (state.inbox ? { ...state, inbox: null } : state)) },
+ openBoard: () => { ui.set(state => ({ ...state, board: true, inbox: null, menu: null, palette: null, newChat: null })) },
+ closeBoard: () => { ui.set(state => (state.board ? { ...state, board: null } : state)) },
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.
@@ -494,7 +497,7 @@ export function apply(ctx) {
if (mode !== 'bot' && mode !== 'agent') return
if (surface.getSnapshot().mode === mode) return
if (mode === 'agent') {
- ui.set(state => ({ ...state, details: null, view: 'details', settings: null, remoteBotSettings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, inbox: null, toast: null }))
+ ui.set(state => ({ ...state, details: null, view: 'details', settings: null, remoteBotSettings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, inbox: null, board: null, toast: null }))
}
writeMode(mode)
surface.set(state => ({ ...state, mode }))
@@ -596,7 +599,9 @@ export function apply(ctx) {
approveCapability: name => after(call('capability-approve', { name })),
setBotConnector: (botId, connector, enabled) => after(call('bot-connectors', { botId, connector, enabled })),
setBotCapability: (botId, name, attached) => after(call('bot-capability', { botId, name, attached })),
- tasks: () => call('tasks'),
+ tasks: (payload) => call('tasks', payload ?? {}),
+ taskUpdate: (payload) => call('task-update', payload),
+ taskCreate: (payload) => call('task-create', payload),
schedules: () => call('schedules'),
scheduleCreate: task => call('schedule-create', task),
scheduleUpdate: task => call('schedule-update', task),
@@ -851,6 +856,7 @@ export function apply(ctx) {
installCompiledStyles(USAGE_CSS, 'usage'),
installCompiledStyles(SCHEDULE_CSS, 'schedules'),
installCompiledStyles(INBOX_CSS, 'inbox'),
+ installCompiledStyles(KANBAN_CSS, 'kanban'),
installCompiledStyles(FEEDBACK_CSS, 'feedback'),
installCompiledStyles(ABOUT_CSS, 'about'),
installCompiledStyles(FLYBOT_CSS, 'flybot'),
diff --git a/core/src/client/kanban.js b/core/src/client/kanban.js
new file mode 100644
index 000000000..6087e004b
--- /dev/null
+++ b/core/src/client/kanban.js
@@ -0,0 +1,383 @@
+// Per-Bot task board (#1058). The records are the task list in src/host/tasks.js.
+// This is not the Agent inbox's board (#471): Inbox stays a list of what needs the
+// owner. Tasks opens one Bot's board, a child of that Bot, or a group an admin can see.
+import { createElement as h, useCallback, useEffect, useState } from 'react'
+import { columnOf, priorityOf, progressOf } from '../shared/task-board.js'
+import { CloseIcon } from './icons.js'
+import { useEscape } from './overlay-hooks.js'
+import { t } from './i18n.js'
+
+const POLL_MS = 15_000
+
+function BoardIcon() {
+ return h('svg', { width: 18, height: 18, viewBox: '0 0 20 20', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinejoin: 'round', 'aria-hidden': true },
+ h('rect', { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }),
+ h('rect', { x: 8, y: 3, width: 4, height: 9, rx: 1 }),
+ h('rect', { x: 13.5, y: 3, width: 4, height: 12, rx: 1 }))
+}
+
+export function shortId(id) {
+ const hex = String(id ?? '').replace(/^task-/, '')
+ return `#${hex.slice(-6)}`
+}
+
+export function boardColumns(tasks) {
+ const columns = { 'not-started': [], 'in-progress': [], done: [] }
+ for (const task of tasks ?? []) {
+ const column = columnOf(task)
+ ;(columns[column] ?? columns['not-started']).push(task)
+ }
+ for (const list of Object.values(columns)) list.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))
+ return columns
+}
+
+function adminOf(roster, room) {
+ const me = roster?.self?.memberId
+ if (!me) return room?.origin !== 'face-to-face'
+ const human = (room?.participants ?? []).find(person => person.kind === 'human' && person.id === me && person.leftSeq === undefined)
+ if (human?.role === 'admin') return true
+ const admin = roster?.byId?.[room?.admin]
+ return Boolean(admin && !admin.remote && admin.ownerId === me)
+}
+
+// Own Bots, nested by who created them, then the groups this person administers.
+export function boardRail(roster) {
+ const me = roster?.self?.memberId
+ const bots = (roster?.bots ?? []).filter(bot => bot && !bot.remote && (!me || !bot.ownerId || bot.ownerId === me))
+ const ids = new Set(bots.map(bot => bot.id))
+ const children = new Map()
+ for (const bot of bots) {
+ const parent = bot.createdBy && ids.has(bot.createdBy) ? bot.createdBy : ''
+ const list = children.get(parent) ?? []
+ list.push(bot)
+ children.set(parent, list)
+ }
+ for (const list of children.values()) list.sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0) || String(a.name).localeCompare(String(b.name)))
+ const rows = []
+ const walk = (parent, depth) => {
+ for (const bot of children.get(parent) ?? []) {
+ rows.push({ kind: 'bot', id: bot.id, name: bot.name, depth, bot })
+ walk(bot.id, depth + 1)
+ }
+ }
+ walk('', 0)
+ const groups = (roster?.rooms ?? [])
+ .filter(room => room && room.hidden !== true && room.origin !== 'face-to-face' && adminOf(roster, room))
+ .map(room => ({ kind: 'group', id: room.id, name: room.displayName || room.name || t('Group'), room }))
+ return { bots: rows, groups }
+}
+
+export function canMoveTask(roster, task) {
+ const me = roster?.self?.memberId
+ if (!me) return true
+ const bot = roster?.byId?.[task?.ownerBotId]
+ return Boolean(bot && !bot.remote && (!bot.ownerId || bot.ownerId === me))
+}
+
+function priorityLabel(priority) {
+ if (priority === 'urgent') return t('Highest')
+ if (priority === 'high') return t('High')
+ if (priority === 'low') return t('Low')
+ return t('Normal')
+}
+
+function statusLabel(status) {
+ switch (status) {
+ case 'claimed': return t('Claimed')
+ case 'working': return t('In progress')
+ case 'blocked': return t('Blocked')
+ case 'result-ready': return t('Result ready')
+ case 'accepted': return t('Done')
+ case 'cancelled': return t('Cancelled')
+ case 'failed': return t('Failed')
+ default: return t('Not started')
+ }
+}
+
+function dropLabel(column) {
+ if (column === 'in-progress') return t('Start task')
+ if (column === 'done') return t('Complete task')
+ return t('Not started')
+}
+
+function columnTitle(column) {
+ if (column === 'in-progress') return t('In progress')
+ if (column === 'done') return t('Done')
+ return t('Not started')
+}
+
+function botName(roster, task) {
+ return roster?.byId?.[task.ownerBotId]?.name || task.ownerBotName || ''
+}
+
+function lanesOf(tasks, roster) {
+ const order = []
+ const map = new Map()
+ for (const task of tasks) {
+ const id = task.ownerBotId ?? ''
+ if (!map.has(id)) { map.set(id, []); order.push(id) }
+ map.get(id).push(task)
+ }
+ return order.map(id => ({ id, name: botName(roster, map.get(id)[0]) || id, tasks: map.get(id) }))
+}
+
+export function useBoardTasks(actions, scope) {
+ const [own, setOwn] = useState([])
+ const [groupTasks, setGroupTasks] = useState([])
+ const key = scope?.kind === 'group' ? `group:${scope.roomId}` : 'own'
+ useEffect(() => {
+ let stopped = false
+ const load = () => {
+ const job = scope?.kind === 'group'
+ ? Promise.resolve(actions?.tasks?.({ roomId: scope.roomId }))
+ : Promise.resolve(actions?.tasks?.())
+ // A reading host or an older core has no board; keep the list already shown.
+ void job.then((list) => {
+ if (stopped || !Array.isArray(list)) return
+ if (scope?.kind === 'group') setGroupTasks(list)
+ else setOwn(list)
+ }).catch(() => undefined)
+ }
+ load()
+ const timer = setInterval(load, POLL_MS)
+ return () => { stopped = true; clearInterval(timer) }
+ }, [key])
+ const tasks = scope?.kind === 'group' ? groupTasks : own.filter(task => !scope?.botId || task.ownerBotId === scope.botId)
+ const replace = (task) => {
+ const write = (list) => (list.some(item => item.id === task.id) ? list.map(item => item.id === task.id ? task : item) : [...list, task])
+ if (scope?.kind === 'group') setGroupTasks(write)
+ else setOwn(write)
+ }
+ return { own, tasks, replace }
+}
+
+function RailButton({ row, selected, meta, onSelect }) {
+ return h('button', {
+ type: 'button',
+ className: 'bt-board-rail-item',
+ 'data-kind': row.kind,
+ 'data-id': row.id,
+ 'data-selected': selected || undefined,
+ 'aria-current': selected ? 'true' : undefined,
+ style: row.depth ? { paddingLeft: `${10 + row.depth * 14}px` } : undefined,
+ onClick: () => onSelect(row.kind === 'group' ? { kind: 'group', roomId: row.id } : { kind: 'bot', botId: row.id }),
+ },
+ h('span', { className: 'bt-board-rail-name' }, row.name),
+ meta ? h('span', { className: 'bt-board-rail-meta' }, meta) : null)
+}
+
+export function BoardCard({ task, roster, movable, onMove, onDrag }) {
+ const column = columnOf(task)
+ const priority = priorityOf(task)
+ const progress = progressOf(task)
+ const who = botName(roster, task)
+ return h('article', {
+ className: 'bt-board-card',
+ 'data-task': task.id,
+ 'data-column': column,
+ 'data-priority': priority,
+ draggable: movable || undefined,
+ onDragStart: (event) => {
+ event.dataTransfer?.setData('text/plain', task.id)
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
+ onDrag?.(task.id)
+ },
+ onDragEnd: () => onDrag?.(null),
+ },
+ h('span', { className: 'bt-board-card-top' },
+ h('span', { className: 'bt-board-id' }, shortId(task.id)),
+ h('span', { className: 'bt-board-priority', 'data-priority': priority }, priorityLabel(priority)),
+ h('span', { className: 'bt-board-status' }, statusLabel(task.status))),
+ h('span', { className: 'bt-board-title' }, task.title || t('Untitled task')),
+ task.note ? h('span', { className: 'bt-board-note' }, task.note) : null,
+ h('span', { className: 'bt-board-foot' },
+ who ? h('span', { className: 'bt-board-who' }, who) : null,
+ h('span', { className: 'bt-board-progress' },
+ h('span', { className: 'bt-board-progress-bar', role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': progress, 'aria-label': t('Progress') },
+ h('span', { style: { width: `${progress}%` } })),
+ h('span', { className: 'bt-board-percent' }, t('{percent}%', { percent: progress })))),
+ movable && column === 'not-started' ? h('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' ? h('button', { type: 'button', className: 'bt-soft bt-soft-sm', onClick: () => onMove?.(task.id, 'done') }, t('Complete')) : null)
+}
+
+function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag }) {
+ return h('section', {
+ className: 'bt-board-column',
+ 'data-column': column,
+ 'aria-label': columnTitle(column),
+ onDragOver: (event) => { if (!dragging) return; event.preventDefault() },
+ onDrop: (event) => {
+ event.preventDefault()
+ const id = event.dataTransfer?.getData('text/plain')
+ if (id) onMove?.(id, column)
+ onDrag?.(null)
+ },
+ },
+ h('h3', { className: 'bt-board-column-head' },
+ h('span', null, columnTitle(column)),
+ h('span', { className: 'bt-board-count' }, String(tasks.length))),
+ dragging ? h('div', { className: 'bt-board-drop' }, dropLabel(column)) : null,
+ tasks.length === 0 && !dragging
+ ? h('p', { className: 'bt-board-empty' }, t('Nothing in this column.'))
+ : lanes
+ ? lanesOf(tasks, roster).map(lane => h('div', { key: lane.id, className: 'bt-board-lane' },
+ h('span', { className: 'bt-board-lane-name' }, lane.name),
+ lane.tasks.map(task => h(BoardCard, { key: task.id, task, roster, movable: canMoveTask(roster, task), onMove, onDrag }))))
+ : tasks.map(task => h(BoardCard, { key: task.id, task, roster, movable: canMoveTask(roster, task), onMove, onDrag })))
+}
+
+export function BoardView({ roster, tasks, rail, scope, counts, 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 h('div', { className: 'bt-board-body' },
+ h('aside', { className: 'bt-board-rail', 'aria-label': t('Task board') },
+ h('h3', { className: 'bt-board-rail-head' }, t('Your Bots')),
+ rail.bots.length === 0 ? h('p', { className: 'bt-board-empty' }, t('No tasks yet.')) : rail.bots.map(row => {
+ const count = counts?.[row.id] ?? { done: 0, total: 0 }
+ return h(RailButton, {
+ key: row.id, row, selected: row.id === selectedBot, onSelect,
+ meta: count.total > 0 ? t('{done}/{total}', { done: count.done, total: count.total }) : '',
+ })
+ }),
+ rail.groups.length > 0 ? h('h3', { className: 'bt-board-rail-head' }, t('Groups you admin')) : null,
+ rail.groups.map(row => h(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect }))),
+ h('div', { className: 'bt-board-main' },
+ error ? h('p', { className: 'bt-board-error', role: 'alert' }, error) : null,
+ scope?.kind === 'bot' && onCreate ? h('form', { className: 'bt-board-add', onSubmit: (event) => { event.preventDefault(); onCreate(draft) } },
+ h('input', { 'aria-label': t('Task title'), placeholder: t('New task'), value: draft ?? '', onChange: (event) => onDraft?.(event.target.value) }),
+ h('button', { type: 'submit', className: 'bt-soft bt-soft-sm' }, t('Add task'))) : null,
+ h('div', { className: 'bt-board-columns' },
+ ['not-started', 'in-progress', 'done'].map(column => h(BoardColumn, {
+ key: column, column, tasks: columns[column], roster, lanes, dragging, onMove, onDrag,
+ })))))
+}
+
+export function KanbanRow({ wide, useUi, actions }) {
+ const open = useUi ? useUi(value => Boolean(value.board)) : false
+ const label = t('Tasks')
+ return h('button', {
+ type: 'button', className: 'bt-row bt-board-row', 'aria-label': label, title: wide ? undefined : label,
+ 'aria-haspopup': 'dialog', 'aria-expanded': open, onClick: () => actions.openBoard(),
+ },
+ h('span', { className: 'bt-board-mark' }, h(BoardIcon)),
+ wide ? h('span', { className: 'bt-board-label' }, label) : null)
+}
+
+function scopeName(roster, scope) {
+ if (scope?.kind === 'group') return roster.roomsById?.[scope.roomId]?.name || t('Group')
+ if (scope?.kind === 'bot') return roster.byId?.[scope.botId]?.name || ''
+ return ''
+}
+
+export function KanbanDialog({ roster, actions, leaving = false }) {
+ const rail = boardRail(roster)
+ const [scope, setScope] = 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] = useState(null)
+ const [error, setError] = useState('')
+ const [draft, setDraft] = useState('')
+ const { own, tasks, replace } = useBoardTasks(actions, scope)
+ const close = useCallback(() => actions.closeBoard(), [])
+ useEscape(close)
+ const counts = {}
+ for (const task of own) {
+ const id = task.ownerBotId
+ const bucket = counts[id] ?? (counts[id] = { done: 0, total: 0 })
+ if (task.status === 'cancelled' || task.status === 'failed') continue
+ bucket.total += 1
+ if (columnOf(task) === 'done') bucket.done += 1
+ }
+ const move = async (id, column) => {
+ const task = tasks.find(item => item.id === id) ?? own.find(item => item.id === id)
+ if (!task || !canMoveTask(roster, task) || columnOf(task) === column) return
+ setError('')
+ try {
+ replace(await actions.taskUpdate({ id, column }))
+ } catch (failure) {
+ setError(failure?.message ?? String(failure))
+ }
+ }
+ const create = async (title) => {
+ const text = String(title ?? '').trim()
+ if (!text || scope?.kind !== 'bot') return
+ setError('')
+ try {
+ replace(await actions.taskCreate({ botId: scope.botId, title: text, column: 'not-started' }))
+ setDraft('')
+ } catch (failure) {
+ setError(failure?.message ?? String(failure))
+ }
+ }
+ const name = scopeName(roster, scope)
+ return h('div', { className: 'bt-mem-layer bt-board-layer', 'data-leaving': leaving || undefined },
+ h('div', { className: 'bt-scrim', onClick: close }),
+ h('div', { className: 'bt-mem bt-board', role: 'dialog', 'aria-modal': true, 'aria-labelledby': 'bt-board-heading' },
+ h('div', { className: 'bt-mem-head' },
+ h('h2', { id: 'bt-board-heading' }, t('Tasks')),
+ h('span', { className: 'bt-mem-status' }, name),
+ h('span', { className: 'bt-mem-head-gap' }),
+ h('button', { type: 'button', className: 'bt-icon-btn', 'aria-label': t('Close tasks'), autoFocus: true, onClick: close }, h(CloseIcon))),
+ h(BoardView, {
+ roster, tasks, rail, scope, counts, dragging, error, draft,
+ onDraft: setDraft, onSelect: setScope, onMove: move, onDrag: setDragging, onCreate: create,
+ })))
+}
+
+export const 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)}
+.bt-board-mark svg{width:18px;height:18px}
+.bt-board-label{flex:1 1 auto;min-width:0;font-size:14px;line-height:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.bt-mem.bt-board{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))}
+.bt-board-body{flex:1 1 auto;min-height:0;display:flex;gap:0}
+.bt-board-rail{flex:none;width:232px;overflow:auto;border-right:.5px solid var(--bt-line);padding:8px 8px 16px;display:flex;flex-direction:column;gap:2px}
+.bt-board-rail-head{margin:10px 8px 4px;font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3)}
+.bt-board-rail-item{display:flex;align-items:center;gap:8px;width:100%;min-height:36px;padding:6px 10px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer}
+.bt-board-rail-item:hover{background:var(--bt-hover)}
+.bt-board-rail-item[data-selected]{background:var(--bt-active,var(--bt-hover))}
+.bt-board-rail-name{flex:1 1 auto;min-width:0;font-size:13.5px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.bt-board-rail-meta{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:10px;padding:12px 14px 16px}
+.bt-board-add{display:flex;gap:8px}
+.bt-board-add input{flex:1 1 auto;min-width:0;height:32px;padding:0 10px;border:.5px solid var(--bt-line-2);border-radius:10px;background:var(--bt-card,var(--bt-main));color:var(--bt-ink);font:inherit}
+.bt-board-columns{flex:1 1 auto;min-height:0;display:flex;gap:10px}
+.bt-board-column{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:8px;padding:8px;border-radius:14px;background:var(--bt-hover);overflow:auto}
+.bt-board-column-head{display:flex;align-items:center;gap:6px;margin:0;padding:2px 4px;font-size:13px;line-height:18px;font-weight:600;color:var(--bt-ink-2)}
+.bt-board-count{font-weight:500;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-drop{margin:0;padding:18px 8px;border:1px dashed var(--bt-line-2);border-radius:12px;text-align:center;font-size:13px;line-height:18px;color:var(--bt-ink-3)}
+.bt-board-empty{margin:0;padding:8px 4px;font-size:13px;line-height:18px;color:var(--bt-ink-3)}
+.bt-board-error{margin:0;font-size:13px;line-height:18px;color:#c21d2e}
+.bt-board-lane{display:flex;flex-direction:column;gap:6px}
+.bt-board-lane-name{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3);padding:0 2px}
+.bt-board-card{display:flex;flex-direction:column;gap:6px;padding:10px;border:.5px solid var(--bt-line-2);border-radius:12px;background:var(--bt-card,var(--bt-main));cursor:grab}
+.bt-board-card-top{display:flex;align-items:center;gap:6px;min-width:0}
+.bt-board-id{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-priority{flex:none;height:18px;padding:0 6px;border-radius:999px;font-size:11px;line-height:18px;font-weight:600}
+.bt-board-priority[data-priority=urgent]{background:rgba(224,33,53,.12);color:#c21d2e}
+.bt-board-priority[data-priority=high]{background:rgba(217,119,6,.14);color:#b45309}
+.bt-board-priority[data-priority=normal]{background:color-mix(in srgb,var(--bt-accent) 14%,transparent);color:color-mix(in srgb,var(--bt-accent) 80%,var(--bt-ink))}
+.bt-board-priority[data-priority=low]{background:color-mix(in srgb,var(--bt-green) 16%,transparent);color:color-mix(in srgb,var(--bt-green) 70%,var(--bt-ink))}
+.bt-board-status{margin-left:auto;font-size:11.5px;line-height:16px;color:var(--bt-ink-3);white-space:nowrap}
+.bt-board-title{font-size:13.5px;line-height:19px;color:var(--bt-ink)}
+.bt-board-note{font-size:12px;line-height:16px;color:var(--bt-ink-2)}
+.bt-board-foot{display:flex;align-items:center;gap:8px}
+.bt-board-who{flex:none;max-width:40%;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.bt-board-progress{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:6px}
+.bt-board-progress-bar{flex:1 1 auto;height:4px;border-radius:999px;background:var(--bt-line);overflow:hidden}
+.bt-board-progress-bar>span{display:block;height:100%;background:var(--bt-accent)}
+.bt-board-percent{flex:none;font-size:11.5px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-card .bt-soft{align-self:flex-start}
+@media (max-width:800px){
+ .bt-mem.bt-board{width:min(100vw,calc(100vw - 16px));height:100%}
+ .bt-board-body{flex-direction:column}
+ .bt-board-rail{width:auto;max-height:180px;border-right:0;border-bottom:.5px solid var(--bt-line)}
+ .bt-board-columns{overflow:auto}
+ .bt-board-column{min-width:220px}
+}
+@media (max-width:640px){
+ .bt-mem.bt-board{width:100vw;border-radius:0;border:0}
+ .bt-board-columns{flex-direction:column}
+ .bt-board-column{min-width:0}
+}
+`
diff --git a/core/src/client/overlays.js b/core/src/client/overlays.js
index c3891beaf..192d850cb 100644
--- a/core/src/client/overlays.js
+++ b/core/src/client/overlays.js
@@ -10,6 +10,7 @@ import { PANEL_LINGER_MS, BotPanel } from './browser-pane.js'
import { MemoryDialog } from './memory-panel.js'
import { RemoteBotSettingsSheet } from './remote-bot-settings.js'
import { InboxDialog } from './inbox.js'
+import { KanbanDialog } from './kanban.js'
import { useLinger } from './motion.js'
// How long a closing dialog or menu stays for its exit (see their [data-leaving] CSS).
@@ -43,6 +44,7 @@ export function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useA
const [memory, memoryLeaving] = useLinger(ui.memory && roster.byId[ui.memory] ? ui.memory : null, DIALOG_EXIT_MS)
const [menu, menuLeaving] = useLinger(ui.menu, MENU_EXIT_MS)
const [inbox, inboxLeaving] = useLinger(ui.inbox ?? null, DIALOG_EXIT_MS)
+ const [board, boardLeaving] = useLinger(ui.board ?? null, DIALOG_EXIT_MS)
return h(Fragment, null,
pane ? createPortal(h(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null,
ui.newChat ? createPortal(h(NewChat, { key: ui.newChat, mode: ui.newChat, roster, actions }), document.body) : null,
@@ -53,5 +55,6 @@ export function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useA
settings ? createPortal(h(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
remoteBotSettings ? createPortal(h(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null,
memory && roster.byId[memory] ? createPortal(h(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
- inbox ? createPortal(h(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null)
+ inbox ? createPortal(h(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null,
+ board ? createPortal(h(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null)
}
diff --git a/core/src/client/sidebar.js b/core/src/client/sidebar.js
index 36078a6fc..b7805c848 100644
--- a/core/src/client/sidebar.js
+++ b/core/src/client/sidebar.js
@@ -14,6 +14,7 @@ import { hostText, t } from './i18n.js'
import { friendBotTitle, roomTitle, sharedRoomTitle, isShownRemoteChat } from '../shared/remote-label.js'
import { showsDshRow } from './dsh-offer.js'
import { InboxRow } from './inbox.js'
+import { KanbanRow } from './kanban.js'
// -------------------------------------------------------------------------
// Sidebar
@@ -389,6 +390,7 @@ export function TeamSidebar(props) {
h('button', { type: 'button', className: 'bt-round', 'aria-label': t('New…'), title: t('New…'), onClick: () => actions.openNewChat('new') }, h(PlusIcon))),
wide && roster.readOnly ? h('div', { className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null,
h(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
+ h(KanbanRow, { wide, useUi, actions }),
h('div', { ref: listRef, className: 'bt-list', role: 'region', 'aria-label': t('Bot list') },
tree ? h('div', { className: 'bt-tree', role: 'group', 'aria-label': t('Bot tree') }, tree.map(node => h(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions })))
: main ? h(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : h('div', { className: 'bt-hint' }, t('Setting up your Main Bot…')),
diff --git a/core/src/host/authz.js b/core/src/host/authz.js
index f09a47a5b..855730add 100644
--- a/core/src/host/authz.js
+++ b/core/src/host/authz.js
@@ -58,7 +58,7 @@ const SESSION_TARGETED = new Set(['earlier', 'send', 'answer-question', 'dismiss
// Task channel names. Requester checks live on the record in tasks.js; listing them
// here keeps a future `task-*` from auto-allowing for any signed-in actor (#111).
const TASK_ENDPOINTS = new Set([
- 'tasks', 'task-create', 'task-accept', 'task-start', 'task-return', 'task-accept-result', 'task-cancel',
+ 'tasks', 'task-create', 'task-update', 'task-accept', 'task-start', 'task-return', 'task-accept-result', 'task-cancel',
'task-wait-input', 'task-answer-input',
])
diff --git a/core/src/host/channel.js b/core/src/host/channel.js
index 625e1a7b4..75425cd8b 100644
--- a/core/src/host/channel.js
+++ b/core/src/host/channel.js
@@ -620,6 +620,7 @@ export function install(rt) {
return ok(true)
case 'tasks':
case 'task-create':
+ case 'task-update':
case 'task-accept':
case 'task-start':
case 'task-return':
diff --git a/core/src/host/constants.js b/core/src/host/constants.js
index 0f5b40321..b564bacce 100644
--- a/core/src/host/constants.js
+++ b/core/src/host/constants.js
@@ -50,7 +50,7 @@ export const AGENT_SESSION_TITLE_MAX = 80
export const TEAM_TOOLS = [
'list_bots', 'message_bot', 'stop_bot', 'create_bot', 'update_bot', 'list_models',
'bot_schedules',
- 'create_group', 'update_group', 'delete_group', 'post_to_group', 'ask_user',
+ 'create_group', 'update_group', 'delete_group', 'post_to_group', 'ask_user', 'track_tasks',
'request_secret', 'list_secrets', 'group_relay', 'read_own_chat',
'read_group_chat', 'remember', 'forget', 'recall', 'read_browser',
'list_capabilities', 'save_capability', 'delete_capability',
diff --git a/core/src/host/prompt.js b/core/src/host/prompt.js
index 64aabdef6..4654ad86c 100644
--- a/core/src/host/prompt.js
+++ b/core/src/host/prompt.js
@@ -22,6 +22,7 @@ const TEAM_RULES = [
'- web_search results say which search backend answered. If web_search fails, never guess URLs or present pages you remember as search results: use a browser tool if you have one, otherwise tell the user that search is unavailable and the reason it gave.',
'- Your conversations are long-lived. When one grows long, either its older messages are condensed in place into a summary you wrote (a "[Checkpoint N]" message), or it moves on to a fresh part that opens with a handoff note you wrote and your latest messages (a "[Handoff · part N]" message). What leaves your working context stays saved. When you need the exact earlier wording or a detail the summary or note does not show in full (a full path, a figure, every item of a complete list or a final summary), call read_own_chat with words to look for, or with part N to read the note that closed an earlier part; never fill such a detail in from memory. Each line it returns starts with an anchor such as [part 2 #1234 · 10-08 14:22]; pass the anchor as around to read what was said around it.',
'- Reply in the user\'s language.',
+ '- You keep your own task list with track_tasks. When the user gives you work, or a group hands you work, record it there (a short title, column not-started / in-progress / done, how far along from 0 to 100, and a priority) and update that same task as it moves. Do not record work nobody asked for. Call track_tasks with no tasks to read the list.',
].join('\n')
const MAIN_RULES = [
diff --git a/core/src/host/tasks.js b/core/src/host/tasks.js
index 9af08f121..4ab53b474 100644
--- a/core/src/host/tasks.js
+++ b/core/src/host/tasks.js
@@ -7,13 +7,16 @@
//
// Create is grant-aware: the requester need not own the Bot. A granted child (or a
// Bot in the named room the requester currently sits in) may take a task within
-// its authority. Outgoing holds stay the owner's (#52 card).
+// its authority. Outgoing holds stay the owner's (#52 card). The board (#1058)
+// also lets that Bot's owner set priority, progress, note and column; the other
+// transitions stay the requester's.
//
// States: open → claimed → working → blocked ⇄ working → result-ready → accepted;
// A returned result may also be cancelled before acceptance. Cancelled and failed
// are terminal, and accepted is reachable only from result-ready. A cancelled or
// failed task never becomes accepted. Records keep pointers and a summary, not transcripts.
import { randomBytes } from 'node:crypto'
+import { columnOf } from '../shared/task-board.js'
import { currentParticipants } from './room-membership.js'
const ACTIVE = new Set(['open', 'claimed', 'working', 'blocked'])
@@ -36,6 +39,34 @@ const forbidden = message => Object.assign(new Error(message), { code: 'bot/forb
const invalid = message => Object.assign(new Error(message), { code: 'bot/invalid' })
const waitingForInput = task => task.status === 'blocked' && task.waitingFor === 'input'
const canLinkSend = task => task.status === 'working' || waitingForInput(task)
+const PRIORITY = new Set(['urgent', 'high', 'normal', 'low'])
+const WALK = ['open', 'claimed', 'working', 'result-ready', 'accepted']
+const TERMINAL_MATCH = new Set(['accepted', 'cancelled', 'failed'])
+
+const readPriority = (value) => {
+ if (value === undefined || value === null || value === '') return undefined
+ const priority = String(value)
+ if (!PRIORITY.has(priority)) throw invalid('Priority must be urgent, high, normal or low')
+ return priority
+}
+const readProgress = (value) => {
+ if (value === undefined || value === null || value === '') return undefined
+ const progress = typeof value === 'number' ? value : Number(value)
+ if (!Number.isInteger(progress) || progress < 0 || progress > 100) throw invalid('Progress must be a whole number from 0 to 100')
+ return progress
+}
+const oneLine = (value, max) => String(value ?? '').replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max)
+const readTitle = value => oneLine(value, 300)
+const readNote = (value) => {
+ if (value === undefined || value === null) return undefined
+ return oneLine(value, 300)
+}
+const readColumn = (value) => {
+ if (value === undefined || value === null || value === '') return undefined
+ const column = String(value)
+ if (column !== 'not-started' && column !== 'in-progress' && column !== 'done') throw invalid('Column must be not-started, in-progress or done')
+ return column
+}
const selectedInput = (value, name) => {
if (typeof value !== 'string' || !value.trim() || value.length > 2000) throw invalid(`${name} must contain 1–2000 characters`)
return value
@@ -99,7 +130,6 @@ export function install(rt) {
record(task, actor, extra)
}
- const view = task => ({ ...task, actors: [...task.actors ?? []] })
const prune = () => {
const all = Object.values(store())
const droppable = all.filter(task => !PROTECTED.has(task.status)).sort((a, b) => a.updatedAt - b.updatedAt)
@@ -107,6 +137,154 @@ export function install(rt) {
}
const save = () => rt.save()
+ const botRecord = id => state.bots?.[id]
+ // The human who owns this Bot. With no signed-in member the single-human product
+ // may see every local Bot, the same way the rest of the task API does.
+ const owns = (actor, botId) => {
+ const bot = botRecord(botId)
+ if (!bot || bot.remote !== undefined) return false
+ if (!rt.authzEnforced?.()) return true
+ return Boolean(actor?.memberId && bot.ownerId === actor.memberId)
+ }
+ // Walk createdBy. Owning the Bot, or any ancestor, is enough to read its tasks.
+ const ancestorOwned = (actor, botId) => {
+ const seen = new Set()
+ let id = botId
+ while (id && !seen.has(id)) {
+ seen.add(id)
+ if (owns(actor, id)) return true
+ id = botRecord(id)?.createdBy
+ }
+ return false
+ }
+ // A group admin is a current human participant with that role, or the owner of
+ // the Bot that administers the group. Membership alone is not enough.
+ const roomAdmin = (actor, room) => {
+ if (!room || !actor?.memberId) return false
+ const me = currentParticipants(room).find(p => p.kind === 'human' && p.id === actor.memberId)
+ if (me?.role === 'admin') return true
+ return owns(actor, room.admin)
+ }
+ const mayUpdate = (task, actor) => {
+ if (!rt.authzEnforced?.()) return true
+ return mayAct(task, actor) || owns(actor, task.ownerBotId)
+ }
+ const view = task => ({
+ ...task,
+ actors: [...task.actors ?? []],
+ ...(botRecord(task.ownerBotId)?.name ? { ownerBotName: botRecord(task.ownerBotId).name } : {}),
+ })
+
+ // Move a card along the existing states. Not started is open/claimed, In progress
+ // is working (or blocked / result-ready), Done is accepted. Reverse moves and a
+ // blocked wait are refused: the wait has its own resume.
+ function applyColumn(task, column, actor, by) {
+ if (!column || columnOf(task) === column) return false
+ if (task.status === 'blocked') throw stale('A blocked task stays put until its wait is cleared')
+ if (task.status === 'cancelled' || task.status === 'failed') throw stale(`a task that is ${task.status} cannot become ${column}`)
+ if (column === 'not-started') throw stale('a task that is in progress or done cannot move back to not started')
+ const target = column === 'in-progress' ? 'working' : 'accepted'
+ const from = WALK.indexOf(task.status)
+ const goal = WALK.indexOf(target)
+ if (from < 0 || goal < 0 || from > goal) throw stale(`a task that is ${task.status} cannot become ${column}`)
+ const extra = by ? { by } : undefined
+ for (let i = from + 1; i <= goal; i++) {
+ const next = WALK[i]
+ transition(task, next, actor, extra)
+ if (next === 'result-ready') task.result = { kind: 'summary', summary: (task.note || 'Completed').slice(0, 2000) }
+ }
+ return true
+ }
+
+ function applyFields(task, body) {
+ const title = body.title !== undefined ? readTitle(body.title) : undefined
+ if (title !== undefined) {
+ if (title === '') throw invalid('A task needs a title')
+ task.title = title
+ }
+ const priority = readPriority(body.priority)
+ if (priority !== undefined) task.priority = priority
+ const progress = readProgress(body.progress)
+ if (progress !== undefined) task.progress = progress
+ if (body.note !== undefined) {
+ const note = readNote(body.note)
+ if (note) task.note = note
+ else delete task.note
+ }
+ return { priority, progress }
+ }
+
+ const roomByName = (botId, name) => {
+ const rooms = Object.values(state.rooms ?? {}).filter(room => Array.isArray(room.members) && room.members.includes(botId) && room.name === name)
+ if (rooms.length !== 1) throw invalid(`No single group named "${name.slice(0, 80)}" that you are in`)
+ return rooms[0].id
+ }
+ const resolveRoom = (botId, group, fallback) => {
+ if (group === undefined || group === null || String(group).trim() === '') return fallback || undefined
+ return roomByName(botId, String(group).trim())
+ }
+ const matchTitle = (botId, title, roomId) => {
+ const found = Object.values(store()).filter(task => task.ownerBotId === botId && task.title === title && !TERMINAL_MATCH.has(task.status) && (task.origin?.roomId ?? undefined) === roomId)
+ if (found.length > 1) throw invalid(`Two open tasks are named "${title}". Pass an id.`)
+ return found[0]
+ }
+ const listed = (botId) => Object.values(store()).filter(task => task.ownerBotId === botId).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 40).map(view)
+
+ // The Bot's own list, written from the conversation. The caller is the Bot;
+ // the requester stays the Bot's owner, so the owner can still move the card.
+ rt.trackBotTasks = async function trackBotTasks(botId, body, origin = {}) {
+ const bot = botRecord(botId)
+ if (!bot || bot.remote !== undefined) throw forbidden('No such Bot')
+ const items = body?.tasks
+ if (items === undefined) return listed(botId)
+ if (!Array.isArray(items)) throw invalid('tasks must be a list')
+ if (items.length > 20) throw invalid('Track at most 20 tasks at once')
+ const actor = bot.ownerId ? { memberId: bot.ownerId } : null
+ const saved = []
+ for (const item of items) {
+ if (item === null || typeof item !== 'object' || Array.isArray(item)) throw invalid('Each task must be an object')
+ const column = readColumn(item.column)
+ const title = item.title !== undefined ? readTitle(item.title) : undefined
+ const roomId = resolveRoom(botId, item.group, origin.roomId)
+ let task
+ let isNew = false
+ if (item.id !== undefined && item.id !== null && String(item.id) !== '') {
+ task = find(item.id)
+ if (!task || task.ownerBotId !== botId) throw forbidden('That task is not yours')
+ } else {
+ if (!title) throw invalid('A new task needs a title')
+ task = matchTitle(botId, title, roomId)
+ }
+ if (!task) {
+ isNew = true
+ const at = stamp()
+ task = {
+ id: `task-${randomBytes(8).toString('hex')}`,
+ status: 'open',
+ createdAt: at,
+ updatedAt: at,
+ requesterId: bot.ownerId ?? null,
+ ownerBotId: botId,
+ title,
+ source: 'conversation',
+ origin: { ...(roomId ? { roomId } : {}), ...(origin.sessionId ? { sessionId: String(origin.sessionId) } : {}) },
+ actors: [{ status: 'open', at, by: 'conversation', ...(actor?.memberId ? { memberId: actor.memberId } : {}) }],
+ }
+ store()[task.id] = task
+ } else if (title) task.title = title
+ const { progress } = applyFields(task, item)
+ if (!task.source) task.source = 'conversation'
+ const before = task.updatedAt
+ const moved = applyColumn(task, column, null, 'conversation')
+ if (column === 'done' && progress === undefined) task.progress = 100
+ if (!isNew && !moved && task.updatedAt === before) record(task, null, { by: 'conversation' })
+ saved.push(task)
+ }
+ prune()
+ await save()
+ return saved.map(view)
+ }
+
// ---------------------------------------------------------------- host hooks
// Name the working task a send belongs to so a hold can block that one (#111).
// Prefer an already-stamped `meta.taskId`; else the Bot's only working task;
@@ -189,11 +367,30 @@ export function install(rt) {
// ---------------------------------------------------------------- channel API
rt.taskEndpoint = async function taskEndpoint(endpoint, body, actor) {
switch (endpoint) {
- case 'tasks':
- return Object.values(store())
- .filter(task => mayAct(task, actor))
- .sort((a, b) => a.createdAt - b.createdAt)
- .map(view)
+ case 'tasks': {
+ const botId = body.botId ? String(body.botId) : ''
+ const roomId = body.roomId ? String(body.roomId) : ''
+ const all = Object.values(store())
+ let rows
+ if (roomId) {
+ const room = state.rooms?.[roomId]
+ if (room === undefined) throw forbidden('No such group')
+ if (rt.authzEnforced?.() && !roomAdmin(actor, room)) throw forbidden('Only a group admin can see that group\'s tasks')
+ const members = new Set(currentParticipants(room).filter(p => p.kind === 'bot' && !p.remote).map(p => p.id))
+ rows = all.filter(task => task.origin?.roomId === roomId && members.has(task.ownerBotId))
+ } else if (botId) {
+ const bot = botRecord(botId)
+ if (bot === undefined || bot.remote !== undefined) throw forbidden('No such Bot')
+ const seesBot = ancestorOwned(actor, botId)
+ rows = all.filter(task => task.ownerBotId === botId && (seesBot || mayAct(task, actor)))
+ if (rt.authzEnforced?.() && !seesBot && rows.length === 0) throw forbidden('You cannot see that Bot\'s tasks')
+ } else {
+ // Ownership and the tree. Group tasks of someone else's Bot stay behind
+ // the room query, so a plain list never becomes a group-admin view.
+ rows = all.filter(task => mayAct(task, actor) || ancestorOwned(actor, task.ownerBotId))
+ }
+ return rows.sort((a, b) => a.createdAt - b.createdAt).map(view)
+ }
case 'task-create': {
const botId = String(body.botId ?? '')
const bot = state.bots?.[botId]
@@ -209,16 +406,33 @@ export function install(rt) {
requesterId,
ownerBotId: botId,
// workBotId is the specialist on the exchange; assigned with #55 wiring (#111).
- title: String(body.title ?? '').slice(0, 300),
+ title: readTitle(body.title),
...(typeof body.criteria === 'string' && body.criteria.trim() !== '' ? { criteria: body.criteria.slice(0, 2000) } : {}),
origin: { ...(roomId ? { roomId } : {}), ...(body.sessionId ? { sessionId: String(body.sessionId) } : {}) },
actors: [{ status: 'open', at: stamp(), ...(actor?.memberId ? { memberId: actor.memberId } : {}) }],
}
+ const { progress } = applyFields(task, body)
+ const column = readColumn(body.column)
+ applyColumn(task, column, actor)
+ if (column === 'done' && progress === undefined) task.progress = 100
store()[task.id] = task
prune()
await save()
return view(task)
}
+ case 'task-update': {
+ const task = find(body.id)
+ if (task === undefined) throw stale(`no task ${String(body.id ?? '')}`)
+ if (!mayUpdate(task, actor)) throw forbidden('Only the requester or the Bot\'s owner may update this task')
+ const { progress } = applyFields(task, body)
+ const column = readColumn(body.column)
+ const before = task.updatedAt
+ const moved = applyColumn(task, column, actor)
+ if (column === 'done' && progress === undefined) task.progress = 100
+ if (!moved && task.updatedAt === before) record(task, actor, { by: 'updated' })
+ await save()
+ return view(task)
+ }
case 'task-accept': {
const task = find(body.id)
if (task === undefined) throw stale(`no task ${String(body.id ?? '')}`)
diff --git a/core/src/host/tools.js b/core/src/host/tools.js
index d731c3f4a..a014ebd50 100644
--- a/core/src/host/tools.js
+++ b/core/src/host/tools.js
@@ -1,6 +1,7 @@
// Team tools: the roster, bot-to-bot messages, group chat settings and posts, and the
// question card. Group rounds and reading chats are in groups.js.
import { randomUUID } from 'node:crypto'
+import { columnOf } from '../shared/task-board.js'
import { defineTool } from './tool-schema.js'
import { COLORS, EXTRA_COLORS, MAX_HOPS, MAX_UNREAD_REQUESTS, ROOM_MODES, SOURCE_KIND } from './constants.js'
import { unquote } from './text.js'
@@ -759,5 +760,43 @@ export function install(rt) {
}))
}
+ ctx.tools.register(defineTool({
+ name: 'track_tasks',
+ description: 'Read or update your own task list. Call it when the user gives you work, or when a task starts, moves, or finishes, so the board stays current. Omit tasks to read the list. Pass tasks to create or update: match an id you already have, or the title of a task you are still tracking. Each task has a title, a column (not-started, in-progress, done), a progress percent from 0 to 100, a priority (low, normal, high, urgent) and a short note. In a group the task is tracked for that group.',
+ parameters: {
+ tasks: {
+ type: 'array',
+ description: 'Tasks to create or update. Omit to read your list.',
+ items: {
+ type: 'object',
+ additionalProperties: false,
+ properties: {
+ id: { type: 'string', description: 'Existing task id. Omit to match an open task by title, or to create one.' },
+ title: { type: 'string', description: 'Short name of the work. Required when creating.' },
+ column: { type: 'string', enum: ['not-started', 'in-progress', 'done'], description: 'Board column.' },
+ progress: { type: 'integer', description: 'How far along, from 0 to 100.' },
+ priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'], description: 'How urgent the task is.' },
+ note: { type: 'string', description: 'One line on where the task stands.' },
+ group: { type: 'string', description: 'Group chat name this task belongs to. In a group, the current group is used when this is omitted.' },
+ },
+ },
+ },
+ },
+ output,
+ async execute(args, exec) {
+ await rt.load()
+ const self = rt.selfOf(exec.agent?.id)
+ if (!rt.botOf(self)) return 'Only a Bot on the team can track tasks.'
+ const roomId = rt.groupOwners.get(exec.agent?.id)?.roomId
+ try {
+ const tasks = await rt.trackBotTasks(self, args ?? {}, { sessionId: exec.agent?.id, roomId })
+ if (!Array.isArray(tasks) || tasks.length === 0) return 'No tasks.'
+ return tasks.map(task => `- ${task.id} [${columnOf(task)} ${task.progress ?? 0}% ${task.priority ?? 'normal'}] ${task.title}`).join('\n')
+ } catch (error) {
+ return error?.message ?? String(error)
+ }
+ },
+ }))
+
Object.assign(rt, { pickRoom, hideBlockingQuestion })
}
diff --git a/core/src/shared/task-board.js b/core/src/shared/task-board.js
new file mode 100644
index 000000000..b9f1d4a1c
--- /dev/null
+++ b/core/src/shared/task-board.js
@@ -0,0 +1,31 @@
+// Board columns for the task records in src/host/tasks.js (#1058).
+// The state machine stays the record. A column is how the board reads it.
+// result-ready still waits for acceptance, so it stays In progress.
+
+export const COLUMNS = ['not-started', 'in-progress', 'done']
+export const PRIORITIES = ['urgent', 'high', 'normal', 'low']
+
+const COLUMN_OF = {
+ open: 'not-started',
+ claimed: 'not-started',
+ working: 'in-progress',
+ blocked: 'in-progress',
+ 'result-ready': 'in-progress',
+ accepted: 'done',
+ cancelled: 'done',
+ failed: 'done',
+}
+
+export function columnOf(task) {
+ return COLUMN_OF[task?.status] ?? 'not-started'
+}
+
+export function priorityOf(task) {
+ return PRIORITIES.includes(task?.priority) ? task.priority : 'normal'
+}
+
+export function progressOf(task) {
+ const value = task?.progress
+ if (typeof value !== 'number' || !Number.isFinite(value)) return columnOf(task) === 'done' && task?.status !== 'cancelled' && task?.status !== 'failed' ? 100 : 0
+ return Math.max(0, Math.min(100, Math.round(value)))
+}
diff --git a/core/test/authz.test.js b/core/test/authz.test.js
index db81a421b..4b7c1e525 100644
--- a/core/test/authz.test.js
+++ b/core/test/authz.test.js
@@ -273,7 +273,7 @@ test('task endpoints are an explicit allow-list, not a task-* prefix', async (t)
await rt.load()
const sessionA = await redeemOwner(rt)
const actorA = await rt.authenticate(sessionA.sessionToken)
- for (const ep of ['tasks', 'task-create', 'task-accept', 'task-start', 'task-return', 'task-accept-result', 'task-cancel']) {
+ for (const ep of ['tasks', 'task-create', 'task-update', 'task-accept', 'task-start', 'task-return', 'task-accept-result', 'task-cancel']) {
assert.equal(rt.authorize(actorA, ep, {}).ok, true, ep)
}
const unknown = rt.authorize(actorA, 'task-explode', {})
diff --git a/core/test/bot-tasks.test.js b/core/test/bot-tasks.test.js
new file mode 100644
index 000000000..6d845f15e
--- /dev/null
+++ b/core/test/bot-tasks.test.js
@@ -0,0 +1,245 @@
+// Per-Bot task board (#1058): the existing task records gain a board column,
+// progress and priority. A Bot records work from its own conversation with
+// track_tasks. The owner sees that Bot's list; a tree parent can read a
+// descendant's list; a group admin can read tasks tagged with the group.
+// A non-admin cannot read another Bot's group tasks, and a read is not a write.
+import assert from 'node:assert/strict'
+import { existsSync, mkdtempSync, readFileSync, 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 { columnOf } from '../src/shared/task-board.js'
+import { fakeContext } from './fake-ctx.js'
+
+const team = () => ({
+ version: 2,
+ revision: 1,
+ mainBotIds: ['a'],
+ members: {
+ 'owner-1': { id: 'owner-1', createdAt: 1, designatedChiefId: 'a' },
+ },
+ bots: {
+ a: { id: 'a', name: 'ChiefA', role: 'Boss', sessionId: 'a', createdAt: 1, ownerId: 'owner-1' },
+ s: { id: 's', name: 'Writer', role: 'Write', sessionId: 's', createdAt: 3, ownerId: 'owner-1', createdBy: 'a' },
+ },
+ rooms: {
+ war: {
+ id: 'war', name: 'War', members: ['a', 's'], admin: 'a', mode: 'everyone', named: true, sessions: { s: 's-war' }, log: [], seq: 0, seen: {},
+ participants: [
+ { kind: 'human', id: 'owner-1', role: 'member', displayName: 'Alice', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 'a', role: 'admin', displayName: 'ChiefA', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 's', role: 'member', displayName: 'Writer', joinedSeq: 0, joinedAt: 1 },
+ ],
+ },
+ },
+ grants: {},
+ agents: {}, exchanges: [], exchangeTurns: {}, questions: {}, prefs: {},
+})
+
+async function start(t, config = {}) {
+ const home = config.home ?? mkdtempSync(join(tmpdir(), 'ds-bot-board-'))
+ if (config.state !== undefined || !existsSync(join(home, 'state.json'))) writeFileSync(join(home, 'state.json'), JSON.stringify(config.state ?? team()))
+ const ctx = fakeContext()
+ const agents = new Map()
+ const agentOf = (sessionId) => {
+ if (!agents.has(sessionId)) {
+ agents.set(sessionId, {
+ session: { id: sessionId }, id: sessionId, calls: [], status: 'idle',
+ followup() {}, steer() {}, inject() {}, cancel() {}, async whenIdle() {},
+ inbox: { nextStep: [], nextTurn: [], remove() {} },
+ })
+ }
+ 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: `s${++created}` }), resolveAgent: async id => ({ agent: agentOf(id) }), selectModel: async () => {}, page: async () => ({ records: [], hasMore: false }) },
+ sessionTitle: { rename: async () => {} },
+ sessions: { flush: async () => {} },
+ })
+ let route
+ let created = 0
+ const register = ctx.connection.fetch.register
+ ctx.connection.fetch.register = (options) => {
+ if (options.path.endsWith('bot')) route = options
+ return register(options)
+ }
+ apply(ctx, { home })
+ const call = async (endpoint, payload, token) => {
+ const headers = token ? { 'x-flybot-member-session': token } : undefined
+ return (await route.fetch(new Request('http://localhost/api/bot', { method: 'POST', headers, body: JSON.stringify({ endpoint, payload }) }))).json()
+ }
+ const stop = async ({ keepHome = false } = {}) => {
+ await ctx.dispose()
+ if (!keepHome) rmSync(home, { recursive: true, force: true, maxRetries: 5 })
+ }
+ const signIn = async (displayName, inviterToken) => {
+ const invite = await call('auth-invite', {}, inviterToken)
+ assert.equal(invite.ok, true, JSON.stringify(invite))
+ const redeemed = await call('auth-redeem', { token: invite.value.token, displayName })
+ assert.equal(redeemed.ok, true, JSON.stringify(redeemed))
+ return redeemed.value
+ }
+ const track = (sessionId, args) => ctx.registeredTools.get('track_tasks').execute(args ?? {}, { agent: { id: sessionId } })
+ return { ctx, call, stop, signIn, track, home, statePath: join(home, 'state.json') }
+}
+
+const list = async (call, token, payload) => {
+ const result = await call('tasks', payload ?? {}, token)
+ assert.equal(result.ok, true, JSON.stringify(result))
+ return result.value
+}
+
+test('a Bot records conversation tasks and the owner moves them across the board', async (t) => {
+ const first = await start(t)
+ let app = first
+ t.after(() => app.stop())
+ const alice = await first.signIn('Alice')
+ const created = await first.track('s', { tasks: [{ title: 'Draft the note', column: 'not-started', priority: 'high', progress: 0 }] })
+ assert.match(created, /task-/)
+ const open = (await list(first.call, alice.sessionToken, { botId: 's' })).find(task => task.title === 'Draft the note')
+ assert.ok(open, 'the owner sees the Bot\'s task')
+ assert.equal(open.source, 'conversation')
+ assert.equal(open.status, 'open')
+ assert.equal(columnOf(open), 'not-started')
+ assert.equal(open.priority, 'high')
+ assert.equal(open.progress, 0)
+ assert.equal(open.ownerBotId, 's')
+ assert.equal(open.requesterId, alice.memberId)
+
+ const again = await first.track('s', { tasks: [{ title: 'Draft the note', column: 'in-progress', progress: 40, note: 'Outline done' }] })
+ assert.match(again, new RegExp(open.id))
+ const working = (await list(first.call, alice.sessionToken, { botId: 's' })).filter(task => task.title === 'Draft the note')
+ assert.equal(working.length, 1, 'the same title updates the open task')
+ assert.equal(working[0].id, open.id)
+ assert.equal(working[0].status, 'working')
+ assert.equal(columnOf(working[0]), 'in-progress')
+ assert.equal(working[0].progress, 40)
+ assert.equal(working[0].note, 'Outline done')
+
+ const started = await first.call('task-update', { id: open.id, column: 'not-started' }, alice.sessionToken)
+ assert.equal(started.ok, false)
+ assert.equal(started.error.code, 'bot/stale')
+ assert.equal((await list(first.call, alice.sessionToken, { botId: 's' })).find(task => task.id === open.id).status, 'working')
+
+ const done = await first.call('task-update', { id: open.id, column: 'done' }, alice.sessionToken)
+ assert.equal(done.ok, true, JSON.stringify(done))
+ assert.equal(done.value.status, 'accepted')
+ assert.equal(columnOf(done.value), 'done')
+ assert.equal(done.value.progress, 100)
+ assert.equal(done.value.result.summary, 'Outline done')
+
+ const late = await first.call('task-update', { id: open.id, column: 'in-progress' }, alice.sessionToken)
+ assert.equal(late.ok, false)
+ assert.equal(late.error.code, 'bot/stale')
+
+ await first.stop({ keepHome: true })
+ const restored = await start(t, { home: first.home })
+ app = restored
+ const saved = (await list(restored.call, alice.sessionToken, { botId: 's' })).find(task => task.id === open.id)
+ assert.equal(saved.status, 'accepted')
+ assert.equal(saved.progress, 100)
+ assert.equal(saved.priority, 'high')
+})
+
+test('board moves walk the task states and a blocked task stays put', async (t) => {
+ const app = await start(t)
+ t.after(() => app.stop())
+ const alice = await app.signIn('Alice')
+ const created = await app.call('task-create', { botId: 's', title: 'Check the figures', priority: 'low', progress: 0 }, alice.sessionToken)
+ assert.equal(created.ok, true, JSON.stringify(created))
+ const id = created.value.id
+ assert.equal(created.value.priority, 'low')
+ assert.equal(created.value.progress, 0)
+ const started = await app.call('task-update', { id, column: 'in-progress', progress: 25 }, alice.sessionToken)
+ assert.equal(started.ok, true, JSON.stringify(started))
+ assert.equal(started.value.status, 'working')
+ assert.equal(started.value.progress, 25)
+ assert.ok(started.value.actors.some(entry => entry.status === 'claimed'))
+ const waiting = await app.call('task-wait-input', { id, question: 'Which quarter?' }, alice.sessionToken)
+ assert.equal(waiting.ok, true, JSON.stringify(waiting))
+ const refused = await app.call('task-update', { id, column: 'done' }, alice.sessionToken)
+ assert.equal(refused.ok, false)
+ assert.equal(refused.error.code, 'bot/stale')
+ assert.equal((await list(app.call, alice.sessionToken)).find(task => task.id === id).status, 'blocked')
+ const bad = await app.call('task-update', { id, priority: 'top' }, alice.sessionToken)
+ assert.equal(bad.ok, false)
+ assert.equal(bad.error.code, 'bot/invalid')
+})
+
+test('a tree parent can read a child, and only a group admin can read other Bots\' group tasks', async (t) => {
+ const first = await start(t)
+ let app = first
+ t.after(() => app.stop())
+ const alice = await first.signIn('Alice')
+ assert.equal(alice.memberId, 'owner-1')
+ const bob = await first.signIn('Bob', alice.sessionToken)
+ const carol = await first.signIn('Carol', alice.sessionToken)
+ await first.stop({ keepHome: true })
+ const saved = JSON.parse(readFileSync(first.statePath, 'utf8'))
+ saved.bots.b = { id: 'b', name: 'BobBot', role: 'Help', sessionId: 'b', createdAt: 4, ownerId: bob.memberId, createdBy: null }
+ saved.bots.c = { id: 'c', name: 'Child', role: 'Help', sessionId: 'c', createdAt: 5, ownerId: bob.memberId, createdBy: 'a' }
+ const room = saved.rooms.war
+ room.members = ['a', 's', 'b', 'c']
+ room.admin = 'a'
+ room.sessions = { s: 's-war', b: 'b-war', c: 'c-own' }
+ room.participants = [
+ { kind: 'human', id: alice.memberId, role: 'member', displayName: 'Alice', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'human', id: bob.memberId, role: 'member', displayName: 'Bob', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'human', id: carol.memberId, role: 'admin', displayName: 'Carol', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 'a', role: 'admin', displayName: 'ChiefA', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 's', role: 'member', displayName: 'Writer', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 'b', role: 'member', displayName: 'BobBot', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 'c', role: 'member', displayName: 'Child', joinedSeq: 0, joinedAt: 1 },
+ ]
+ writeFileSync(first.statePath, JSON.stringify(saved))
+ const next = await start(t, { home: first.home })
+ app = next
+
+ const groupLine = await next.track('s-war', { tasks: [{ title: 'Answer in the group', column: 'in-progress', progress: 10, priority: 'urgent' }] })
+ assert.match(groupLine, /in-progress/)
+ const writer = (await list(next.call, alice.sessionToken, { botId: 's' })).find(task => task.title === 'Answer in the group')
+ assert.equal(writer.origin.roomId, 'war')
+ assert.equal(writer.origin.sessionId, 's-war')
+
+ await next.track('b', { tasks: [{ title: 'Private draft', column: 'not-started' }] })
+ await next.track('b-war', { tasks: [{ title: 'Bob\'s group item', column: 'not-started', priority: 'normal' }] })
+ await next.track('c', { tasks: [{ title: 'Child follow-up', column: 'in-progress', progress: 15 }] })
+
+ const child = await list(next.call, alice.sessionToken, { botId: 'c' })
+ assert.equal(child.length, 1)
+ assert.equal(child[0].title, 'Child follow-up')
+ assert.equal(child[0].ownerBotName, 'Child')
+ const carolChild = await next.call('tasks', { botId: 'c' }, carol.sessionToken)
+ assert.equal(carolChild.ok, false)
+ assert.equal(carolChild.error.code, 'bot/forbidden')
+ const aliceAll = await list(next.call, alice.sessionToken)
+ assert.ok(aliceAll.some(task => task.id === child[0].id), 'a parent\'s owner sees the descendant without a group query')
+ assert.equal(aliceAll.some(task => task.title === 'Private draft' || task.title === 'Bob\'s group item'), false)
+
+ const roomTasks = await list(next.call, alice.sessionToken, { roomId: 'war' })
+ assert.deepEqual(roomTasks.map(task => task.title).sort(), ['Answer in the group', 'Bob\'s group item'])
+ const carolRoom = await list(next.call, carol.sessionToken, { roomId: 'war' })
+ assert.deepEqual(carolRoom.map(task => task.title).sort(), ['Answer in the group', 'Bob\'s group item'])
+ const bobRoom = await next.call('tasks', { roomId: 'war' }, bob.sessionToken)
+ assert.equal(bobRoom.ok, false)
+ assert.equal(bobRoom.error.code, 'bot/forbidden')
+ const bobOwn = await list(next.call, bob.sessionToken, { botId: 'b' })
+ assert.deepEqual(bobOwn.map(task => task.title).sort(), ['Bob\'s group item', 'Private draft'])
+ const bobAll = await list(next.call, bob.sessionToken)
+ assert.equal(bobAll.some(task => task.ownerBotId === 's'), false, 'a non-admin does not see another Bot\'s group task')
+
+ const edited = await next.call('task-update', { id: roomTasks.find(task => task.title === 'Bob\'s group item').id, column: 'done' }, alice.sessionToken)
+ assert.equal(edited.ok, false)
+ assert.equal(edited.error.code, 'bot/forbidden')
+ const carolEdit = await next.call('task-update', { id: writer.id, progress: 90 }, carol.sessionToken)
+ assert.equal(carolEdit.ok, false)
+ assert.equal(carolEdit.error.code, 'bot/forbidden')
+ assert.equal((await list(next.call, alice.sessionToken, { botId: 's' })).find(task => task.id === writer.id).progress, 10)
+
+ const stolen = await next.track('s', { tasks: [{ id: bobOwn.find(task => task.title === 'Private draft').id, column: 'done' }] })
+ assert.match(stolen, /not yours/)
+ assert.equal((await list(next.call, bob.sessionToken, { botId: 'b' })).find(task => task.title === 'Private draft').status, 'open')
+})
diff --git a/core/test/install.test.js b/core/test/install.test.js
index 8b97b87e4..2d0704c69 100644
--- a/core/test/install.test.js
+++ b/core/test/install.test.js
@@ -60,6 +60,7 @@ test('apply registers listeners, tools, and effects in a fixed order', (t) => {
'tool delete_capability',
'tool attach_capability',
'tool detach_capability',
+ 'tool track_tasks',
'effect ds-bot: chat index',
'effect ds-bot: group chat section',
'prompt bot-room 10150',
diff --git a/core/test/kanban.test.js b/core/test/kanban.test.js
new file mode 100644
index 000000000..ca2f8fff1
--- /dev/null
+++ b/core/test/kanban.test.js
@@ -0,0 +1,134 @@
+// The task board (#1058): three columns, a Bot rail that nests children, and a
+// group rail limited to groups the viewer administers. Moving a card is the
+// owner's action; another Bot's card in a group view is visible and not movable.
+import assert from 'node:assert/strict'
+import { test } from 'node:test'
+import { fileURLToPath } from 'node:url'
+import { runInNewContext } from 'node:vm'
+import { build } from 'esbuild'
+
+const bundle = await build({
+ entryPoints: [fileURLToPath(new URL('../src/client/kanban.js', import.meta.url))],
+ bundle: true, format: 'cjs', platform: 'node', write: false,
+ external: ['react', 'react-dom'],
+})
+const module = { exports: {} }
+runInNewContext(bundle.outputFiles[0].text, {
+ module, exports: module.exports, setTimeout, clearTimeout, setInterval, clearInterval, Date, Promise,
+ require: (name) => {
+ const platform = {
+ react: {
+ createElement: (type, props, ...children) => ({ type, props: props ?? {}, children: children.flat(Infinity) }),
+ useState: initial => [typeof initial === 'function' ? initial() : initial, () => {}],
+ useEffect: () => {}, useCallback: fn => fn,
+ },
+ }
+ if (!(name in platform)) throw new Error(`no platform module ${name}`)
+ return platform[name]
+ },
+})
+const { BoardView, boardColumns, boardRail, canMoveTask, shortId } = module.exports
+
+const expand = (node) => {
+ if (Array.isArray(node)) return node.flatMap(expand)
+ if (node === null || typeof node !== 'object') return node
+ if (typeof node.type === 'function') return expand(node.type({ ...node.props, children: node.children }))
+ return { ...node, children: node.children.map(expand) }
+}
+const all = (node, out = []) => {
+ if (Array.isArray(node)) { for (const item of node) all(item, out); return out }
+ if (node && typeof node === 'object') { out.push(node); all(node.children, out) }
+ return out
+}
+const textOf = node => (typeof node === 'string' ? node : node && typeof node === 'object' ? node.children.map(textOf).join('') : '')
+const byClass = (tree, name) => all(tree).filter(node => typeof node.props?.className === 'string' && node.props.className.split(' ').includes(name))
+
+const roster = {
+ self: { memberId: 'm1' },
+ bots: [
+ { id: 'a', name: 'Chief', ownerId: 'm1', createdAt: 1 },
+ { id: 's', name: 'Writer', ownerId: 'm1', createdBy: 'a', createdAt: 2 },
+ { id: 'b', name: 'Other', ownerId: 'm2', createdAt: 3 },
+ ],
+ rooms: [
+ { id: 'war', name: 'War', admin: 'a', participants: [{ kind: 'human', id: 'm1', role: 'member' }] },
+ { id: 'ops', name: 'Ops', admin: 'b', participants: [{ kind: 'human', id: 'm1', role: 'member' }] },
+ { id: 'face', name: 'Face', origin: 'face-to-face', admin: 'a', participants: [{ kind: 'human', id: 'm1', role: 'admin' }] },
+ ],
+}
+roster.byId = Object.fromEntries(roster.bots.map(bot => [bot.id, bot]))
+roster.roomsById = Object.fromEntries(roster.rooms.map(room => [room.id, room]))
+
+const task = (id, status, extra) => ({ id, status, title: `Task ${id}`, ownerBotId: 's', priority: 'normal', progress: 0, updatedAt: 1, ...extra })
+const tasks = [
+ task('task-abcdef123456', 'open', { title: 'Draft the note', priority: 'high', progress: 0, updatedAt: 2 }),
+ task('task-ffffff999999', 'working', { title: 'Ship the note', priority: 'urgent', progress: 40, note: 'Halfway', updatedAt: 3 }),
+ task('task-000000aaaaaa', 'accepted', { title: 'Old note', ownerBotId: 'a', priority: 'low', progress: 100, updatedAt: 1 }),
+ task('task-bbbbbbcccccc', 'open', { title: 'Theirs', ownerBotId: 'b', ownerBotName: 'Other', updatedAt: 4 }),
+]
+
+test('columns follow the task state and a card carries id, priority and progress', () => {
+ const columns = boardColumns(tasks.filter(item => item.ownerBotId === 's' || item.ownerBotId === 'a'))
+ assert.equal(columns['not-started'].map(item => item.title).join('|'), 'Draft the note')
+ assert.equal(columns['in-progress'].map(item => item.title).join('|'), 'Ship the note')
+ assert.equal(columns.done.map(item => item.title).join('|'), 'Old note')
+ assert.equal(shortId('task-abcdef123456'), '#123456')
+ const rail = boardRail(roster)
+ // Names come back from the bundled module's realm; join avoids deepEqual's cross-realm miss.
+ assert.equal(rail.bots.map(row => `${row.name}:${row.depth}`).join('|'), 'Chief:0|Writer:1')
+ assert.equal(rail.groups.map(row => row.name).join('|'), 'War')
+ assert.equal(canMoveTask(roster, tasks[0]), true)
+ assert.equal(canMoveTask(roster, tasks[3]), false)
+})
+
+test('the board draws the three columns, nests the child, and moves only the owner\'s card', () => {
+ const moved = []
+ const tree = expand({
+ type: BoardView,
+ props: {
+ roster,
+ tasks: tasks.filter(item => item.ownerBotId === 's'),
+ rail: boardRail(roster),
+ scope: { kind: 'bot', botId: 's' },
+ counts: { s: { done: 0, total: 1 }, a: { done: 1, total: 1 } },
+ dragging: 'task-abcdef123456',
+ onMove: (id, column) => moved.push([id, column]),
+ onCreate: () => {},
+ onSelect: () => {},
+ },
+ children: [],
+ })
+ const columns = byClass(tree, 'bt-board-column').map(node => node.props['data-column'])
+ assert.deepEqual(columns, ['not-started', 'in-progress', 'done'])
+ assert.match(textOf(tree), /Draft the note/)
+ assert.match(textOf(tree), /#123456/)
+ assert.match(textOf(tree), /High/)
+ assert.match(textOf(tree), /40%/)
+ assert.match(textOf(tree), /Halfway/)
+ assert.match(textOf(tree), /Start task/)
+ assert.match(textOf(tree), /Complete task/)
+ const writer = all(tree).find(node => node.props?.['data-id'] === 's')
+ assert.equal(writer.props.style.paddingLeft, '24px')
+ assert.equal(all(tree).some(node => node.props?.['data-id'] === 'ops'), false)
+ const start = all(tree).find(node => node.type === 'button' && textOf(node) === 'Start')
+ start.props.onClick()
+ assert.deepEqual(moved, [['task-abcdef123456', 'in-progress']])
+ const progress = byClass(tree, 'bt-board-column').find(node => node.props['data-column'] === 'in-progress')
+ progress.props.onDrop({ preventDefault() {}, dataTransfer: { getData: () => 'task-abcdef123456' } })
+ assert.deepEqual(moved[1], ['task-abcdef123456', 'in-progress'])
+ const group = expand({
+ type: BoardView,
+ props: {
+ roster,
+ tasks: [tasks[3]],
+ rail: boardRail(roster),
+ scope: { kind: 'group', roomId: 'war' },
+ onMove: () => moved.push('group'),
+ },
+ children: [],
+ })
+ assert.match(textOf(group), /Theirs/)
+ assert.match(textOf(group), /Other/)
+ assert.equal(all(group).some(node => node.type === 'button' && (textOf(node) === 'Start' || textOf(node) === 'Complete')), false)
+ assert.equal(byClass(group, 'bt-board-card')[0].props.draggable, undefined)
+})
diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md
index aac47df96..1a5d3f8cf 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/kanban.js` | Per-Bot task board (#1058): 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. 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`. |
@@ -128,7 +129,13 @@ The overlay (`flybot-slots.js` `FlyBotApprovals`) is the fallback, not a second
- Each card has exactly one status, as a colored pill with an icon and a word (never color alone), the Bot, where and when, the title on one line with the full text on hover, and **Open** (closes the inbox, opens the chat). An approval the group chat's card can answer (`answerable`) also has **Reject** / **Allow once**, which call `actions.answerApproval`, the same answer as that card (no second approval path); the card leaves and a status line says what was done; an error stays on the card. No native confirm dialog. A held Bot message, an approval DSH's card answers in the Bot's own chat, and every task have **Open** only: Bot-sent messages are approved in their chat. Every action is a button, so the dialog is keyboard reachable; the close button takes focus and Escape closes.
- **Latest activity (#471):** every card may have one more line, *Latest: …* (`card.activity`, `bt-inbox-activity`, full text on hover): a task's newest note, else what its Bot runs there now (only for running turns and tasks), else the chat's last line (`previewOf`); left out when it only repeats the title or the reason.
- **Tool-call count (#471):** a card whose place used at least one tool shows one muted line, `N tool calls` (`card.calls`, `bt-inbox-calls`): the count of the turn running in that place now, else of the newest ended turn there, from the work snapshot (`work.bots[id].calls`, `work.rooms[roomId][].calls`, `work.ended[].calls`, [core-host](core-host.md#tool-call-count-per-turn-471)). One place only, never added across places; a count of 0 or unknown draws no line. Nothing new is polled and the card stays read-only. i18n key `{count} tool calls`.
-- Out of scope so far (#471): the three-column board with a List / Board toggle (v2), a first-run guide, team views (#51/#149/#55/#57).
+- Out of scope so far (#471): the three-column board with a List / Board toggle (v2), a first-run guide, team views (#51/#149/#55/#57). The task board (#1058) is a separate dialog and does not replace this list.
+
+### Bot task board (#1058)
+
+- A **Tasks** row sits under Inbox. It opens a dialog: a left rail of the viewer's own Bots (a child indented under the Bot that created it) and, below that, groups the viewer administers (human role admin, or they own the Bot in `room.admin`; face-to-face rooms stay out).
+- The board has three columns, **Not started**, **In progress** and **Done**, filled from `actions.tasks` (`src/host/tasks.js`, [core-host](core-host.md#bot-task-board-1058)). A card shows a short id, a priority chip, the title, an optional note, the Bot's name and a progress percent. **Start** and **Complete**, or a drop on a column, call `actions.taskUpdate`. A card the viewer does not own (another member's Bot in a group view) has neither the buttons nor a drag.
+- Choosing a group loads `tasks { roomId }` and lanes the cards by Bot. Choosing a Bot filters that Bot's own list, including a child. **New task** adds one on the selected Bot. The host decides who may read or move a task; the dialog only hides the actions.
### Phone-width layout (#362, step 1)
diff --git a/docs/subsystems/core-host.md b/docs/subsystems/core-host.md
index 1a5b9c5aa..077bc173b 100644
--- a/docs/subsystems/core-host.md
+++ b/docs/subsystems/core-host.md
@@ -171,6 +171,14 @@ These endpoints do not prompt, deliver a clarification, resume an agent or grant
execution/tool permissions. Automatic exchange wiring, runtime-scoped observation
credentials and team mode remain separate work under #55/#198.
+### Bot task board (#1058)
+
+Each Bot's work sits on the same `state.tasks` records. A column is read from the status (`open`/`claimed` not started, `working`/`blocked`/`result-ready` in progress, `accepted`/`cancelled`/`failed` done) by `src/shared/task-board.js`. `task-update` sets `priority` (`urgent`/`high`/`normal`/`low`), `progress` (0–100), `note` and a column; a column walks the existing states forward (`open` → `claimed` → `working` → `result-ready` → `accepted`) and refuses a reverse move or a blocked wait. The requester or the Bot's owner may update. Anyone else, including a group admin, may not.
+
+`track_tasks` is the conversation path. The calling Bot creates or updates only its own tasks (`source: 'conversation'`, `requesterId` the Bot's owner, `origin.sessionId` the turn, `origin.roomId` the group when the turn is in one). The same title in the same group updates the open task instead of adding another. The prompt tells the Bot to record work the user or a group actually handed it.
+
+`tasks` without a filter returns tasks the actor requested or whose Bot they own, including a descendant (`createdBy`) they do not themselves own. `tasks { botId }` is that Bot's list under the same rule. `tasks { roomId }` is the group view: a current human admin, or the owner of `room.admin`, sees tasks member Bots tagged with that group, and nobody else does. A plain list does not include another Bot's group tasks. Names come back as `ownerBotName` only. Tests: `core/test/bot-tasks.test.js`.
+
### Work view step (#546)
`work.bots[id]` (`turns.js` `workView`) carries `step`: what the newest running call acts on, from its arguments (`command`, `cmd`, `pattern`, `query`, `url`, `file_path`, `path`, else `description`), whitespace collapsed and capped at 80 characters. It is in the owner-only Bot view (`authz.js` `ownedLive` picks the actor's own Bots), not in `work.rooms`, which other members of a shared group read. It goes with the call's result and the turn's end. Covered by `core/test/group-status.test.js`.
diff --git a/docs/subsystems/team.md b/docs/subsystems/team.md
index 048a567db..ae00d7588 100644
--- a/docs/subsystems/team.md
+++ b/docs/subsystems/team.md
@@ -115,7 +115,7 @@ Private context crosses owners only through an explicit, owner-granted share; ro
## Task lifecycle foundation (landed, #54)
-`state.tasks` (`core/src/host/tasks.js`): a request becomes a tracked task — `open → claimed → working → blocked ⇄ working → result-ready → accepted`, `cancelled`/`failed` terminal, `accepted` only from `result-ready`. Only the requester's session moves their task (`bot/forbidden` otherwise). Create does not require owning the Bot: a matching per-Bot grant, or both the requester and the Bot as current members of the named room, is enough — a granted child can take a handoff within its authority. Outgoing holds stay the owner's: a held send (#52) blocks **that** task (`meta.taskId` stamped on the send from the working task — origin session/room when the Bot has several, else the latest — or an explicit id) until that owner decides. `result-ready` is protected from prune while it waits for acceptance. Task channel names are an explicit allow-list (`tasks` / `task-create` / `task-accept` / `task-start` / `task-return` / `task-accept-result` / `task-cancel`). Records carry per-status actor entries, pointers to the originating conversation, and a summary — never private transcripts. The in-room task view and audit trail are #57's; clarification (ask the requester) and `workBotId` (the specialist on the exchange) wait on #55's exchange wiring. Team mode stays disabled.
+`state.tasks` (`core/src/host/tasks.js`): a request becomes a tracked task — `open → claimed → working → blocked ⇄ working → result-ready → accepted`, `cancelled`/`failed` terminal, `accepted` only from `result-ready`. Only the requester's session moves their task (`bot/forbidden` otherwise). Create does not require owning the Bot: a matching per-Bot grant, or both the requester and the Bot as current members of the named room, is enough — a granted child can take a handoff within its authority. Outgoing holds stay the owner's: a held send (#52) blocks **that** task (`meta.taskId` stamped on the send from the working task — origin session/room when the Bot has several, else the latest — or an explicit id) until that owner decides. `result-ready` is protected from prune while it waits for acceptance. Task channel names are an explicit allow-list (`tasks` / `task-create` / `task-update` / `task-accept` / `task-start` / `task-return` / `task-accept-result` / `task-cancel`). The board (#1058) lets the requester or the Bot's owner move a card along those states, a tree parent read a descendant's tasks, and a group admin read tasks tagged with that group; it does not add a second record. Records carry per-status actor entries, pointers to the originating conversation, and a summary — never private transcripts. The in-room task view and audit trail are #57's; clarification (ask the requester) and `workBotId` (the specialist on the exchange) wait on #55's exchange wiring. Team mode stays disabled.
### Requester-controlled input-wait bookkeeping (#207)
diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch
index 88be1d87d..83fdccbbb 100644
--- a/patches/ds-bot/0001-core-fixes.patch
+++ b/patches/ds-bot/0001-core-fixes.patch
@@ -825,7 +825,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,37 @@ const ZH = {
+@@ -360,9 +360,60 @@ const ZH = {
'DSH update': 'DSH 更新',
'DSH {version}': 'DSH {version}',
'Restart DSH to start using DSH {version}.': '重启 DSH 后开始使用 DSH {version}。',
@@ -856,6 +856,29 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
+ 'Latest: {text}': '最新:{text}',
+ '{count} tool calls': '{count} 次工具调用',
+ 'Untitled task': '未命名任务',
++ // Bot task board (#1058)
++ Tasks: '任务',
++ 'Close tasks': '关闭任务',
++ 'Task board': '任务板',
++ 'Not started': '未开始',
++ 'Groups you admin': '你管理的群',
++ 'No tasks yet.': '还没有任务。',
++ 'Nothing in this column.': '这一列是空的。',
++ Highest: '最高',
++ High: '较高',
++ Low: '较低',
++ Claimed: '已认领',
++ Blocked: '受阻',
++ Start: '开始',
++ Complete: '完成',
++ 'Start task': '开始任务',
++ 'Complete task': '完成任务',
++ 'Add task': '添加任务',
++ 'Task title': '任务标题',
++ Progress: '进度',
++ Group: '群',
++ '{percent}%': '{percent}%',
++ '{done}/{total}': '{done}/{total}',
+ 'for {time}': '已 {time}',
+ 'just started': '刚开始',
+ 'Open {name}': '打开 {name}',
@@ -3381,7 +3404,7 @@ diff --git a/src/host/models.js b/src/host/models.js
diff --git a/src/host/prompt.js b/src/host/prompt.js
--- a/src/host/prompt.js
+++ b/src/host/prompt.js
-@@ -4,30 +4,35 @@
+@@ -4,30 +4,36 @@
const TEAM_RULES = [
'## How your Bot team works',
'- Every Bot is its own long-lived conversation with the user, with its own memory and computer.',
@@ -3404,6 +3427,7 @@ diff --git a/src/host/prompt.js b/src/host/prompt.js
'- web_search results say which search backend answered. If web_search fails, never guess URLs or present pages you remember as search results: use a browser tool if you have one, otherwise tell the user that search is unavailable and the reason it gave.',
'- Your conversations are long-lived. When one grows long, either its older messages are condensed in place into a summary you wrote (a "[Checkpoint N]" message), or it moves on to a fresh part that opens with a handoff note you wrote and your latest messages (a "[Handoff · part N]" message). What leaves your working context stays saved. When you need the exact earlier wording or a detail the summary or note does not show in full (a full path, a figure, every item of a complete list or a final summary), call read_own_chat with words to look for, or with part N to read the note that closed an earlier part; never fill such a detail in from memory. Each line it returns starts with an anchor such as [part 2 #1234 · 10-08 14:22]; pass the anchor as around to read what was said around it.',
'- Reply in the user\'s language.',
++ '- You keep your own task list with track_tasks. When the user gives you work, or a group hands you work, record it there (a short title, column not-started / in-progress / done, how far along from 0 to 100, and a priority) and update that same task as it moves. Do not record work nobody asked for. Call track_tasks with no tasks to read the list.',
].join('\n')
const MAIN_RULES = [
@@ -7096,9 +7120,11 @@ diff --git a/test/images.test.js b/test/images.test.js
diff --git a/test/install.test.js b/test/install.test.js
--- a/test/install.test.js
+++ b/test/install.test.js
-@@ -61,6 +61,8 @@ test('apply registers listeners, tools, and effects in a fixed order', (t) => {
+@@ -60,7 +60,10 @@ test('apply registers listeners, tools, and effects in a fixed order', (t) => {
+ 'tool delete_capability',
'tool attach_capability',
'tool detach_capability',
++ 'tool track_tasks',
'effect ds-bot: chat index',
+ 'effect ds-bot: group chat section',
+ 'prompt bot-room 10150',
diff --git a/patches/ds-bot/0002-flybot-api-host.patch b/patches/ds-bot/0002-flybot-api-host.patch
index 82762b0f0..19d4cfba6 100644
--- a/patches/ds-bot/0002-flybot-api-host.patch
+++ b/patches/ds-bot/0002-flybot-api-host.patch
@@ -76,12 +76,13 @@ diff --git a/index.js b/index.js
diff --git a/src/host/constants.js b/src/host/constants.js
--- a/src/host/constants.js
+++ b/src/host/constants.js
-@@ -49,14 +49,13 @@ export const AGENT_SESSION_TITLE_MAX = 80
+@@ -49,14 +49,13 @@
// Desktop profile, so a restriction filters this list down to registered names.
export const TEAM_TOOLS = [
'list_bots', 'message_bot', 'stop_bot', 'create_bot', 'update_bot', 'list_models',
+- 'create_group', 'update_group', 'delete_group', 'post_to_group', 'ask_user',
+ 'bot_schedules',
- 'create_group', 'update_group', 'delete_group', 'post_to_group', 'ask_user',
++ 'create_group', 'update_group', 'delete_group', 'post_to_group', 'ask_user', 'track_tasks',
'request_secret', 'list_secrets', 'group_relay', 'read_own_chat',
'read_group_chat', 'remember', 'forget', 'recall', 'read_browser',
'list_capabilities', 'save_capability', 'delete_capability',
@@ -92,6 +93,7 @@ diff --git a/src/host/constants.js b/src/host/constants.js
// Answers a group gives to what members post after the round moved on (their finished
// work, mostly), before the user speaks there again.
export const ROOM_LATE_ROUNDS = 10
+
diff --git a/src/host/home-lock.js b/src/host/home-lock.js
--- a/src/host/home-lock.js
+++ b/src/host/home-lock.js
@@ -918,12 +920,13 @@ diff --git a/src/host/store.js b/src/host/store.js
diff --git a/src/host/tools.js b/src/host/tools.js
--- a/src/host/tools.js
+++ b/src/host/tools.js
-@@ -1,9 +1,14 @@
+@@ -1,9 +1,15 @@
// Team tools: the roster, bot-to-bot messages, group chat settings and posts, and the
// question card. Group rounds and reading chats are in groups.js.
import { randomUUID } from 'node:crypto'
-import { defineTool } from '@deepseek-ai/dsh-tools'
-import { COLORS, EXTRA_COLORS, MAX_HOPS, MAX_UNREAD_REQUESTS, ROOM_BOT_ROUNDS, ROOM_MODES, SOURCE_KIND } from './constants.js'
++import { columnOf } from '../shared/task-board.js'
+import { defineTool } from './tool-schema.js'
+import { COLORS, EXTRA_COLORS, MAX_HOPS, MAX_UNREAD_REQUESTS, ROOM_MODES, SOURCE_KIND } from './constants.js'
import { unquote } from './text.js'
@@ -935,7 +938,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
export const output = { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] }
export const GROUP_REF = { type: 'string', required: true, description: 'Exact group chat name from list_bots' }
-@@ -27,39 +27,110 @@ export function install(rt) {
+@@ -27,39 +33,110 @@
const roomLine = (room, self) => {
const members = room.members.map(rt.botOf).filter(Boolean)
.map(bot => `${bot.name}${bot.id === room.admin ? ' (admin)' : ''}${bot.id === self ? ' (you)' : ''}`)
@@ -1053,7 +1056,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
].join('\n')
},
}))
-@@ -86,7 +86,7 @@ export function install(rt) {
+@@ -86,7 +163,7 @@
name: 'message_bot',
description: 'Send a message to another Bot on the team. If that Bot is working, it reads your message at its next step and can change course, then answers once that work is done; if it is idle, it starts on it right away. Its reply arrives later as a new message to you; with reply_to "user" it answers the user in its own chat instead.',
parameters: {
@@ -1062,7 +1065,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
message: { type: 'string', required: true, description: 'Self-contained message; the other Bot cannot see this conversation' },
reply_to: {
type: 'string',
-@@ -105,7 +105,17 @@ export function install(rt) {
+@@ -105,7 +182,17 @@
const sender = callerBot(exec)
if (sender === undefined) return 'Only Bots on the team can use message_bot.'
const target = rt.findBot(args.to)
@@ -1081,7 +1084,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
if (target.id === sender.id) return 'You cannot message yourself.'
const turn = rt.live.get(exec.agent?.id)
const hop = Math.max(turn?.info?.hop ?? 0, ...(turn?.joined ?? []).map(joined => joined.info?.hop ?? 0))
-@@ -118,7 +118,11 @@ export function install(rt) {
+@@ -118,7 +205,11 @@
const toUser = args.reply_to === 'user'
const working = rt.live.get(chat)?.turn !== undefined
const steer = args.when !== 'queue' && !toUser
@@ -1094,7 +1097,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
const sent = toUser
? `Delivered to ${target.name}. It will answer the user in its own chat; nothing comes back to you, so point the user to ${target.name}'s chat instead of relaying.`
: working && steer
-@@ -153,7 +153,7 @@ export function install(rt) {
+@@ -153,7 +244,7 @@
const sender = callerBot(exec)
if (sender === undefined || !rt.isMain(sender.id)) refuse('Only a Main Bot can stop other Bots.')
const target = rt.findBot(args.bot)
@@ -1103,7 +1106,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
if (target.id === sender.id) refuse('You cannot stop yourself.')
const { running } = activeSessions(target.id)
if (running.length === 0) refuse(`${target.name} is not running right now, so there is nothing to stop. Use message_bot to reach it.`)
-@@ -162,20 +162,20 @@ export function install(rt) {
+@@ -162,20 +253,20 @@
agent.cancel({ kind: 'hook', reason: 'ds-bot/stopped' })
}
const why = String(args.reason ?? '').trim() || 'Stop what you are doing; the previous turn was ended on purpose. Acknowledge briefly.'
@@ -1127,7 +1130,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
work_dir: WORK_DIR_PARAM,
},
output,
-@@ -187,7 +187,7 @@ export function install(rt) {
+@@ -187,7 +278,7 @@
const name = String(args.name ?? '').trim()
await rt.approveFolder(exec, { path: workDir, who: { en: `the new Bot ${name}`, zh: `新 Bot ${name} ` } })
const bot = await rt.createBot({ ...args, workDir, createdBy: rt.selfOf(exec.agent.id) })
@@ -1136,7 +1139,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
} catch (error) {
return `Could not create the Bot: ${error.message}`
}
-@@ -257,7 +257,7 @@ export function install(rt) {
+@@ -257,7 +348,7 @@
}
return { bots, unknown }
}
@@ -1145,7 +1148,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
const groupNames = self => visibleRooms(self).map(room => `"${room.name}"`).join(', ') || 'none'
const pickRoom = (ref, self) => {
const matches = rt.findRooms(ref).filter(room => visibleRooms(self).includes(room))
-@@ -395,12 +395,11 @@ export function install(rt) {
+@@ -395,12 +486,11 @@
if ((info?.hop ?? 0) >= MAX_HOPS) return 'This chain of Bot messages is already long. Stop here and report to the user instead.'
const text = String(args.message ?? '').trim()
if (text === '') return 'Write a message to post.'
@@ -1161,7 +1164,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
} catch (failure) {
return `Could not post in "${room.name}": ${failure.message}`
}
-@@ -445,6 +445,10 @@ export function install(rt) {
+@@ -445,6 +535,10 @@
.filter(option => option.label !== '')
.slice(0, 6)
if (options.length === 0) return 'Give at least one option.'
@@ -1172,7 +1175,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
// A hidden Bot-to-Bot turn shows none of its calls, so its card goes to the
// composer dock, which takes questions without a call.
const turn = rt.live.get(id)?.turn
-@@ -465,6 +465,77 @@ export function install(rt) {
+@@ -465,6 +559,77 @@
},
}))
@@ -1250,7 +1253,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
// dsh's own question tool holds the turn open until the user answers, and every
// message from another Bot or a group round queues behind it. Bot Sessions ask
// through ask_user instead, so the tool is taken out of their tool list.
-@@ -525,7 +525,7 @@ export function install(rt) {
+@@ -525,7 +690,7 @@
name: 'save_capability',
description: 'Create or update a shared MCP server, skill, or pack. Changes apply from your next message.',
parameters: {
@@ -1259,7 +1262,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
kind: { type: 'string', required: true, enum: ['mcp', 'skill', 'pack'] },
description: { type: 'string', description: 'What this capability provides' },
transport: { type: 'string', enum: ['stdio', 'streamable-http'], description: 'How to reach an MCP server: a local command or an HTTP URL' },
-@@ -545,7 +545,7 @@ export function install(rt) {
+@@ -545,7 +710,7 @@
try {
const item = await rt.capabilitySave(args, { botId: self })
const approval = item.kind === 'mcp' && item.transport === 'stdio' && !item.approved
@@ -1268,7 +1271,7 @@ diff --git a/src/host/tools.js b/src/host/tools.js
: ''
return `Saved ${item.name}. Bots with it attached use the change from their next message.${approval}`
} catch (error) { return error.message }
-@@ -587,7 +587,7 @@ export function install(rt) {
+@@ -587,7 +752,7 @@
: attached ? [attached] : []
const approvals = members
.filter(item => item.kind === 'mcp' && item.transport === 'stdio' && item.approved !== true)
@@ -1277,6 +1280,51 @@ diff --git a/src/host/tools.js b/src/host/tools.js
.join('')
return `${verb} ${args.name} ${args.bot ? `to ${result.bot}` : 'to yourself'}. Changes apply from ${result.bot}'s next message.${endpoint === 'attach' ? approvals : ''}`
} catch (error) { return error.message }
+@@ -595,5 +760,43 @@
+ }))
+ }
+
++ ctx.tools.register(defineTool({
++ name: 'track_tasks',
++ description: 'Read or update your own task list. Call it when the user gives you work, or when a task starts, moves, or finishes, so the board stays current. Omit tasks to read the list. Pass tasks to create or update: match an id you already have, or the title of a task you are still tracking. Each task has a title, a column (not-started, in-progress, done), a progress percent from 0 to 100, a priority (low, normal, high, urgent) and a short note. In a group the task is tracked for that group.',
++ parameters: {
++ tasks: {
++ type: 'array',
++ description: 'Tasks to create or update. Omit to read your list.',
++ items: {
++ type: 'object',
++ additionalProperties: false,
++ properties: {
++ id: { type: 'string', description: 'Existing task id. Omit to match an open task by title, or to create one.' },
++ title: { type: 'string', description: 'Short name of the work. Required when creating.' },
++ column: { type: 'string', enum: ['not-started', 'in-progress', 'done'], description: 'Board column.' },
++ progress: { type: 'integer', description: 'How far along, from 0 to 100.' },
++ priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'], description: 'How urgent the task is.' },
++ note: { type: 'string', description: 'One line on where the task stands.' },
++ group: { type: 'string', description: 'Group chat name this task belongs to. In a group, the current group is used when this is omitted.' },
++ },
++ },
++ },
++ },
++ output,
++ async execute(args, exec) {
++ await rt.load()
++ const self = rt.selfOf(exec.agent?.id)
++ if (!rt.botOf(self)) return 'Only a Bot on the team can track tasks.'
++ const roomId = rt.groupOwners.get(exec.agent?.id)?.roomId
++ try {
++ const tasks = await rt.trackBotTasks(self, args ?? {}, { sessionId: exec.agent?.id, roomId })
++ if (!Array.isArray(tasks) || tasks.length === 0) return 'No tasks.'
++ return tasks.map(task => `- ${task.id} [${columnOf(task)} ${task.progress ?? 0}% ${task.priority ?? 'normal'}] ${task.title}`).join('\n')
++ } catch (error) {
++ return error?.message ?? String(error)
++ }
++ },
++ }))
++
+ Object.assign(rt, { pickRoom, hideBlockingQuestion })
+ }
+
diff --git a/test/blocking-question.test.js b/test/blocking-question.test.js
--- a/test/blocking-question.test.js
+++ b/test/blocking-question.test.js
diff --git a/patches/ds-bot/0003-flybot-api-client.patch b/patches/ds-bot/0003-flybot-api-client.patch
index bed6d051f..34757da23 100644
--- a/patches/ds-bot/0003-flybot-api-client.patch
+++ b/patches/ds-bot/0003-flybot-api-client.patch
@@ -1165,7 +1165,7 @@ 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,64 @@
+@@ -1,44 +1,65 @@
+import { FLYBOT_CSS, FlyBotApprovals, FlyBotSessionDock, createClientApi as createFlyBotClient } from './flybot-slots.js'
import { call } from './api.js'
-import { isHex, inkText, paintCss } from './inks.js'
@@ -1206,6 +1206,7 @@ diff --git a/src/client/index.js b/src/client/index.js
import { USAGE_CSS } from './usage-page.js'
import { SCHEDULE_CSS } from './schedules.js'
+import { INBOX_CSS } from './inbox.js'
++import { KANBAN_CSS } from './kanban.js'
+import { MOBILE_CSS } from './mobile.js'
+import { PANE_MIN_WIDTH_CSS } from './panes.js'
import { STORAGE, migrateStorage } from './storage.js'
@@ -1348,7 +1349,7 @@ diff --git a/src/client/index.js b/src/client/index.js
expected = target
ctx.uiWorkspace.openSession(target)
},
-@@ -444,10 +444,20 @@ export function apply(ctx) {
+@@ -444,10 +444,22 @@ export function apply(ctx) {
// 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').
@@ -1366,8 +1367,10 @@ diff --git a/src/client/index.js b/src/client/index.js
openMemory: (botId) => { ui.set(state => ({ ...state, memory: botId, menu: null, palette: null })) },
closeMemory: () => { ui.set(state => (state.memory ? { ...state, memory: null } : state)) },
+ // The Agent inbox (#349) opens over the conversation; Open on a card closes it first.
-+ openInbox: () => { ui.set(state => ({ ...state, inbox: true, menu: null, palette: null, newChat: null })) },
++ openInbox: () => { ui.set(state => ({ ...state, inbox: true, board: null, menu: null, palette: null, newChat: null })) },
+ closeInbox: () => { ui.set(state => (state.inbox ? { ...state, inbox: null } : state)) },
++ openBoard: () => { ui.set(state => ({ ...state, board: true, inbox: null, menu: null, palette: null, newChat: null })) },
++ closeBoard: () => { ui.set(state => (state.board ? { ...state, board: null } : state)) },
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.
@@ -1376,7 +1379,7 @@ diff --git a/src/client/index.js b/src/client/index.js
if (surface.getSnapshot().mode === mode) return
if (mode === 'agent') {
- ui.set(state => ({ ...state, details: null, view: 'details', settings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, toast: null }))
-+ ui.set(state => ({ ...state, details: null, view: 'details', settings: null, remoteBotSettings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, inbox: null, toast: null }))
++ ui.set(state => ({ ...state, details: null, view: 'details', settings: null, remoteBotSettings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, inbox: null, board: null, toast: null }))
}
writeMode(mode)
surface.set(state => ({ ...state, mode }))
@@ -1401,7 +1404,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,24 @@ export function apply(ctx) {
+@@ -539,15 +539,26 @@ export function apply(ctx) {
secretCancel: requestId => after(call('secret-cancel', { requestId })),
secretUpdate: (name, scope) => after(call('secret-update', { name, scope })),
secretDelete: name => after(call('secret-delete', { name })),
@@ -1417,7 +1420,9 @@ diff --git a/src/client/index.js b/src/client/index.js
approveCapability: name => after(call('capability-approve', { name })),
+ setBotConnector: (botId, connector, enabled) => after(call('bot-connectors', { botId, connector, enabled })),
setBotCapability: (botId, name, attached) => after(call('bot-capability', { botId, name, attached })),
-+ tasks: () => call('tasks'),
++ tasks: (payload) => call('tasks', payload ?? {}),
++ taskUpdate: (payload) => call('task-update', payload),
++ taskCreate: (payload) => call('task-create', payload),
schedules: () => call('schedules'),
scheduleCreate: task => call('schedule-create', task),
+ scheduleUpdate: task => call('schedule-update', task),
@@ -1514,7 +1519,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,44 @@ export function apply(ctx) {
+@@ -752,33 +752,45 @@ export function apply(ctx) {
installCompiledStyles(SHELL_CSS, 'shell'),
installCompiledStyles(SIDEBAR_CSS, 'sidebar'),
installCompiledStyles(SECRET_CSS, 'secrets'),
@@ -1523,6 +1528,7 @@ diff --git a/src/client/index.js b/src/client/index.js
installCompiledStyles(USAGE_CSS, 'usage'),
installCompiledStyles(SCHEDULE_CSS, 'schedules'),
+ installCompiledStyles(INBOX_CSS, 'inbox'),
++ installCompiledStyles(KANBAN_CSS, 'kanban'),
installCompiledStyles(FEEDBACK_CSS, 'feedback'),
installCompiledStyles(ABOUT_CSS, 'about'),
+ installCompiledStyles(FLYBOT_CSS, 'flybot'),
@@ -1852,16 +1858,17 @@ diff --git a/src/client/new-chat.js b/src/client/new-chat.js
diff --git a/src/client/overlays.js b/src/client/overlays.js
--- a/src/client/overlays.js
+++ b/src/client/overlays.js
-@@ -8,6 +8,8 @@ import { SettingsDialog } from './settings.js'
+@@ -8,6 +8,9 @@ import { SettingsDialog } from './settings.js'
import { ContextMenu } from './context-menu.js'
import { PANEL_LINGER_MS, BotPanel } from './browser-pane.js'
import { MemoryDialog } from './memory-panel.js'
+import { RemoteBotSettingsSheet } from './remote-bot-settings.js'
+import { InboxDialog } from './inbox.js'
++import { KanbanDialog } from './kanban.js'
import { useLinger } from './motion.js'
// How long a closing dialog or menu stays for its exit (see their [data-leaving] CSS).
-@@ -36,8 +36,11 @@ export function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useA
+@@ -36,8 +36,12 @@ export function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useA
}, [ui.details, ui.view, ui.renaming, ui.tab])
const pane = open ?? kept
const [settings, settingsLeaving] = useLinger(ui.settings, DIALOG_EXIT_MS)
@@ -1870,17 +1877,19 @@ diff --git a/src/client/overlays.js b/src/client/overlays.js
const [memory, memoryLeaving] = useLinger(ui.memory && roster.byId[ui.memory] ? ui.memory : null, DIALOG_EXIT_MS)
const [menu, menuLeaving] = useLinger(ui.menu, MENU_EXIT_MS)
+ const [inbox, inboxLeaving] = useLinger(ui.inbox ?? null, DIALOG_EXIT_MS)
++ const [board, boardLeaving] = useLinger(ui.board ?? null, DIALOG_EXIT_MS)
return h(Fragment, null,
pane ? createPortal(h(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null,
ui.newChat ? createPortal(h(NewChat, { key: ui.newChat, mode: ui.newChat, roster, actions }), document.body) : null,
-@@ -46,5 +46,7 @@ export function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useA
+@@ -46,5 +46,8 @@ export function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useA
ui.voice ? createPortal(h(VoiceMode, { key: ui.voice, sessionId: ui.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
menu ? createPortal(h(ContextMenu, { key: `${menu.kind ?? ''}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null,
settings ? createPortal(h(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
- memory && roster.byId[memory] ? createPortal(h(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null)
+ remoteBotSettings ? createPortal(h(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null,
+ memory && roster.byId[memory] ? createPortal(h(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
-+ inbox ? createPortal(h(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null)
++ inbox ? createPortal(h(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null,
++ board ? createPortal(h(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null)
}
diff --git a/src/client/palette.js b/src/client/palette.js
--- a/src/client/palette.js
@@ -1923,7 +1932,7 @@ diff --git a/src/client/palette.js b/src/client/palette.js
diff --git a/src/client/sidebar.js b/src/client/sidebar.js
--- a/src/client/sidebar.js
+++ b/src/client/sidebar.js
-@@ -1,16 +1,19 @@
+@@ -1,16 +1,20 @@
-import { createElement as h, Fragment, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react'
+import { FlyBotSections, FlyBotAccountHeader, useSidebarStructure, botTree } from './flybot-slots.js'
+import { Component, createElement as h, Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
@@ -1943,10 +1952,11 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
+import { friendBotTitle, roomTitle, sharedRoomTitle, isShownRemoteChat } from '../shared/remote-label.js'
import { showsDshRow } from './dsh-offer.js'
+import { InboxRow } from './inbox.js'
++import { KanbanRow } from './kanban.js'
// -------------------------------------------------------------------------
// Sidebar
-@@ -37,77 +37,148 @@
+@@ -37,77 +37,148 @@ const workKeys = text => (text === '' ? [] : text.split(',').map((key) => {
}))
const workLine = (lead, tool, more) => h(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ''}`)
@@ -2113,7 +2123,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
: wide && unread && !active ? h('span', { className: 'bt-unread', 'aria-hidden': true }) : null)
}
-@@ -235,6 +235,31 @@
+@@ -235,6 +235,31 @@ function useListGlide(listRef, layout, key) {
}, [layout, key])
}
@@ -2145,7 +2155,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
export function TeamSidebar(props) {
const { wide, useRoster, useUi, useSessions, actions } = props
const listRef = useRef(null)
-@@ -249,14 +249,26 @@
+@@ -249,14 +249,26 @@ export function TeamSidebar(props) {
const order = useMemo(() => {
const stamps = JSON.parse(updated)
const entries = [
@@ -2175,11 +2185,12 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.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 }
-@@ -277,13 +277,16 @@
+@@ -277,13 +277,17 @@ export function TeamSidebar(props) {
h(UpdateButton, { useUpdate: props.useUpdate, actions }),
h('button', { type: 'button', className: 'bt-round', 'aria-label': t('New…'), title: t('New…'), onClick: () => actions.openNewChat('new') }, h(PlusIcon))),
wide && roster.readOnly ? h('div', { className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null,
+ h(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
++ h(KanbanRow, { wide, useUi, actions }),
h('div', { ref: listRef, className: 'bt-list', role: 'region', 'aria-label': t('Bot list') },
- main ? h(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : h('div', { className: 'bt-hint' }, t('Setting up your Main Bot…')),
- order.map(entry => (entry.kind === 'bot'
@@ -2195,7 +2206,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
}
// The update button at the sidebar's top right: a cloud while a newer DS Bot —
-@@ -327,29 +327,29 @@
+@@ -327,29 +327,29 @@ function UpdateButton({ useUpdate, actions }) {
setDshError('')
void actions.installDshUpdate().catch(failure => setDshError(hostText(failure?.message ?? String(failure))))
}
@@ -2232,7 +2243,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
: t('You have {version}.', { version: update.current })),
message ? h('div', { className: 'bt-update-error', role: 'alert' }, t('The update did not finish: {reason}', { reason: message })) : null,
h('div', { className: 'bt-update-actions' },
-@@ -372,36 +372,12 @@
+@@ -372,36 +372,12 @@ function UpdateButton({ useUpdate, actions }) {
}, dshPhase === 'installing' ? t('Updating…') : dshPhase === 'failed' ? t('Try again') : t('Update')))) : null), document.body) : null)
}
@@ -2273,7 +2284,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
}
// Account seat of the Settings trigger: the visible label stays for assistive tech only.
-@@ -442,6 +442,26 @@
+@@ -442,6 +442,26 @@ export function openHarnessOnboarding(id) {
})
}
@@ -2300,7 +2311,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
function openThroughMenu(trigger) {
const before = new Set(document.querySelectorAll('[role="menu"]'))
trigger.click()
-@@ -466,8 +466,12 @@
+@@ -466,8 +466,12 @@ function pressSettingsShortcut() {
}))
}
@@ -2315,7 +2326,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
// `seat` is the shell's settings.launcher seat: at priority -10 the whale wins it
// over a shell account plugin's own launcher (the Desktop's "··· More"), which comes
// back in Agent mode; FooterAccount stands in while no seat exists at all.
-@@ -511,9 +511,10 @@
+@@ -511,9 +511,10 @@ export function AccountLauncher({ seat = true, settingsOpen, openSettings, openO
const groups = [
[
[t('Usage'), GaugeIcon, () => actions.openSettings('usage')],
@@ -2329,7 +2340,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
],
[
[t('Switch to classic Agent mode'), SwitchIcon, () => actions.setSurface('agent')],
-@@ -523,6 +523,7 @@
+@@ -523,6 +523,7 @@ export function AccountLauncher({ seat = true, settingsOpen, openSettings, openO
h('button', { ref: button, type: 'button', className: 'bt-account', 'aria-label': t('Account'), 'aria-haspopup': 'menu', 'aria-expanded': open, onClick: toggle },
h('span', { className: 'bt-you', 'aria-hidden': true }, h(BotMark, { look: MAIN_LOOK, size: 24 }))),
open && box ? createPortal(h('div', { className: 'bt-menu bt-menu-up bt-account-menu', role: 'menu', 'aria-label': t('Account'), style: box },
@@ -2337,7 +2348,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
groups.map((group, index) => h(Fragment, { key: index },
index > 0 ? h('div', { className: 'bt-menu-sep', role: 'separator' }) : null,
group.map(([label, Icon, run, hint]) => h('button', {
-@@ -556,7 +556,10 @@
+@@ -556,7 +556,10 @@ function useLandOnMainBot(roster, useSessions, actions) {
return undefined
}
const last = lastBot.current
@@ -2349,7 +2360,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
return () => clearTimeout(timer)
}, [mainView, roster])
}
-@@ -572,9 +572,9 @@
+@@ -572,9 +572,9 @@ export function FooterAccount(props) {
export function BotReturn({ wide, useSurface, actions }) {
const mode = useSurface(value => value.mode)
if (mode !== 'agent') return null
@@ -2362,7 +2373,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
}
// Agent mode has none of the shell-group styles, so the return button carries its own.
-@@ -582,22 +582,22 @@
+@@ -582,22 +582,22 @@ export const RETURN_CSS = `
.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease}
.bt-return:hover{background:rgba(127,127,127,.14)}
.bt-return:active{transform:scale(.96)}
@@ -2390,7 +2401,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
// shell's footer classes are CSS-module names, so they match on their stems.
export const SIDEBAR_CSS = `
.bt-side-top .bt-brand{cursor:default;padding-left:6px}
-@@ -658,4 +658,14 @@
+@@ -658,4 +658,14 @@ export const SIDEBAR_CSS = `
.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)}
@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}}
@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}}
@@ -2704,7 +2715,7 @@ diff --git a/src/client/user-message.js b/src/client/user-message.js
// -------------------------------------------------------------------------
// The user's own messages. DSH draws every "@word" in them as a file reference, and
-@@ -28,12 +28,79 @@ export function contentParts(content) {
+@@ -28,10 +28,77 @@ export function contentParts(content) {
}
// In a group only its members are called; elsewhere a mention opens that Bot's chat.
@@ -2744,8 +2755,8 @@ diff --git a/src/client/user-message.js b/src/client/user-message.js
+ const remotes = (room?.participants ?? []).filter(p => p.remote && p.leftSeq === undefined)
+ .map(p => ({ id: p.handle || p.id, name: remoteBotTitle(p), kind: 'bot', remote: p, mentionLabel: remoteBotTitle(p) }))
+ return bots.concat(humans, remotes, remoteMentionableBots(roster, roster.bots))
- }
-
++}
++
+// A face-to-face line was typed on either home, so a Bot is matched by every name the room
+// host routes on (faceAliases: "SelfA's Chief" typed on B is A's own Chief on A) and the chip
+// reads as the Members panel names it, "SelfA · Chief", the same on both homes (#305).
@@ -2782,11 +2793,9 @@ diff --git a/src/client/user-message.js b/src/client/user-message.js
+ ownerName: remote?.ownerName,
+ mine: roster?.self?.memberId === authorId,
+ }
-+}
-+
+ }
+
// Runs of plain text and Bot mentions, in order.
- export function userTextParts(text, bots, referenceLabels = []) {
- const parts = []
@@ -43,16 +43,25 @@ export function userTextParts(text, bots, referenceLabels = []) {
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) })
diff --git a/patches/ds-bot/0005-social-seams.patch b/patches/ds-bot/0005-social-seams.patch
index f26d5aa65..f63bf4c3b 100644
--- a/patches/ds-bot/0005-social-seams.patch
+++ b/patches/ds-bot/0005-social-seams.patch
@@ -770,12 +770,13 @@ diff --git a/src/host/channel.js b/src/host/channel.js
await rt.ensureCapabilities(sessionId)
await ctx.sessionController.prompt({
requestId: randomUUID(),
-@@ -319,32 +319,112 @@ export function install(rt) {
+@@ -319,32 +319,113 @@ export function install(rt) {
case 'diagnostics-clear':
await rt.diagnostics.clear()
return ok(true)
+ case 'tasks':
+ case 'task-create':
++ case 'task-update':
+ case 'task-accept':
+ case 'task-start':
+ case 'task-return':
diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch
index aa912dfac..107fd6451 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,50 @@
+@@ -49,6 +49,50 @@ var ZH = {
"Unread activity": "有未读",
Search: "搜索",
"New chat": "新对话",
@@ -54,7 +54,7 @@ diff --git a/client.js b/client.js
"New…": "新建",
"Animation": "动画",
"Bot animation": "Bot 动画",
-@@ -63,6 +63,21 @@
+@@ -63,6 +63,21 @@ var ZH = {
Duplicate: "复制一份",
Hidden: "已隐藏",
Admin: "管理员",
@@ -76,7 +76,7 @@ diff --git a/client.js b/client.js
Add: "添加",
Close: "关闭",
Copy: "复制",
-@@ -78,21 +78,68 @@
+@@ -78,21 +78,68 @@ var ZH = {
" and ": "、",
and: "和",
"{count} members": "{count} 位成员",
@@ -148,7 +148,7 @@ diff --git a/client.js b/client.js
Connectors: "连接器",
"Search connectors": "搜索连接器",
"No connectors": "暂无连接器",
-@@ -129,6 +129,59 @@
+@@ -129,6 +129,59 @@ var ZH = {
"Connecting…": "正在连接…",
"Could not connect: {error}": "连接失败:{error}",
"Connected as {account}": "已连接:{account}",
@@ -208,7 +208,7 @@ diff --git a/client.js b/client.js
Connected: "已连接",
Disconnect: "断开",
Connect: "连接",
-@@ -141,17 +141,44 @@
+@@ -141,17 +141,44 @@ var ZH = {
"GitHub addresses must start with https://": "GitHub 地址必须以 https:// 开头",
"This DSH cannot load connectors": "这个 DSH 不能加载连接器",
"No such connector": "没有这个连接器",
@@ -257,7 +257,7 @@ diff --git a/client.js b/client.js
// Header
"Message {name}": "给 {name} 发消息",
Message: "发消息",
-@@ -162,8 +162,6 @@
+@@ -162,8 +162,6 @@ var ZH = {
"Dismiss new messages": "关闭新消息提示",
New: "新",
// New chat
@@ -266,7 +266,7 @@ diff --git a/client.js b/client.js
"Add to group chat": "加入群聊",
"Message Bot": "给 Bot 发消息",
"To:": "发给:",
-@@ -187,9 +187,20 @@
+@@ -187,9 +187,20 @@ var ZH = {
Everyone: "所有人",
"Every member answers in turn, admin first.": "每位成员轮流回答,管理员先说。",
"Members · {count}": "成员 · {count}",
@@ -287,7 +287,7 @@ diff --git a/client.js b/client.js
"Remove {name} from the group": "把 {name} 移出群聊",
"Remove from group": "移出群聊",
"Add member": "添加成员",
-@@ -247,6 +247,25 @@
+@@ -247,6 +247,25 @@ var ZH = {
"This model is not available now. Pick another one.": "这个模型现在不可用,请换一个。",
"Every message of this Bot runs on it.": "这个 Bot 的每条消息都用它。",
"Applies from the next message.": "从下一条消息开始生效。",
@@ -313,7 +313,7 @@ diff --git a/client.js b/client.js
Profile: "资料",
Label: "标签",
"Optional. Shown under the name.": "可选,显示在名字下方。",
-@@ -270,6 +270,7 @@
+@@ -270,6 +270,7 @@ var ZH = {
"Kept above the other chats.": "排在其他对话上面。",
"Search still finds it.": "仍然可以搜索到。",
Manage: "管理",
@@ -321,7 +321,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 @@
+@@ -303,6 +303,8 @@ var ZH = {
"Harness default": "Harness 默认",
Saving: "保存中",
"Manage models": "管理模型",
@@ -330,7 +330,7 @@ diff --git a/client.js b/client.js
// Shapes
Whale: "鲸鱼",
Star: "星星",
-@@ -374,9 +374,10 @@
+@@ -374,9 +374,10 @@ var ZH = {
"No group chats yet.": "还没有群聊。",
"A group chat lets several Bots work on one thing with you.": "在群聊里,几个 Bot 和你一起做一件事。",
"Back to group chats": "返回群聊列表",
@@ -343,7 +343,7 @@ diff --git a/client.js b/client.js
"Harness settings": "Harness 设置",
"Back to Bots": "返回 Bot 列表",
"Close settings": "关闭设置",
-@@ -385,7 +385,6 @@
+@@ -385,7 +385,6 @@ var ZH = {
Pin: "置顶",
Pinned: "已置顶",
"Mark as Unread": "标为未读",
@@ -351,7 +351,7 @@ diff --git a/client.js b/client.js
"Group settings": "群设置",
"Rename group": "重命名群聊",
"Copy conversation ID": "复制对话 ID",
-@@ -394,6 +394,8 @@
+@@ -394,6 +394,8 @@ var ZH = {
// The DSH Agent's own row and Session list. "DSH Agent" is a product name and
// stays in English.
"Add DSH Agent": "添加 DSH Agent",
@@ -360,7 +360,7 @@ diff --git a/client.js b/client.js
"Remove DSH Agent": "移除 DSH Agent",
"New session": "新会话",
Archive: "归档",
-@@ -404,13 +404,13 @@
+@@ -404,13 +404,13 @@ var ZH = {
"{n} h": "{n} 小时",
"{n} d": "{n} 天",
// Plugin updates
@@ -380,7 +380,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,36 @@
+@@ -420,8 +420,59 @@ var ZH = {
"DSH update": "DSH 更新",
"DSH {version}": "DSH {version}",
"Restart DSH to start using DSH {version}.": "重启 DSH 后开始使用 DSH {version}。",
@@ -410,6 +410,29 @@ diff --git a/client.js b/client.js
+ "Latest: {text}": "最新:{text}",
+ "{count} tool calls": "{count} 次工具调用",
+ "Untitled task": "未命名任务",
++ // Bot task board (#1058)
++ Tasks: "任务",
++ "Close tasks": "关闭任务",
++ "Task board": "任务板",
++ "Not started": "未开始",
++ "Groups you admin": "你管理的群",
++ "No tasks yet.": "还没有任务。",
++ "Nothing in this column.": "这一列是空的。",
++ Highest: "最高",
++ High: "较高",
++ Low: "较低",
++ Claimed: "已认领",
++ Blocked: "受阻",
++ Start: "开始",
++ Complete: "完成",
++ "Start task": "开始任务",
++ "Complete task": "完成任务",
++ "Add task": "添加任务",
++ "Task title": "任务标题",
++ Progress: "进度",
++ Group: "群",
++ "{percent}%": "{percent}%",
++ "{done}/{total}": "{done}/{total}",
+ "for {time}": "已 {time}",
+ "just started": "刚开始",
+ "Open {name}": "打开 {name}",
@@ -418,7 +441,7 @@ diff --git a/client.js b/client.js
// Command palette
Messages: "消息",
Commands: "命令",
-@@ -446,6 +446,7 @@
+@@ -446,6 +446,7 @@ var ZH = {
Reply: "回复",
"More message actions": "更多操作",
More: "更多",
@@ -426,7 +449,7 @@ diff --git a/client.js b/client.js
Reactions: "表情",
"Message actions": "消息操作",
Download: "下载",
-@@ -459,6 +459,9 @@
+@@ -459,6 +459,9 @@ var ZH = {
"Replies from": "群聊回复:",
"Reply from": "收到回复:",
"Message from": "收到消息:",
@@ -436,7 +459,7 @@ diff --git a/client.js b/client.js
"Stopped by": "叫停者:",
"{count} messages with": "{count} 条消息,来自",
"Routine started": "例行任务开始",
-@@ -468,6 +468,8 @@
+@@ -468,6 +468,8 @@ var ZH = {
"Working out a plan": "理一下思路",
"Sorting out the details": "梳理细节",
"On it": "在做了",
@@ -445,7 +468,7 @@ diff --git a/client.js b/client.js
"Busy with it": "正在处理",
"Moving ahead": "推进中",
"Taking care of it": "处理中",
-@@ -495,7 +495,6 @@
+@@ -495,7 +495,6 @@ var ZH = {
"Looking back through the chat": "翻看聊天记录",
"{name} looked up its own chat": "{name} 查看了自己的对话",
"{name} looked up its own chat for “{text}”": "{name} 在自己的对话里查找「{text}」",
@@ -453,7 +476,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,29 @@
+@@ -506,12 +506,29 @@ var ZH = {
"Asked to make the tool call it wrote as text": "工具调用被写成了文字,已让它重新调用",
"A Bot": "一个 Bot",
"{name} is waiting for your approval": "{name} 等你批准",
@@ -484,7 +507,7 @@ diff --git a/client.js b/client.js
"Couldn't message {name}": "没能给 {name} 发消息",
Messaged: "已发消息给",
"Couldn't stop {name}": "没能叫停 {name}",
-@@ -539,7 +539,7 @@
+@@ -539,7 +539,7 @@ var ZH = {
Dismissed: "已关闭",
"Your answer": "你的回答",
// Secrets
@@ -493,7 +516,7 @@ diff --git a/client.js b/client.js
"No Bot": "没有 Bot",
"Paste or type the key": "粘贴或输入密钥",
"Only {name}": "只给 {name}",
-@@ -582,7 +582,7 @@
+@@ -582,7 +582,7 @@ var ZH = {
"NAME, e.g. GITHUB_TOKEN": "名字,例如 GITHUB_TOKEN",
"What it is for": "用途",
Value: "值",
@@ -502,7 +525,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 @@
+@@ -747,11 +747,11 @@ var ZH = {
"Compared with the {count} days before": "与再往前 {count} 天相比",
"{share}% of all usage from {span}": "占 {span} 全部用量的 {share}%",
// About
@@ -517,7 +540,7 @@ diff --git a/client.js b/client.js
Updates: "更新",
"Check for updates": "检查更新",
"Checking…": "正在检查…",
-@@ -761,6 +761,13 @@
+@@ -761,6 +761,13 @@ var ZH = {
"Update checks are off.": "已关闭更新检查。",
"Installed from a local folder; update it there.": "从本地文件夹安装,请在那里更新。",
"Updates are not offered for this install.": "这种安装方式不提供更新。",
@@ -531,7 +554,7 @@ diff --git a/client.js b/client.js
Links: "链接",
"Source code": "源代码",
"Release notes": "发布说明",
-@@ -776,7 +776,7 @@
+@@ -776,7 +776,7 @@ var ZH = {
Show: "查看",
Hide: "收起",
"GitHub issue": "GitHub issue",
@@ -540,7 +563,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 @@
+@@ -788,7 +788,7 @@ var ZH = {
"Recent errors": "最近的错误",
"Recent errors · {count}": "最近的错误 · {count}",
"No errors recorded.": "没有错误记录。",
@@ -549,7 +572,7 @@ diff --git a/client.js b/client.js
"Model call": "模型调用",
"This window": "这个窗口",
"Error log": "错误日志",
-@@ -821,6 +821,14 @@
+@@ -821,6 +821,14 @@ var ZH = {
"Add at least one more Bot": "再至少添加一个 Bot",
Members: "成员",
// Connectors and scheduled tasks
@@ -564,7 +587,7 @@ diff --git a/client.js b/client.js
"Scheduled tasks": "定时任务",
"Every day": "每天",
"Every {count} days": "每 {count} 天",
-@@ -834,7 +834,15 @@
+@@ -834,7 +834,15 @@ var ZH = {
"Every weekend at {time}": "每个周末 {time}",
"On the schedule {expression}": "按计划 {expression}",
"Next: {time}": "下次 {time}",
@@ -580,7 +603,7 @@ diff --git a/client.js b/client.js
"in {name}": "在 {name}",
"Click again to delete": "再点一次删除",
"Delete task": "删除任务",
-@@ -856,8 +856,8 @@
+@@ -856,8 +856,8 @@ var ZH = {
Unit: "单位",
"Create task": "创建任务",
"Turn on Automation tasks in DSH": "在 DSH 里打开「自动化任务」",
@@ -591,7 +614,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 @@
+@@ -873,6 +873,12 @@ var ZH = {
"Repeat every 1 minute to 7 days": "重复间隔要在 1 分钟到 7 天之间",
"Pick when it runs": "选择什么时候运行",
"No such task": "没有这个任务",
@@ -604,7 +627,7 @@ diff --git a/client.js b/client.js
"Working folder": "工作文件夹",
"Working folder: {folder}": "工作文件夹:{folder}",
Folder: "文件夹",
-@@ -902,6 +902,8 @@
+@@ -902,6 +902,8 @@ var HOST_ERRORS = [
[/^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)。这个窗口只能查看,不能修改;关掉另一个后刷新。"],
@@ -613,7 +636,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 @@
+@@ -930,6 +930,20 @@ var HOST_ZH = {
"Unknown Bot": "找不到这个 Bot",
"Unknown conversation": "找不到这个对话",
"A group chat needs at least two Bots": "群聊至少要两个 Bot",
@@ -634,7 +657,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 @@
+@@ -951,7 +951,9 @@ var HOST_ZH = {
"The model wrote nothing": "模型没有输出内容",
"Write the entry to save.": "请写下要保存的内容。",
"This DeepSeek Harness has no folder chooser": "这个 DeepSeek Harness 没有文件夹选择窗口",
@@ -645,7 +668,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 @@
+@@ -1016,6 +1016,14 @@ function noteClientError(where, error) {
entries.push({ time: now, level: "error", text, ...code ? { code } : {} });
if (entries.length > MAX) entries.splice(0, entries.length - MAX);
}
@@ -660,7 +683,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 @@
+@@ -1042,12 +1042,17 @@ var FAILURES = {
};
function failureLabel(code, message) {
if (FAILURES[code]) return t(FAILURES[code]);
@@ -681,7 +704,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 @@
+@@ -1066,10 +1066,16 @@ var REFUSALS = /* @__PURE__ */ new Set(["bot/refused", "bot/invalid", "bot/read-
var call = async (endpoint, payload, signal) => {
let response;
try {
@@ -699,7 +722,7 @@ diff --git a/client.js b/client.js
body: JSON.stringify({ endpoint, payload: payload ?? null }),
signal
});
-@@ -1087,7 +1087,7 @@
+@@ -1087,7 +1087,7 @@ var call = async (endpoint, payload, signal) => {
if (!result.ok) {
const code = result.error?.code ?? "bot/error";
const message = hostText(result.error?.message ?? "Request failed");
@@ -708,7 +731,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 @@
+@@ -1097,6 +1097,373 @@ var call = async (endpoint, payload, signal) => {
return result.value;
};
@@ -1082,7 +1105,7 @@ diff --git a/client.js b/client.js
// src/client/inks.js
var INKS = {
deepseek: ["#4D6BFE", "#6A84FF"],
-@@ -1177,15 +1177,75 @@
+@@ -1177,15 +1177,75 @@ function defaultShape(id) {
value = Math.imul(value ^ value >>> 13, 3266489909);
return DEFAULT_SHAPES[((value ^ value >>> 16) >>> 0) % DEFAULT_SHAPES.length];
}
@@ -1160,18 +1183,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 @@
+@@ -1206,8 +1206,8 @@ function bodyOf(def, paint, flat = false) {
return "";
}
}
-var eyeMarkup = ([cx, cy, w, h34, tilt]) => ``;
-var happyEyeMarkup = ([cx, cy, w, h34, tilt]) => ``;
-+var eyeMarkup = ([cx, cy, w, h42, tilt]) => ``;
-+var happyEyeMarkup = ([cx, cy, w, h42, tilt]) => ``;
++var eyeMarkup = ([cx, cy, w, h43, tilt]) => ``;
++var happyEyeMarkup = ([cx, cy, w, h43, 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,163 +1242,17 @@
+@@ -1242,162 +1242,16 @@ function maskOf(shape) {
}
var DEFAULT_MASK = maskOf(FALLBACK_SHAPE);
@@ -1194,16 +1217,7 @@ diff --git a/client.js b/client.js
- for (const listener of [...listeners2]) listener();
- }
- };
-+// src/client/cluster-order.js
-+function clusterOrder(people2) {
-+ const list = Array.isArray(people2) ? people2 : [];
-+ const remote = (bot) => Boolean(bot?.remote);
-+ if (!list.some(remote)) return list;
-+ const locals = list.filter((bot) => !remote(bot));
-+ const remotes = list.filter(remote);
-+ if (list.length <= 4) return [...remotes, ...locals];
-+ return [...locals, ...remotes];
- }
+-}
-var EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bots: [], rooms: [], agents: [], exchangeTurns: {}, questions: {}, secrets: [], capabilities: [], capabilityStatuses: {}, secretRequests: {}, models: [], defaultModel: null, modelsKey: null, prefs: {}, byId: {}, roomsById: {}, agentsById: {}, agentOf: {}, ready: false };
-var isMainOf = (roster, id) => id !== void 0 && (roster.mainBotIds ?? []).includes(id);
-function indexRoster(value) {
@@ -1235,7 +1249,7 @@ diff --git a/client.js b/client.js
- }
- return { ...state, [sessionId]: own };
-}
-
+-
-// src/client/themes.js
-var THEME_KEYS = ["bg", "sidebar", "bubble", "card", "text", "user", "userText", "accent"];
-var THEMES = {
@@ -1281,7 +1295,16 @@ diff --git a/client.js b/client.js
- "--bt-tag-bg": rgba(mode.text, dark ? 0.09 : 0.06),
- "--bt-tag-ink": rgba(mode.text, 0.58)
- };
--}
++// src/client/cluster-order.js
++function clusterOrder(people2) {
++ const list = Array.isArray(people2) ? people2 : [];
++ const remote = (bot) => Boolean(bot?.remote);
++ if (!list.some(remote)) return list;
++ const locals = list.filter((bot) => !remote(bot));
++ const remotes = list.filter(remote);
++ if (list.length <= 4) return [...remotes, ...locals];
++ return [...locals, ...remotes];
+ }
-var declarations = (vars) => Object.entries(vars).map(([name, value]) => `${name}:${value}`).join(";");
-var themeCss = (theme, accent) => `html body{${declarations(themeVars(theme.light, false, accent ?? theme.light.accent))}}
-html body[data-ds-dark-theme]{${declarations(themeVars(theme.dark, true, accent ?? theme.dark.accent))}}
@@ -1340,14 +1363,24 @@ diff --git a/client.js b/client.js
- };
-}
-var allThemes = () => ({ ...THEMES, ...customThemes });
--
+
// src/shared/mentions.js
var ALL_WORDS = ["all", "everyone", "所有人", "全体"];
- var NAME_GOES_ON = /^(?:[a-z0-9_\-/\\]|\.[a-z0-9_-])/i;
-@@ -1445,14 +1445,36 @@
+@@ -1445,17 +1445,39 @@ function mentionSpans(text, bots, { references = [] } = {}) {
}
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})`;
+- cursor = span.end;
+function unmatchedMentions(text, bots, { references = [] } = {}) {
+ const value = String(text ?? "");
+ const skip = [
@@ -1369,20 +1402,22 @@ 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;
-- for (const span of mentionSpans(text, bots)) {
++ let out = "";
++ let cursor = 0;
+ 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})`;
- cursor = span.end;
-@@ -1463,6 +1463,20 @@
++ 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) {
const anchor = event.target instanceof Element ? event.target.closest(`a[href^="${MENTION_ORIGIN}"]`) : null;
return anchor ? anchor.getAttribute("href").slice(MENTION_ORIGIN.length) : null;
}
@@ -1403,7 +1438,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 @@
+@@ -1483,6 +1483,15 @@ function relativeShort(ms) {
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;
@@ -1419,7 +1454,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 @@
+@@ -1494,6 +1494,13 @@ function reminderPrompts(text) {
}
}
var stripHeader = (text) => text.replace(HEADER, "");
@@ -1433,7 +1468,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 @@
+@@ -1506,25 +1506,35 @@ function summaryBlocks(text) {
paragraph = [];
};
for (const raw of String(text ?? "").split("\n")) {
@@ -1473,7 +1508,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 @@
+@@ -1597,14 +1597,21 @@ function commRunOf(order, peers, turn, hiddenTurns = []) {
}
return { count, peerIds };
}
@@ -1498,16 +1533,65 @@ diff --git a/client.js b/client.js
}
return "";
}
-@@ -1624,9 +1624,876 @@
+@@ -1624,211 +1624,1078 @@ function usageOf(list, ids) {
const hit = input > 0 ? Math.round(cacheRead / input * 100) : 0;
return t("{tokens} tok · Cache hit {hit}%", { tokens: formatTokens(input + output), hit });
}
+-
+-// src/client/motion.js
+-var import_react = require("react");
+-var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
+-function springEasing(stiffness, damping) {
+- const w0 = Math.sqrt(stiffness);
+- const zeta = damping / (2 * w0);
+- const at = (t2) => {
+- if (zeta >= 1) return 1 - (1 + w0 * t2) * Math.exp(-w0 * t2);
+- const wd = w0 * Math.sqrt(1 - zeta * zeta);
+- return 1 - Math.exp(-zeta * w0 * t2) * (Math.cos(wd * t2) + zeta * w0 / wd * Math.sin(wd * t2));
+- };
+- let end = 0.05;
+- while (end < 2 && Math.abs(1 - at(end)) > 1e-3) end += 0.01;
+- const points = Array.from({ length: 25 }, (_, index) => (index === 24 ? 1 : at(end * index / 24)).toFixed(4));
+- return { duration: Math.round(end * 1e3), easing: `linear(${points.join(",")})`, stiffness, damping };
+function faceRowTitle(title) {
+ const text = String(title ?? "");
+ const named = text.match(/^Face-to-face · (.+) · (\d{6})$/);
+ if (named) return `${named[2]} · ${named[1]}`;
+ return text.match(/^Face-to-face · (\d{6})$/)?.[1] ?? text;
-+}
+ }
+-var SPRING_SHAPE = springEasing(400, 31.2);
+-var SPRING_TAP = springEasing(900, 48);
+-var SPRING_LIST = springEasing(484, 35.2);
+-var SPRING_STRETCH = springEasing(320, 20);
+-var SPRING_POP = springEasing(500, 22);
+-var SPRING_LEAD = springEasing(784, 48.2);
+-var SPRING_TRAIL = springEasing(289, 29.2);
+-var QUICK_EDGES = { lead: springEasing(900, 51), trail: springEasing(400, 34) };
+-var spring = (curve, ...properties) => properties.map((property) => `${property} ${curve.duration}ms ${curve.easing}`).join(",");
+-function liveSprings(apply2) {
+- const values = /* @__PURE__ */ new Map();
+- let frame = 0;
+- let last = 0;
+- const read = () => Object.fromEntries([...values].map(([key, value]) => [key, value.x]));
+- const step = (now) => {
+- const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3));
+- last = now;
+- const steps = Math.ceil(dt * 240);
+- const h34 = dt / steps;
+- let moving = false;
+- for (const value of values.values()) {
+- for (let index = 0; index < steps; index += 1) {
+- value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h34;
+- value.x += value.v * h34;
+- }
+- if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) {
+- value.x = value.to;
+- value.v = 0;
+- } else moving = true;
+- }
+- apply2(read());
+- frame = moving ? requestAnimationFrame(step) : 0;
+- };
+function modelOf(bot, roster) {
+ const ref = bot.model ?? bot.appliedModel;
+ if (!ref) return void 0;
@@ -1522,28 +1606,112 @@ diff --git a/client.js b/client.js
+function awayGroups(groups, roomsById) {
+ return (groups ?? []).filter((entry) => roomsById?.[entry.id]?.origin !== "face-to-face");
+}
-
--// src/client/motion.js
++
+// src/client/sources.js
+function createSource(initial) {
+ let value = initial;
+ const listeners2 = /* @__PURE__ */ new Set();
-+ return {
+ return {
+- target: () => values.size ? Object.fromEntries([...values].map(([key, value]) => [key, value.to])) : null,
+- set(targets, curves, jump) {
+- for (const [key, to] of Object.entries(targets)) {
+- const value = values.get(key);
+- const { stiffness: k, damping: c } = curves[key];
+- if (!value || jump) values.set(key, { x: to, v: 0, to, k, c });
+- else Object.assign(value, { to, k, c });
+- }
+- if (jump) {
+- cancelAnimationFrame(frame);
+- frame = 0;
+- apply2(read());
+- return;
+- }
+- if (!frame) {
+- last = performance.now();
+- frame = requestAnimationFrame(step);
+- }
+ getSnapshot: () => value,
+ subscribe: (listener) => {
+ listeners2.add(listener);
+ return () => {
+ listeners2.delete(listener);
+ };
-+ },
+ },
+- stop: () => cancelAnimationFrame(frame)
+- };
+-}
+-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) {
@@ -1569,25 +1737,86 @@ 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;
+ return cache.has(sessionId);
-+}
+ }
+-
+function countCards(state, sessionId, callIds, delta) {
+ const own = { ...state[sessionId] };
+ for (const callId of callIds) {
@@ -1599,7 +1828,7 @@ diff --git a/client.js b/client.js
+}
+
+// src/client/mark.js
- var import_react = require("react");
++var import_react = require("react");
+var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"];
+var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle";
+var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working";
@@ -1859,29 +2088,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: h42, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) {
-+ if (!preview || typeof h42 !== "function") return null;
++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;
+ 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 h42(
++ return h43(
+ "div",
+ { className: "fs-preview", "data-flybot": at, role: "status" },
-+ h42("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"),
-+ preview.lead ? h42("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null,
-+ preview.people ? h42("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null,
-+ preview.expiresAt ? h42("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null,
-+ onJoin || onClear ? h42(
++ 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(
+ "div",
+ { className: "fs-preview-foot", "data-flybot": child("foot") },
-+ showClear && onClear ? h42("button", {
++ showClear && onClear ? h43("button", {
+ type: "button",
+ className: "fs-btn fs-btn-sm",
+ "data-flybot": child("clear"),
+ disabled: !!busy,
+ onClick: onClear
+ }, clearLabel) : null,
-+ onJoin ? h42("button", {
++ onJoin ? h43("button", {
+ type: "button",
+ className: "fs-btn fs-btn-sm fs-btn-primary",
+ "data-flybot": child("join"),
@@ -2373,101 +2602,212 @@ 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);
-@@ -1659,12 +1659,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 h42 = 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) * h42;
-+ value.x += value.v * h42;
- }
- if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) {
- value.x = value.to;
-@@ -1699,10 +1699,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);
++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_react.useRef)(null);
-- const own = (0, import_react.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,
-@@ -1736,7 +1736,7 @@
- springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion());
- container.dataset.glide = "";
- };
-- (0, import_react.useLayoutEffect)(() => {
++ if (own.current === null) {
++ own.current = {
++ key: void 0,
++ node: null,
++ observer: null,
++ springs: liveSprings(({ start, end }) => {
++ const knob = thumb.current;
++ if (!knob) return;
++ knob.style.transform = axis === "x" ? `translateX(${start}px)` : `translateY(${start}px)`;
++ knob.style[axis === "x" ? "width" : "height"] = `${Math.max(0, end - start)}px`;
++ })
++ };
++ }
++ const place = (jump) => {
++ const container = box.current;
++ const knob = thumb.current;
++ if (!container || !knob) return;
++ const chosen = container.querySelector(selector);
++ if (!chosen) {
++ knob.dataset.hidden = "";
++ return;
++ }
++ const start = axis === "x" ? chosen.offsetLeft : chosen.offsetTop;
++ const end = start + (axis === "x" ? chosen.offsetWidth : chosen.offsetHeight);
++ const { springs } = own.current;
++ const before = springs.target();
++ const hidden = knob.dataset.hidden !== void 0;
++ if (jump && !hidden && before?.start === start && before?.end === end && container.dataset.glide !== void 0) return;
++ delete knob.dataset.hidden;
++ const forward = before === null || start >= before.start;
++ springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion());
++ container.dataset.glide = "";
++ };
+ (0, import_react3.useLayoutEffect)(() => {
- const state = own.current;
- const node = box.current;
- if (node === state.node && key === state.key) return;
-@@ -1750,7 +1750,7 @@
- }
- place(fresh);
- });
-- (0, import_react.useEffect)(() => () => {
++ const state = own.current;
++ const node = box.current;
++ if (node === state.node && key === state.key) return;
++ const fresh = node !== state.node;
++ state.key = key;
++ if (fresh) {
++ state.observer?.disconnect();
++ state.node = node;
++ state.observer = node && typeof ResizeObserver === "function" ? new ResizeObserver(() => place(true)) : null;
++ state.observer?.observe(node);
++ }
++ place(fresh);
++ });
+ (0, import_react3.useEffect)(() => () => {
- own.current.observer?.disconnect();
- own.current.springs.stop();
- }, []);
-@@ -1758,16 +1758,16 @@
- }
- function Segmented({ label, value, className, children }) {
- const glide = useGlide(value);
-- return (0, import_react.createElement)(
++ own.current.observer?.disconnect();
++ own.current.springs.stop();
++ }, []);
++ return { box, thumb };
++}
++function Segmented({ label, value, className, children }) {
++ const glide = useGlide(value);
+ return (0, import_react3.createElement)(
- "div",
- { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label },
-- (0, import_react.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ "div",
++ { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label },
+ (0, import_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_react.useRef)(null);
-- (0, import_react.useLayoutEffect)(() => {
++ 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();
-@@ -1788,8 +1788,8 @@
- }, [key]);
- }
- function useMorph(box, key) {
-- const last = (0, import_react.useRef)({ key, height: null });
-- (0, import_react.useLayoutEffect)(() => {
++ const container = list.current;
++ if (!container) return;
++ const next = /* @__PURE__ */ new Map();
++ const moving = tops.current !== null && !reducedMotion();
++ for (const node of container.querySelectorAll(selector)) {
++ const name = id(node);
++ const top = node.offsetTop;
++ next.set(name, top);
++ if (!moving) continue;
++ const before = tops.current.get(name);
++ if (before === void 0) {
++ node.animate([{ opacity: 0, filter: "blur(4px)" }, { opacity: 1, filter: "blur(0)" }], { duration: 180, easing: "ease-out" });
++ } else if (Math.abs(before - top) > 0.5) {
++ node.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], { duration: SPRING_LIST.duration, easing: SPRING_LIST.easing });
++ }
++ }
++ tops.current = next;
++ }, [key]);
++}
++function useMorph(box, key) {
+ const last = (0, import_react3.useRef)({ key, height: null });
+ (0, import_react3.useLayoutEffect)(() => {
- const node = box.current;
- const state = last.current;
- if (!node) {
-@@ -1817,8 +1817,8 @@
- });
- }
- function useLinger(value, ms) {
-- const [kept, setKept] = (0, import_react.useState)(value);
-- (0, import_react.useEffect)(() => {
++ const node = box.current;
++ const state = last.current;
++ if (!node) {
++ last.current = { key, height: null };
++ return;
++ }
++ const running = node.getAnimations().filter((animation2) => animation2.id === "bt-morph");
++ if (key === state.key) {
++ if (running.length === 0) state.height = node.offsetHeight;
++ return;
++ }
++ const from = running.length ? node.offsetHeight : state.height;
++ for (const animation2 of running) animation2.cancel();
++ const to = node.offsetHeight;
++ last.current = { key, height: to };
++ if (from === null || Math.abs(from - to) < 1 || reducedMotion()) return;
++ node.dataset.morph = "";
++ const animation = node.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, id: "bt-morph" });
++ animation.onfinish = () => {
++ delete node.dataset.morph;
++ };
++ animation.oncancel = () => {
++ if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph;
++ };
++ });
++}
++function useLinger(value, ms) {
+ const [kept, setKept] = (0, import_react3.useState)(value);
+ (0, import_react3.useEffect)(() => {
- if (value) {
- setKept(value);
- return void 0;
-@@ -1838,6 +1838,7 @@
++ 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)}
+@@ -1838,6 +1838,7 @@ ${Object.entries(INKS).map(([name, [, dark]]) => `body[data-ds-dark-theme]{--bt-
.bt-mark{display:inline-block;flex:none;position:relative;line-height:0}
.bt-mark svg{width:100%;height:100%;overflow:visible}
.bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)}
@@ -2475,7 +2815,7 @@ diff --git a/client.js b/client.js
.bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box}
.bt-mark .bt-b{transform-origin:50% 100%}
.bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center}
-@@ -1856,6 +1856,14 @@
+@@ -1856,6 +1856,14 @@ body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=loo
@keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}}
body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite}
body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none}
@@ -2490,7 +2830,7 @@ diff --git a/client.js b/client.js
@keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}}
/* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its
size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes
-@@ -1933,15 +1933,23 @@
+@@ -1933,15 +1933,23 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default}
.bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
.bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0}
.bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap}
@@ -2516,7 +2856,7 @@ diff --git a/client.js b/client.js
.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,21 +1950,23 @@
+@@ -1950,21 +1950,23 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default}
.bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto}
.bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400}
.bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@@ -2544,7 +2884,7 @@ diff --git a/client.js b/client.js
.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}}
-@@ -1992,6 +1992,7 @@
+@@ -1992,6 +1992,7 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default}
.bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0}
.bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere}
@@ -2552,7 +2892,7 @@ diff --git a/client.js b/client.js
.bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px}
.bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px}
.bt-bubble [class*="_markdown"]>*+*{margin-top:10px}
-@@ -2085,6 +2085,7 @@
+@@ -2085,6 +2085,7 @@ button.bt-q-opt:active{background:var(--bt-active)}
@media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}}
.bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out}
@@ -2560,7 +2900,7 @@ diff --git a/client.js b/client.js
.bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box}
.bt-to-label{color:var(--bt-ink-2)}
.bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px}
-@@ -2098,7 +2098,9 @@
+@@ -2098,7 +2098,9 @@ button.bt-q-opt:active{background:var(--bt-active)}
.bt-option[aria-selected=true]{background:var(--bt-active)}
.bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)}
.bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@@ -2571,7 +2911,7 @@ diff --git a/client.js b/client.js
.bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none}
.bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)}
.bt-nc-spacer{flex:1}
-@@ -2127,6 +2127,10 @@
+@@ -2127,6 +2127,10 @@ button.bt-q-opt:active{background:var(--bt-active)}
.bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer}
.bt-send:disabled{opacity:.35;cursor:default}
.bt-error{padding:0 14px 10px;color:#e02135;font-size:12px}
@@ -2582,7 +2922,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}
-@@ -2163,6 +2163,8 @@
+@@ -2163,6 +2163,8 @@ button.bt-q-opt:active{background:var(--bt-active)}
.bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0}
.bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px}
.bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -2591,7 +2931,7 @@ diff --git a/client.js b/client.js
.bt-drawer-name:hover{background:var(--bt-hover)}
.bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500}
.bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center}
-@@ -2255,6 +2255,7 @@
+@@ -2255,6 +2255,7 @@ body[data-ds-dark-theme] .bt-scrim{background:rgba(0,0,0,.5)}
.bt-settings-nav>.bt-thumb{left:8px;right:8px;border-radius:8px;background:var(--bt-active)}
.bt-settings-nav[data-glide] .bt-settings-item[aria-current=page]{background:transparent}
.bt-settings-shell{margin-top:auto}
@@ -2599,7 +2939,7 @@ diff --git a/client.js b/client.js
/* Usage while another page is open: out of the flow at Usage's own width, so it keeps
its layout, and not rendered. */
.bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none}
-@@ -2288,6 +2288,8 @@
+@@ -2288,6 +2288,8 @@ button.bt-set-row:hover{background:var(--bt-hover)}
.bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)}
.bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px}
.bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px}
@@ -2608,7 +2948,7 @@ diff --git a/client.js b/client.js
.bt-set-copy{flex:1;display:flex;flex-direction:column;gap:2px;min-width:0}
.bt-set-hint{font-size:12px;line-height:16px;color:color-mix(in srgb,var(--bt-ink-2) 50%,var(--bt-ink-3))}
.bt-set-control{flex:none;display:flex;align-items:center;gap:6px}
-@@ -2308,8 +2308,17 @@
+@@ -2308,8 +2308,17 @@ button.bt-set-row:hover{background:var(--bt-hover)}
.bt-connector-actions{flex:none;display:flex;gap:6px}
.bt-connector-form{display:flex;flex-direction:column;gap:8px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out}
.bt-connector-form-row{display:flex;align-items:center;gap:6px}
@@ -2626,7 +2966,7 @@ diff --git a/client.js b/client.js
/* Shell chrome reshaped for the team view. Selectors use stable data attributes where the
shell offers them and CSS-module name suffixes elsewhere. */
-@@ -2427,7 +2427,8 @@
+@@ -2427,7 +2427,8 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side
.bt-palette-empty-label{font-size:14px;font-weight:500;color:var(--bt-ink-2)}
.bt-palette-empty-hint{font-size:13px;line-height:16px;color:var(--bt-ink-3)}
@media (prefers-reduced-motion:reduce){.bt-palette{animation:bt-fade .1s ease-out}}
@@ -2636,7 +2976,7 @@ 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
-@@ -2599,6 +2599,14 @@
+@@ -2599,6 +2599,14 @@ var CONVERSATION_CSS = `
.bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)}
.bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
.bt-think summary::-webkit-details-marker{display:none}
@@ -2651,7 +2991,7 @@ diff --git a/client.js b/client.js
.bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)}
.bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap}
.bt-event>span{padding:0 2px}
-@@ -2612,7 +2612,6 @@
+@@ -2612,7 +2612,6 @@ var CONVERSATION_CSS = `
.bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap}
[class*="_scroll"]>[class*="_column"]::before{content:var(--bt-first-sep,none);display:block;flex:none;height:28px;margin:17px 0 12px;font-size:12px;line-height:28px;text-align:center;color:var(--bt-ink-2);white-space:nowrap}
@@ -2659,7 +2999,7 @@ diff --git a/client.js b/client.js
.bt-group-replies{display:contents}
/* A Bot chat is flat in every work-details mode, without changing the user's mode
(it is a DSH-wide setting): step groups lose their header and stay open, and what a
-@@ -2622,21 +2622,23 @@
+@@ -2622,21 +2622,23 @@ var CONVERSATION_CSS = `
[data-step-process]>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important}
:is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important}
[class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important}
@@ -2691,7 +3031,7 @@ diff --git a/client.js b/client.js
[data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important}
[data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important}
[data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important}
-@@ -2648,6 +2648,9 @@
+@@ -2648,6 +2648,9 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
[data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important}
.bt-typing{display:flex;align-items:center;gap:12px;height:36px;min-width:0;padding-inline-start:8px;transform-origin:0 50%;animation:bt-typing-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards}
.bt-typing[data-exiting]{animation:bt-typing-out .14s cubic-bezier(.22,1,.36,1) forwards}
@@ -2701,7 +3041,7 @@ diff --git a/client.js b/client.js
.bt-typing>.bt-mark,.bt-typing>.bt-cluster{flex-shrink:0}
.bt-typing>.bt-mark{cursor:pointer;transition:filter .16s ease-out}
.bt-typing>.bt-mark:hover{filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))}
-@@ -2679,7 +2679,8 @@
+@@ -2679,7 +2679,8 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
@keyframes bt-fade-in{from{opacity:0}to{opacity:1}}
@keyframes bt-fade-out{from{opacity:1}to{opacity:0}}
[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important}
@@ -2711,7 +3051,7 @@ diff --git a/client.js b/client.js
[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important}
[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important}
[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important}
-@@ -2691,6 +2691,8 @@
+@@ -2691,6 +2691,8 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important}
[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important}
[data-composer-placeholder]{font-size:0!important;line-height:0!important}
@@ -2720,7 +3060,7 @@ diff --git a/client.js b/client.js
[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)}
/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */
[data-composer-card] button[class*="_add"]{display:none!important}
-@@ -2722,6 +2722,7 @@
+@@ -2722,6 +2722,7 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%}
.bt-mic:active,.bt-voice:active{scale:.9}
[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid}
body[data-bt-room] .bt-voice{display:none!important}
@@ -2728,7 +3068,7 @@ diff --git a/client.js b/client.js
body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)}
.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out}
[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2}
-@@ -2776,6 +2776,7 @@
+@@ -2776,6 +2776,7 @@ div:has(> nav [class*="_marks"]){display:none!important}
.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none}
.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px}
.bt-urow{display:flex;justify-content:flex-end}
@@ -2736,100 +3076,99 @@ diff --git a/client.js b/client.js
.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere}
.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important}
.bt-ububble p{margin:0;line-height:22px}
-@@ -2855,226 +2855,203 @@
- };
+@@ -2818,1962 +2818,3620 @@ function compileFragmentSelectors(text, names) {
+ return compiled.get(fragment);
+ });
}
-
--// src/client/mark.js
--var import_react2 = require("react");
--var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"];
--var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle";
--var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working";
--function useGaze(ref, enabled) {
-- (0, import_react2.useEffect)(() => {
-- if (!enabled) return void 0;
-- let frame = 0;
-- let point = null;
-- const place = () => {
-- frame = 0;
-- const node = ref.current;
-- if (!node || !point) return;
-- const box = node.getBoundingClientRect();
-- const clamp = (value) => Math.max(-1, Math.min(1, value));
-- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2));
-- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2));
-- };
-- const onMove = (event) => {
-- point = { x: event.clientX, y: event.clientY };
-- if (!frame) frame = requestAnimationFrame(place);
-- };
-- const onLeave = () => {
-- point = null;
-- ref.current?.style.setProperty("--bt-gx", "0");
-- ref.current?.style.setProperty("--bt-gy", "0");
-- };
-- window.addEventListener("pointermove", onMove, { passive: true });
-- document.documentElement.addEventListener("pointerleave", onLeave);
-- return () => {
-- cancelAnimationFrame(frame);
-- window.removeEventListener("pointermove", onMove);
-- document.documentElement.removeEventListener("pointerleave", onLeave);
-- };
-- }, [enabled]);
+-function installCompiledStyles(text, tag) {
+- const style = installStyles("", tag);
+- const cache = /* @__PURE__ */ new WeakMap();
+- const render = () => {
+- const names = /* @__PURE__ */ new Set();
+- for (const sheet of document.styleSheets) {
+- if (sheet.ownerNode?.dataset?.dshBot) continue;
+- let list = cache.get(sheet);
+- if (!list) {
+- list = sheetClassNames(sheet);
+- cache.set(sheet, list);
+- }
+- for (const name of list) names.add(name);
+- }
+- const next = compileFragmentSelectors(text, names);
+- if (style.textContent !== next) style.textContent = next;
+- };
+- render();
+- let timer;
+- const schedule = () => {
+- clearTimeout(timer);
+- timer = setTimeout(render, 120);
+- };
+- const onLoad = (event) => {
+- if (event.target instanceof HTMLLinkElement) schedule();
+- };
+- const observer = new MutationObserver(schedule);
+- observer.observe(document.head, { childList: true });
+- document.addEventListener("load", onLoad, true);
+- return () => {
+- observer.disconnect();
+- document.removeEventListener("load", onLoad, true);
+- clearTimeout(timer);
+- style.remove();
+- };
++function 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();
++ };
++}
++
+// src/shared/face-title.js
+var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+function isGenericFaceTitle(name) {
+ const n = String(name ?? "").trim();
+ return !n || n === "Face-to-face room";
- }
--var FIDGETS = ["hop", "sway", "stretch", "look"];
--function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) {
-- const look = given ?? lookOf(bot);
-- const ref = (0, import_react2.useRef)(null);
-- const maskId = (0, import_react2.useRef)("");
-- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`;
-- const [poked, setPoked] = (0, import_react2.useState)(false);
-- useGaze(ref, gaze);
-- const common = {
-- ref,
-- className: poked ? "bt-mark bt-mark-poke" : "bt-mark",
-- "data-shape": look.image ? "image" : shapeId(look.shape),
-- "data-state": STATES.includes(state) ? state : "idle",
-- "data-live": live ? "" : void 0,
-- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0,
-- "data-gaze": gaze ? "" : void 0,
-- "data-poke": pokeable ? "" : void 0,
-- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} },
-- role: label ? "img" : void 0,
-- "aria-label": label,
-- "aria-hidden": label ? void 0 : true,
-- onClick: pokeable ? () => setPoked(true) : void 0,
-- onAnimationEnd: poked ? (event) => {
-- if (/^bt-poke/.test(event.animationName)) setPoked(false);
-- } : void 0
-- };
-- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false }));
-- const box = shapeOf(look.shape).size;
-- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } }));
++}
+function formatFaceWhen(value) {
+ const date = value instanceof Date ? value : new Date(value);
+ if (!Number.isFinite(date.getTime())) return "";
+ const hh = String(date.getHours()).padStart(2, "0");
+ const mm = String(date.getMinutes()).padStart(2, "0");
+ return `${MONTHS[date.getMonth()]} ${date.getDate()} ${hh}:${mm}`;
- }
--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 isAutoFaceTitle(name) {
+ const n = String(name ?? "").trim();
+ return isGenericFaceTitle(n) || n.startsWith("Face-to-face · ");
- }
--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 ownersFromMembers(members) {
+ const rows = [];
+ const seen = /* @__PURE__ */ new Set();
@@ -2843,13 +3182,7 @@ diff --git a/client.js b/client.js
+ }
+ rows.sort((a, b) => a.sort.localeCompare(b.sort));
+ return rows.slice(0, 3).map((row) => row.owner);
- }
--function 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 uniqueOwners(owners) {
+ const seen = /* @__PURE__ */ new Set();
+ const out = [];
@@ -2862,53 +3195,17 @@ diff --git a/client.js b/client.js
+ }
+ out.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
+ return out.slice(0, 3);
- }
--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 joinCodeOf(code) {
+ return String(code ?? "").replace(/\D/g, "").slice(0, 6);
- }
--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 FACE_JOIN_WINDOW_MS = 10 * 6e4;
+function faceRoomCreatedAt({ createdAt, expiresAt, startedAt } = {}) {
+ if (createdAt && Number.isFinite(Date.parse(createdAt))) return createdAt;
+ const deadline = Date.parse(expiresAt);
+ if (Number.isFinite(deadline)) return new Date(deadline - FACE_JOIN_WINDOW_MS).toISOString();
+ return startedAt || void 0;
- }
--function 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 formatFaceRoomTitle({ name, owners, members, code, at } = {}) {
+ const named = String(name ?? "").trim();
+ if (named && !isAutoFaceTitle(named)) return named;
@@ -2941,25 +3238,11 @@ diff --git a/client.js b/client.js
+ const room2 = roster.roomsById?.[sessionId];
+ if (bot?.remote) {
+ return friendBotTitle(bot);
- }
-- const half = frame / 2;
-- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]];
++ }
+ if (room2 && room2.origin === "face-to-face" && (room2.face?.status === "closed" || room2.face?.status === "left") && isAutoFaceTitle(room2.name)) return "Face-to-face · closed";
+ if (room2) return roomTitle(room2);
+ return bot?.name || "";
- }
--function RoomAvatar({ room: room2, roster, size = 36 }) {
-- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean);
-- const people = members.length === 1 ? [members[0], null] : members;
-- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } });
-- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size });
-- if (size <= 20 && people.length > 1) {
-- return (0, import_react2.createElement)(
-- "span",
-- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } },
-- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)),
-- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null
-- );
++}
+function installTabTitle(sessions, roster, doc = globalThis.document) {
+ if (!doc || !sessions || !roster) return () => {
+ };
@@ -2989,25 +3272,7 @@ diff --git a/client.js b/client.js
+ link.rel = "icon";
+ link.type = "image/svg+xml";
+ document.head.appendChild(link);
- }
-- const seats = clusterSeats(Math.min(people.length, 4), size);
-- const overflow = people.length > 4 ? people.length - 3 : 0;
-- const gap = size / 14;
-- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap }));
-- return (0, import_react2.createElement)(
-- "span",
-- { className: "bt-cluster", style: { width: size, height: size } },
-- seats.map(([x, y, s], index) => {
-- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`);
-- const style = { left: x, top: y, width: s, height: s };
-- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" });
-- if (overflow > 0 && index === 3) {
-- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`);
-- }
-- const bot = people[index];
-- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s));
-- })
-- );
++ }
+ link.href = href;
+ const retitle = () => {
+ const next = brandTitle(document.title);
@@ -3021,127 +3286,115 @@ diff --git a/client.js b/client.js
+ observer?.disconnect();
+ link.remove();
+ };
- }
-
- // src/client/icons.js
--var import_react3 = require("react");
++}
++
++// src/client/icons.js
+var import_react4 = require("react");
- function PlusIcon() {
-- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
++function PlusIcon() {
+ return (0, import_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_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
++}
++function SearchIcon() {
+ return (0, import_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_react3.createElement)(
++}
++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_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }),
-- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }),
-- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" })
++ "svg",
++ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true },
+ (0, import_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_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }));
++ );
++}
++function ArrowUpIcon() {
+ return (0, import_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_react3.createElement)(
++}
++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_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }),
-- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" })
++ "svg",
++ { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true },
+ (0, import_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_react3.createElement)(
++ );
++}
++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_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 }))
++ "svg",
++ { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
+ [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react4.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 }))
- );
- }
- function ShareIcon() {
-- return (0, import_react3.createElement)(
++ );
++}
++function ShareIcon() {
+ return (0, import_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_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" })
++ "svg",
++ { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+ (0, import_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_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
++ );
++}
++function CloseIcon() {
+ return (0, import_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_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_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_react3.createElement)(
++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_react3.createElement)("path", { key: index, d }))
++ "svg",
++ { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+ paths.map((d, index) => (0, import_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_react3.createElement)(
++ );
++};
++var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5";
++function CloudDownloadIcon({ done = false }) {
+ return (0, import_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_react3.createElement)("path", { d: CLOUD }),
-- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" }))
++ "svg",
++ { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+ (0, import_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"]);
-@@ -3093,14 +3093,14 @@
- var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]);
- var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]);
- function PinMarkIcon() {
-- return (0, import_react3.createElement)(
++ );
++}
++var 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_react3.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_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }),
-- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" })
++ "g",
++ { transform: "rotate(45 10 10)" },
+ (0, import_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" })
- )
- );
- }
-@@ -3112,21 +3112,48 @@
- var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]);
- var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]);
- var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]);
++ )
++ );
++}
++var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]);
++var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]);
++var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]);
++var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]);
++var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]);
++var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]);
++var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]);
++var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]);
+var LinkIcon = lineIcon(["M8.5 11.5a3.5 3.5 0 0 0 5 0l2.5-2.5a3.5 3.5 0 0 0-5-5l-1 1", "M11.5 8.5a3.5 3.5 0 0 0-5 0L4 11a3.5 3.5 0 0 0 5 5l1-1"]);
- var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]);
- var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]);
- var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]);
++var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]);
++var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]);
++var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]);
+var ChatIcon = lineIcon(["M4 5.5A1.5 1.5 0 0 1 5.5 4h9A1.5 1.5 0 0 1 16 5.5v6a1.5 1.5 0 0 1-1.5 1.5H9l-3.5 3v-3A1.5 1.5 0 0 1 4 11.5z"]);
+var BotIcon = lineIcon(["M6 7.5h8a2 2 0 0 1 2 2V14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9.5a2 2 0 0 1 2-2z", "M10 4.5v3", "M10 3.5h.01", "M7.75 11.5h.01M12.25 11.5h.01"]);
+var TerminalIcon = lineIcon(["M4.5 4.5h11a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14V6a1.5 1.5 0 0 1 1.5-1.5z", "M6.5 8.25L8.75 10 6.5 11.75", "M10.5 12h3"]);
- var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]);
- var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon };
++var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]);
++var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon };
+function FlyBotMark({ size = 24 }) {
+ return (0, import_react4.createElement)(
+ "svg",
@@ -3164,663 +3417,971 @@ diff --git a/client.js b/client.js
+ (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 })
+ );
-+}
+ }
- // src/client/questions.js
--var import_react4 = require("react");
+-// 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/questions.js
+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_react4.useRef)(onPick);
++var keyLetter = (index) => String.fromCharCode(65 + index);
++var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option);
++var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable;
++function useOptionKeys(count, onPick, ownRef) {
+ const pick2 = (0, import_react5.useRef)(onPick);
- pick2.current = onPick;
-- (0, import_react4.useEffect)(() => {
++ 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;
-@@ -3146,10 +3146,10 @@
- }, [count, ownRef]);
++ if (count === 0) return void 0;
++ const onKey = (event) => {
++ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return;
++ if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return;
++ if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return;
++ const index = event.key.toUpperCase().charCodeAt(0) - 65;
++ if (index >= 0 && index < count) {
++ event.preventDefault();
++ pick2.current(index);
++ } else if (index === count && ownRef.current) {
++ event.preventDefault();
++ ownRef.current.focus();
++ }
+ };
+- }, [enabled]);
++ 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 [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();
-@@ -3167,7 +3167,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_react5.createElement)("textarea", {
- ref: ownRef,
- rows: 1,
- value: own,
-@@ -3186,7 +3186,7 @@
- }) : null;
- const rows = options.map((option, optionIndex) => {
- const selected = multi && picked.includes(option.label);
-- return (0, import_react4.createElement)(
-+ return (0, import_react5.createElement)(
- "button",
- {
- key: option.label,
-@@ -3196,36 +3196,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_react5.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)),
+-var FIDGETS = ["hop", "sway", "stretch", "look"];
+-function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) {
+- const look = given ?? lookOf(bot);
+- const ref = (0, import_react2.useRef)(null);
+- const maskId = (0, import_react2.useRef)("");
+- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`;
+- const [poked, setPoked] = (0, import_react2.useState)(false);
+- useGaze(ref, gaze);
+- const common = {
+- ref,
+- className: poked ? "bt-mark bt-mark-poke" : "bt-mark",
+- "data-shape": look.image ? "image" : shapeId(look.shape),
+- "data-state": STATES.includes(state) ? state : "idle",
+- "data-live": live ? "" : void 0,
+- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0,
+- "data-gaze": gaze ? "" : void 0,
+- "data-poke": pokeable ? "" : void 0,
+- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} },
+- role: label ? "img" : void 0,
+- "aria-label": label,
+- "aria-hidden": label ? void 0 : true,
+- onClick: pokeable ? () => setPoked(true) : void 0,
+- onAnimationEnd: poked ? (event) => {
+- if (/^bt-poke/.test(event.animationName)) setPoked(false);
+- } : void 0
++function 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] });
+ };
+- 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 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_react4.createElement)("span", { className: "bt-q-label" }, option.label),
-- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null
++ "span",
++ { className: "bt-q-body" },
+ (0, import_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_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : 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_react4.createElement)(
++ );
++ });
++ 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_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null,
-- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field)
++ "label",
++ { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 },
+ ownLetter ? (0, import_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_react4.createElement)(
++ ));
++ }
++ let foot = null;
++ if (raised && multi) {
+ foot = (0, import_react5.createElement)(
- "div",
- { className: "bt-q-foot" },
-- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")),
-- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit"))
++ "div",
++ { className: "bt-q-foot" },
+ (0, import_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_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")));
++ );
++ } else if ((raised || multi) && canSubmit) {
+ foot = (0, import_react5.createElement)("div", { className: "bt-q-foot" }, (0, import_react5.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")));
- }
-- return (0, import_react4.createElement)(
++ }
+ return (0, import_react5.createElement)(
- "form",
- {
- className: raised ? "bt-q bt-q-raised" : "bt-q",
-@@ -3235,24 +3235,24 @@
- submit();
- }
- },
-- (0, import_react4.createElement)(
++ "form",
++ {
++ className: raised ? "bt-q bt-q-raised" : "bt-q",
++ "aria-labelledby": titleId,
++ onSubmit: (event) => {
++ event.preventDefault();
++ submit();
++ }
++ },
+ (0, import_react5.createElement)(
- "div",
- { className: "bt-q-head" },
-- (0, import_react4.createElement)(
++ "div",
++ { className: "bt-q-head" },
+ (0, import_react5.createElement)(
- "div",
- { className: "bt-q-text" },
-- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question),
-- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null,
-- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null
++ "div",
++ { className: "bt-q-text" },
+ (0, import_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_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_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_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null,
-- field && !raised ? (0, import_react4.createElement)(
++ ),
+ 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_react4.createElement)("label", { className: "bt-q-field" }, field),
-- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null
++ "div",
++ { className: "bt-q-custom" },
+ (0, import_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
++ ) : null,
++ foot
);
-@@ -3262,29 +3262,29 @@
- const prompt = question?.question ?? "";
- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []];
- if (chosen.length === 0) {
-- return (0, import_react4.createElement)(
+ }
+-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 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_react4.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_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)),
-- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed"))
++ "div",
++ { className: "bt-q-head" },
+ (0, import_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"))
- )
- );
- }
-- return (0, import_react4.createElement)(
++ )
++ );
++ }
+ return (0, import_react5.createElement)(
- "div",
- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt },
-- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null,
-- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => {
++ "div",
++ { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt },
+ prompt ? (0, import_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_react4.createElement)(
++ 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_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null,
-- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)),
-- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon))
++ "div",
++ { key: `${label}-${index}`, className: "bt-q-opt" },
+ optionIndex >= 0 ? (0, import_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))
- );
- }))
++ );
++ }))
);
-@@ -3292,8 +3292,8 @@
- function QuestionCard({ matched }) {
- const pending = matched;
- const questions = pending.questions ?? [];
-- const [index, setIndex] = (0, import_react4.useState)(0);
-- const [answers, setAnswers] = (0, import_react4.useState)([]);
+ }
+-function 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;
++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) => {
-@@ -3305,10 +3305,10 @@
++ const question = questions[index];
++ if (question === void 0) return null;
++ const onAnswer = (answer) => {
++ const next = [...answers, { id: question.id, ...answer }];
++ if (index + 1 < questions.length) {
++ setAnswers(next);
++ setIndex(index + 1);
++ return;
}
- pending.answer({ answers: next });
- };
-- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true });
+- if (was === "idle" || was === "alert") return void 0;
+- setSettled(true);
+- const timer = setTimeout(() => setSettled(false), 1800);
+- return () => clearTimeout(timer);
+- }, [state]);
+- return settled;
++ pending.answer({ answers: next });
++ };
+ return (0, import_react5.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true });
}
- function PendingQuestion({ sessionId, question, actions, inline }) {
-- const [answered, setAnswered] = (0, import_react4.useState)(null);
+-function 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 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);
-@@ -3318,10 +3318,10 @@
- setAnswered(question.id);
- void actions.dismissQuestion(sessionId);
- };
-- return (0, import_react4.createElement)(
++ if (answered === question.id) return null;
++ const onAnswer = (answer) => {
++ setAnswered(question.id);
++ void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null));
++ };
++ const onDismiss = () => {
++ setAnswered(question.id);
++ void actions.dismissQuestion(sessionId);
++ };
+ return (0, import_react5.createElement)(
- "div",
- { className: inline ? "bt-q-inline" : "bt-q-dock" },
-- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline })
++ "div",
++ { className: inline ? "bt-q-inline" : "bt-q-dock" },
+ (0, import_react5.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline })
);
}
- function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) {
-@@ -3329,16 +3329,16 @@
- 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 clusterSeats(count, frame) {
+- if (count === 1) return [[0, 0, frame]];
+- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]];
+- if (count === 3) {
+- const size = frame * 5 / 9;
+- const rest = frame - size;
+- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]];
+- }
+- const half = frame / 2;
+- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]];
++function 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_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null;
+-function RoomAvatar({ room: room2, roster, size = 36 }) {
+- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean);
+- const people = members.length === 1 ? [members[0], null] : members;
+- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } });
+- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size });
+- if (size <= 20 && people.length > 1) {
+- return (0, import_react2.createElement)(
+- "span",
+- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } },
+- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)),
+- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null
+- );
++function 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_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, {
+- 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));
+- })
+- );
++ 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: [] }
-@@ -3346,37 +3346,37 @@
++ key: question.id,
++ question,
++ answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] }
++ })));
}
- // src/client/secret-card.js
--var import_react5 = require("react");
+-// 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" }));
++// src/client/secret-card.js
+var import_react6 = require("react");
- var MIN_VALUE = 4;
- var NONE = {};
--var KeyIcon = () => (0, import_react5.createElement)(
++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_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }),
-- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" })
++ "svg",
++ { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+ (0, import_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_react5.createElement)(
++);
+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_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }),
-- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 })
++ "svg",
++ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+ (0, import_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_react5.createElement)(
++);
+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_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }),
-- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" })
++ "svg",
++ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+ (0, import_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 Bots");
++);
++var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc");
++var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname);
++var scopeLabel = (scope, roster) => {
+ if (scope === "all") return t("All my Bots");
- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean);
- return names.length === 0 ? t("No Bot") : names.join(t(" and "));
- };
- var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "";
- function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) {
-- return (0, import_react5.createElement)("input", {
++ const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean);
++ return names.length === 0 ? t("No Bot") : names.join(t(" and "));
++};
++var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "";
++function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) {
+ return (0, import_react6.createElement)("input", {
- className: "bt-secret-input",
- type: MASK_BY_CSS ? "text" : "password",
- "data-masked": MASK_BY_CSS ? "" : void 0,
-@@ -3401,12 +3401,12 @@
- });
++ className: "bt-secret-input",
++ type: MASK_BY_CSS ? "text" : "password",
++ "data-masked": MASK_BY_CSS ? "" : void 0,
++ value,
++ autoFocus,
++ autoComplete: MASK_BY_CSS ? "off" : "new-password",
++ autoCorrect: "off",
++ autoCapitalize: "off",
++ spellCheck: false,
++ "data-1p-ignore": "",
++ "data-lpignore": "true",
++ "data-bwignore": "",
++ "data-form-type": "other",
++ "aria-label": label,
++ placeholder: t("Paste or type the key"),
++ onChange: (event) => onChange(event.target.value),
++ onKeyDown: (event) => {
++ if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
++ event.preventDefault();
++ onEnter?.();
++ }
++ });
}
- function ScopeSwitch({ value, onChange, bot }) {
-- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]];
-- return (0, import_react5.createElement)(
+-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 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_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }),
-- options.map(([id, text]) => (0, import_react5.createElement)("button", {
++ "div",
++ { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") },
+ (0, import_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",
-@@ -3417,26 +3417,26 @@
++ key: id,
++ type: "button",
++ role: "radio",
++ "aria-checked": value === id,
++ className: "bt-secret-seg",
++ onClick: () => onChange(id)
++ }, text))
);
}
- function SecretHead({ bot, title, detail, name, onClose }) {
-- return (0, import_react5.createElement)(
+-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 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 SecretHead({ bot, title, detail, name, onClose }) {
+ return (0, import_react6.createElement)(
- "div",
- { className: "bt-q-head bt-secret-head" },
-- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))),
-- (0, import_react5.createElement)(
++ "div",
++ { className: "bt-q-head bt-secret-head" },
+ (0, import_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_react5.createElement)("p", { className: "bt-q-title" }, title),
-- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "")
++ "div",
++ { className: "bt-q-text" },
+ (0, import_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_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_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_react5.useState)("");
-- const [scope, setScope] = (0, import_react5.useState)("all");
-- const [replacing, setReplacing] = (0, import_react5.useState)(false);
-- const [busy, setBusy] = (0, import_react5.useState)(false);
-- const [error, setError] = (0, import_react5.useState)("");
-- const noteId = (0, import_react5.useId)();
+-function 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 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);
-@@ -3452,73 +3452,73 @@
- if (!busy) void run(actions.secretAllow(request.id));
- };
- const filling = request.kind === "fill" || replacing;
-- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null;
-- return (0, import_react5.createElement)(
++ const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name);
++ const run = (promise) => {
++ setBusy(true);
++ setError("");
++ return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false));
++ };
++ const cancel = () => run(actions.secretCancel(request.id));
++ const long = value.trim().length >= MIN_VALUE;
++ const save = () => {
++ if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope }));
++ };
++ const allow = () => {
++ if (!busy) void run(actions.secretAllow(request.id));
++ };
++ const filling = request.kind === "fill" || replacing;
+ const warn = insecure() ? (0, import_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_react5.createElement)(SecretHead, {
++ "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_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null,
-- filling ? (0, import_react5.createElement)(
++ bot,
++ title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }),
++ detail: request.purpose,
++ name: request.name,
++ onClose: busy ? void 0 : cancel
++ }),
+ request.kind === "allow" ? (0, import_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_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })),
-- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null
++ "div",
++ { className: "bt-secret-body" },
+ (0, import_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_react5.createElement)(
++ ) : 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_react5.createElement)("code", null, `$DSH_SECRET_${request.name}`),
++ "div",
++ { className: "bt-secret-note", id: noteId },
++ t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }),
+ (0, import_react6.createElement)("code", null, `$DSH_SECRET_${request.name}`),
- ".",
- filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : ""
- ),
- warn,
-- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null,
-- (0, import_react5.createElement)(
++ ".",
++ filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : ""
++ ),
++ warn,
+ error ? (0, import_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_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null,
-- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")),
-- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow"))
++ "div",
++ { className: "bt-q-foot" },
+ request.kind === "allow" && !replacing ? (0, import_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_react5.createElement)(
+-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 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_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_react6.createElement)(
- "div",
- { className: "bt-q-head" },
-- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })),
-- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)(
++ "div",
++ { className: "bt-q-head" },
+ (0, import_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_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`)
++ "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_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_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))
- )
++ )
);
}
- function RequestView({ bot, request, roster, actions }) {
-- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster });
+-function CloseIcon() {
+- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }));
++function 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_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions }));
+-var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" }));
+-var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" }));
+-var lineIcon = (paths) => function LineIcon() {
+- return (0, import_react3.createElement)(
+- "svg",
+- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d }))
+- );
+-};
+-var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5";
+-function CloudDownloadIcon({ done = false }) {
+- return (0, import_react3.createElement)(
+- "svg",
+- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+- (0, import_react3.createElement)("path", { d: CLOUD }),
+- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" }))
+- );
++function 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_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") })));
++ if (done && /already saved/.test(contentText(content))) {
+ return (0, import_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 null;
- }
-@@ -3531,44 +3531,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 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_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 })));
}
+-var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]);
+-var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]);
+-var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]);
+-var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]);
+-var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]);
+-var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]);
+-var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]);
+-var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]);
+-var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]);
+-var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]);
+-var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]);
+-var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]);
+-var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]);
+-var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]);
+-var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]);
+-function PinMarkIcon() {
+- return (0, import_react3.createElement)(
+- "svg",
+- { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
+- (0, import_react3.createElement)(
+- "g",
+- { transform: "rotate(45 10 10)" },
+- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }),
+- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" })
+var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some";
- function ScopeEditor({ secret, roster, onSave }) {
-- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some");
-- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope);
++function ScopeEditor({ secret, roster, onSave }) {
+ const [mode, setMode] = (0, import_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 same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join();
-- return (0, import_react5.createElement)(
++ const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]);
+ const next = mode === "all" ? "all" : mode === "none" ? [] : picked;
+ const same2 = next === "all" ? secret.scope === "all" : secret.scope !== "all" && [...next].sort().join() === [...secret.scope].sort().join();
+ return (0, import_react6.createElement)(
- "div",
- { className: "bt-secret-edit-block" },
-- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")),
-- (0, import_react5.createElement)(
++ "div",
++ { className: "bt-secret-edit-block" },
+ (0, import_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 Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)(
++ "div",
++ { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) },
+ [["all", t("All my Bots")], ["some", t("Chosen Bots")], ["none", t("No Bot")]].map(([id, text]) => (0, import_react6.createElement)(
- "label",
- { key: id, className: "bt-secret-radio" },
-- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }),
++ "label",
++ { key: id, className: "bt-secret-radio" },
+ (0, import_react6.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }),
- text
- ))
- ),
-- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", {
++ text
++ ))
++ ),
+ mode === "some" ? (0, import_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_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null,
-- (0, import_react5.createElement)(
++ key: bot.id,
++ type: "button",
++ className: "bt-secret-bot",
++ "aria-pressed": picked.includes(bot.id),
++ onClick: () => toggle(bot.id)
+ }, (0, import_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_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access"))
++ "div",
++ { className: "bt-secret-row-actions" },
+ (0, import_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_react5.useState)("");
-- const [replaced, setReplaced] = (0, import_react5.useState)(false);
+-var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]);
+-var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]);
+-var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]);
+-var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]);
+-var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]);
+-var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]);
+-var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]);
+-var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]);
+-var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]);
+-var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]);
+-var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]);
+-var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]);
+-var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon };
+-
+-// 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();
++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;
-@@ -3583,66 +3583,66 @@
- setValue(next);
- setReplaced(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);
+ }
+- };
+- 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] });
++ });
};
-- return (0, import_react5.createElement)(
+- const submit = () => {
+- if (!canSubmit) return;
+- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} });
++ const edit = (next) => {
++ setValue(next);
++ setReplaced(false);
+ };
+- 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)(
+ return (0, import_react6.createElement)(
- "div",
- { className: "bt-secret-edit-block" },
-- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")),
-- (0, import_react5.createElement)(
++ "div",
++ { className: "bt-secret-edit-block" },
+ (0, import_react6.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")),
+ (0, import_react6.createElement)(
- "div",
- { className: "bt-secret-inline" },
-- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })),
-- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace"))
++ "div",
++ { className: "bt-secret-inline" },
+ (0, import_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"))
- ),
-- 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_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
- );
- }
- function SecretRow({ secret, roster, actions, run, open, onToggle }) {
-- const [confirm, setConfirm] = (0, import_react5.useState)(false);
-- (0, import_react5.useEffect)(() => {
++ );
++}
++function SecretRow({ secret, roster, actions, run, open, onToggle }) {
+ const [confirm, setConfirm] = (0, import_react6.useState)(false);
+ (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(" · ");
-- return (0, import_react5.createElement)(
++ if (!open) setConfirm(false);
++ }, [open]);
++ const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you");
++ const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean);
++ const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · ");
+ return (0, import_react6.createElement)(
- "div",
- { className: "bt-secret-item", "data-open": open ? "" : void 0 },
-- (0, import_react5.createElement)(
++ "div",
++ { className: "bt-secret-item", "data-open": open ? "" : void 0 },
+ (0, import_react6.createElement)(
"button",
- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle },
-- (0, import_react5.createElement)(
+- {
+- 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)(
++ { 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_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)
+- { 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
++ { 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_react5.createElement)(
+- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null
+- );
+- });
+- if (raised && field) {
+- rows.push((0, import_react4.createElement)(
+- "label",
+- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 },
+- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null,
+- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field)
+- ));
+ (0, import_react6.createElement)(
- "span",
- { className: "bt-set-control" },
-- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false })))
++ "span",
++ { className: "bt-set-control" },
+ secret.scope === "all" ? (0, import_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_react5.createElement)(
++ )
++ ),
+ open ? (0, import_react6.createElement)(
- "div",
- { className: "bt-secret-edit" },
-- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }),
-- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }),
-- (0, import_react5.createElement)(
++ "div",
++ { className: "bt-secret-edit" },
+ (0, import_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)(
- "div",
- { className: "bt-secret-row-actions" },
- confirm ? [
-- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })),
-- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")),
-- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete"))
-- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete"))
++ "div",
++ { className: "bt-secret-row-actions" },
++ confirm ? [
+ (0, import_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_react5.useState)("");
-- const [purpose, setPurpose] = (0, import_react5.useState)("");
-- const [value, setValue] = (0, import_react5.useState)("");
++ )
++ ) : null
++ );
++}
++function AddSecret({ actions, run, onDone }) {
+ const [name, setName] = (0, import_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;
-@@ -3653,43 +3653,43 @@
- }
- });
- };
-- return (0, import_react5.createElement)(
++ const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE;
++ const save = () => {
++ if (!ready) return;
++ void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => {
++ if (ok) {
++ setValue("");
++ onDone();
++ }
++ });
++ };
+ return (0, import_react6.createElement)(
- "div",
- { className: "bt-set-row bt-set-paste bt-secret-add" },
-- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }),
-- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }),
-- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })),
-- (0, import_react5.createElement)(
++ "div",
++ { className: "bt-set-row bt-set-paste bt-secret-add" },
+ (0, import_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_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")),
-- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")),
-- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key"))
++ "div",
++ { className: "bt-secret-row-actions" },
+ (0, import_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_react5.useState)(null);
-- const [adding, setAdding] = (0, import_react5.useState)(false);
++ )
++ );
++}
++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_react5.createElement)(
++ 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_react5.createElement)("code", null, "$DSH_SECRET_NAME"),
++ "p",
++ { key: "about", className: "bt-secret-about" },
++ t("Keys your Bots asked for. Bots never see the values: they use them as "),
+ (0, import_react6.createElement)("code", null, "$DSH_SECRET_NAME"),
- t(" in their shell, and every value is replaced by "),
-- (0, import_react5.createElement)("code", null, "[secret:NAME]"),
++ t(" in their shell, and every value is replaced by "),
+ (0, import_react6.createElement)("code", null, "[secret:NAME]"),
- t(" before anything goes to a model.")
- ),
-- (0, import_react5.createElement)(
++ t(" before anything goes to a model.")
++ ),
+ (0, import_react6.createElement)(
- Section,
- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) },
-- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null,
-- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })),
-- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null
++ Section,
++ { key: "list", title: t("Secrets · {count}", { count: secrets.length }) },
+ secrets.length === 0 && !adding ? (0, import_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_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_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")))
- ];
- }
- var SECRET_CSS = `
-@@ -3756,6 +3756,144 @@
- @media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}}
- `;
-
++ ];
++}
++var SECRET_CSS = `
++.bt-secret-head{align-items:center;gap:10px}
++.bt-secret-who{position:relative;flex:none;display:inline-flex}
++.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)}
++.bt-secret-lock svg{width:11px;height:11px}
++.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)}
++.bt-secret-body{display:flex;flex-direction:column;gap:8px}
++.bt-secret-field{align-items:center;min-height:36px}
++.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em}
++.bt-secret-input[data-masked]{-webkit-text-security:disc}
++.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0}
++.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)}
++.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}}
++.bt-secret-glide[data-at="1"]{transform:translateX(100%)}
++.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease}
++.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500}
++.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px}
++/* A key card asks the user to act, so the process group carrying it stays open in every
++ work-details mode (same override as .bt-group-replies in styles.js). */
++[data-step-process]:has(.bt-secret)>div:first-child{display:none!important}
++[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important}
++.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)}
++.bt-secret-ok svg{width:13px;height:13px;color:#16a34a}
++@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}}
++.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)}
++.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)}
++.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px}
++.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)}
++.bt-secret-error{font-size:12px;line-height:16px;color:#e02135}
++.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default}
++.bt-secret-done .bt-q-head{align-items:center}
++.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500}
++.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)}
++.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards}
++.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards}
++@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}}
++@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}}
++.bt-secret-event{display:inline-flex;align-items:center;gap:5px}
++.bt-secret-dock{flex-direction:column;align-items:center}
++.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))}
++.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)}
++.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)}
++.bt-secret-title svg{flex:none;color:var(--bt-ink-2)}
++.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)}
++.bt-secret-faces{display:inline-flex}
++.bt-secret-faces>*+*{margin-left:-6px}
++.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards}
++.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px}
++.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)}
++.bt-secret-radios{display:flex;gap:16px;font-size:13px}
++.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer}
++.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px}
++.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease}
++.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)}
++.bt-secret-bot svg{width:13px;height:13px}
++.bt-secret-inline{display:flex;gap:8px;align-items:center}
++.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap}
++.bt-secret-row-actions .bt-set-hint{flex:1}
++.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px}
++.bt-secret-danger svg{width:13px;height:13px}
++.bt-secret-add .bt-input{height:34px}
++@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}}
++`;
++
+// src/client/face-seats.js
+function faceSeats(room2, roster, { left = false } = {}) {
+ const records = (room2.presentedParticipants ?? room2.participants ?? []).filter((p) => p.kind !== "human");
@@ -3840,7 +4401,17 @@ diff --git a/client.js b/client.js
+ const bot = roster.byId[id];
+ if (!bot || seats.some((item) => item.local?.id === id)) continue;
+ seats.push({ key: id, local: bot, record: { id }, person: cleanPeerName(roster.self?.displayName, ""), bot: bot.name, mine: true });
-+ }
+ }
+- let foot = null;
+- if (raised && multi) {
+- foot = (0, import_react4.createElement)(
+- "div",
+- { className: "bt-q-foot" },
+- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")),
+- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit"))
+- );
+- } else if ((raised || multi) && canSubmit) {
+- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")));
+ if (!left) return seats;
+ const gone = records.filter((p) => p.leftSeq !== void 0 && !seats.some((item) => item.key === (p.handle || p.id))).map((item) => ({ ...seat(item), left: true }));
+ return [...seats, ...gone];
@@ -3873,9 +4444,46 @@ diff --git a/client.js b/client.js
+ continue;
+ }
+ people2.push({ key, name: seat.person || (seat.mine ? cleanPeerName(roster.self?.displayName, "") : "") || t("Someone"), mine: seat.mine, seats: [seat] });
-+ }
+ }
+- 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
+- );
+ return [...people2.filter((person) => person.mine), ...people2.filter((person) => !person.mine)];
-+}
+ }
+-function SettledQuestion({ question, answer }) {
+- const options = optionsOf(question);
+- const prompt = question?.question ?? "";
+- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []];
+- if (chosen.length === 0) {
+- return (0, import_react4.createElement)(
+function facePersonLabel(person) {
+ return person.mine ? t("{name} (you)", { name: person.name }) : person.name;
+}
@@ -3930,17 +4538,37 @@ diff --git a/client.js b/client.js
+ if (!waiting) {
+ const failed = held.status === "failed";
+ return (0, import_react7.createElement)(
-+ "div",
+ "div",
+- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt },
+- (0, import_react4.createElement)(
+- "div",
+- { className: "bt-q-head" },
+- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)),
+- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed"))
+- )
+ { className: "bt-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")),
+ // #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
-+ );
-+ }
+ );
+ }
+- return (0, import_react4.createElement)(
+ const plain = (0, import_react7.createElement)(
-+ "div",
+ "div",
+- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt },
+- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null,
+- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => {
+- const optionIndex = options.findIndex((option) => option.label === label);
+- return (0, import_react4.createElement)(
+- "div",
+- { key: `${label}-${index}`, className: "bt-q-opt" },
+- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null,
+- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)),
+- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon))
+- );
+- }))
+ { className: "bt-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 })),
@@ -3951,209 +4579,442 @@ diff --git a/client.js b/client.js
+ (0, import_react7.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
-+ );
+ );
+-}
+-function QuestionCard({ matched }) {
+- const pending = matched;
+- const questions = pending.questions ?? [];
+- const [index, setIndex] = (0, import_react4.useState)(0);
+- const [answers, setAnswers] = (0, import_react4.useState)([]);
+- const question = questions[index];
+- if (question === void 0) return null;
+- const onAnswer = (answer) => {
+- const next = [...answers, { id: question.id, ...answer }];
+- if (index + 1 < questions.length) {
+- setAnswers(next);
+- setIndex(index + 1);
+- return;
+- }
+- pending.answer({ answers: next });
+- };
+- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true });
+-}
+-function PendingQuestion({ sessionId, question, actions, inline }) {
+- const [answered, setAnswered] = (0, import_react4.useState)(null);
+- if (answered === question.id) return null;
+- const onAnswer = (answer) => {
+- setAnswered(question.id);
+- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null));
+- };
+- const onDismiss = () => {
+- setAnswered(question.id);
+- void actions.dismissQuestion(sessionId);
+- };
+- return (0, import_react4.createElement)(
+ return (0, import_react7.createElement)(
-+ "div",
+ "div",
+- { className: inline ? "bt-q-inline" : "bt-q-dock" },
+- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline })
+ { className: "bt-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
-+ );
-+}
-+
- // src/client/turn-reasoning.js
- function turnReasoning(steps, current) {
- const members = steps.length ? steps : [current];
-@@ -3767,7 +3767,7 @@
- var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim();
+ );
+ }
+-function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) {
+- const question = useRoster((value) => value.questions?.[sessionId]);
+- const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true;
+- const loading = useSession?.(historyLoading) === true;
+- if (question === void 0 || question.callId && (drawn || loading)) return null;
+- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false });
+-}
+-function AnsweredCell({ node }) {
+- const data = node.data ?? {};
+- const questions = Array.isArray(data.questions) ? data.questions : [];
+- if (questions.length === 0) {
+- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null;
+- }
+- const answers = Array.isArray(data.answers) ? data.answers : [];
+- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, {
+- key: question.id,
+- question,
+- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] }
+- })));
+-}
- // src/client/cells.js
--var import_react6 = require("react");
+-// src/client/secret-card.js
+-var import_react5 = require("react");
+-var MIN_VALUE = 4;
+-var NONE = {};
+-var KeyIcon = () => (0, import_react5.createElement)(
+- "svg",
+- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }),
+- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" })
+-);
+-var LockIcon = () => (0, import_react5.createElement)(
+- "svg",
+- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }),
+- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 })
+-);
+-var WarnIcon = () => (0, import_react5.createElement)(
+- "svg",
+- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }),
+- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" })
+-);
+-var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc");
+-var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname);
+-var scopeLabel = (scope, roster) => {
+- if (scope === "all") return t("All Bots");
+- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean);
+- return names.length === 0 ? t("No Bot") : names.join(t(" and "));
+-};
+-var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "";
+-function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) {
+- return (0, import_react5.createElement)("input", {
+- className: "bt-secret-input",
+- type: MASK_BY_CSS ? "text" : "password",
+- "data-masked": MASK_BY_CSS ? "" : void 0,
+- value,
+- autoFocus,
+- autoComplete: MASK_BY_CSS ? "off" : "new-password",
+- autoCorrect: "off",
+- autoCapitalize: "off",
+- spellCheck: false,
+- "data-1p-ignore": "",
+- "data-lpignore": "true",
+- "data-bwignore": "",
+- "data-form-type": "other",
+- "aria-label": label,
+- placeholder: t("Paste or type the key"),
+- onChange: (event) => onChange(event.target.value),
+- onKeyDown: (event) => {
+- if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
+- event.preventDefault();
+- onEnter?.();
+- }
+- });
++// 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 ScopeSwitch({ value, onChange, bot }) {
+- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]];
+- return (0, import_react5.createElement)(
++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_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") });
-@@ -3780,7 +3780,7 @@
- actions.openSession(target);
- };
- const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null;
-- return (0, import_react6.createElement)(
++var import_react_dom = require("react-dom");
++var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
++var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") });
++function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) {
++ const onClick = (event) => {
++ const target = mentionTarget(event);
++ if (target === null) return;
++ event.preventDefault();
++ event.stopPropagation();
++ actions.openSession(target);
++ };
++ const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null;
+ return (0, import_react8.createElement)(
"div",
- { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick },
- texts2.map((text, index) => {
-@@ -3788,21 +3788,21 @@
- const picked = key ? reactions?.[index] ?? [] : [];
- const last = index === texts2.length - 1;
- const live = streaming && last;
-- return (0, import_react6.createElement)(
+- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") },
+- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }),
+- options.map(([id, text]) => (0, import_react5.createElement)("button", {
+- key: id,
+- type: "button",
+- role: "radio",
+- "aria-checked": value === id,
+- className: "bt-secret-seg",
+- onClick: () => onChange(id)
+- }, text))
++ { className: "bt-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_react6.createElement)(
++ "div",
++ { key: index, className: "bt-msg" },
+ (0, import_react8.createElement)(
- "div",
- { className: "bt-msg-line" },
- lead && last ? lead : null,
-- (0, import_react6.createElement)(
++ "div",
++ { className: "bt-msg-line" },
++ lead && last ? lead : null,
+ (0, import_react8.createElement)(
- "div",
- { className: "bt-bubble" },
-- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions })
++ "div",
++ { className: "bt-bubble" },
+ (0, import_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_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_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_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", {
++ ),
+ 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",
-@@ -3813,9 +3813,10 @@
- })
++ key: emoji,
++ type: "button",
++ className: "bt-react",
++ "aria-label": t("Remove reaction {emoji}", { emoji }),
++ onClick: () => actions.toggleReaction(msgKey, index, emoji)
++ }, emoji))) : null
++ );
++ })
);
}
+-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
+- );
+var WAITING_OTHERS_NOTICE = "Waiting for the owner's approval";
- var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"];
- function useDismiss(open, close, refs) {
-- (0, import_react6.useEffect)(() => {
++var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"];
++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;
-@@ -3836,11 +3836,11 @@
- }, [open]);
++ 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);
+-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));
++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) => {
-@@ -3865,25 +3865,25 @@
- link.click();
- setTimeout(() => URL.revokeObjectURL(url), 1e3);
++ 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 item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => {
+- 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 copy2 = () => {
++ void navigator.clipboard?.writeText(text).then(() => {
++ setCopied(true);
++ setTimeout(() => setCopied(false), 1e3);
++ });
+ };
+- const allow = () => {
+- if (!busy) void run(actions.secretAllow(request.id));
++ 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 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 item = (label, Icon, run) => (0, import_react8.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => {
- close();
- run();
-- } }, (0, import_react6.createElement)(Icon), label);
-- return (0, import_react6.createElement)(
++ close();
++ run();
+ } }, (0, import_react8.createElement)(Icon), label);
+ return (0, import_react8.createElement)(
"div",
- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 },
-- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null,
-- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null,
-- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)),
-- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)(
+- { className: "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)(
++ { 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_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => {
+- { 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)(
++ { 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_react6.createElement)(
++ 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),
-@@ -3892,11 +3892,12 @@
+- { 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"))
+- )
++ { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place },
++ item(t("Copy"), CopyIcon, copy2),
++ item(t("Download"), DownloadIcon, download2)
++ ), document.body) : null
);
}
- function ThinkingRow({ text }) {
-- return (0, import_react6.createElement)(
+-function SettledSecret({ bot, request, roster }) {
+- const canceled = request.status === "canceled";
+- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name });
+- return (0, import_react5.createElement)(
+- "div",
+- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status },
+- (0, import_react5.createElement)(
+- "div",
+- { className: "bt-q-head" },
+- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })),
+- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)(
+- "p",
+- { className: "bt-q-title" },
+- text,
+- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`)
+- )),
+- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon))
+- )
++function ThinkingRow({ text }) {
+ if (typeof text !== "string" || text.trim() === "") return null;
+ return (0, import_react8.createElement)(
- "details",
- { className: "bt-think" },
-- (0, import_react6.createElement)("summary", null, t("Thought it through")),
-- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text)
++ "details",
++ { className: "bt-think" },
+ (0, import_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)
);
}
- function useFileMentions({ node, openFile, fileMentions, useTurnData }) {
-@@ -3904,19 +3904,33 @@
- const location2 = node.location;
- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0;
- const seq = node.data.finalNode?.seq;
-- return (0, import_react6.useMemo)(() => {
+-function RequestView({ bot, request, roster, actions }) {
+- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster });
++function 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]);
++ 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 SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) {
+- const bot = roster.byId[sessionId];
+- if (!bot) return null;
+- const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId);
+- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions }));
+- if (done && /already saved/.test(contentText(content))) {
+- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") })));
+- }
+- return null;
+function 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 SecretDock({ sessionId, useRoster, useCards, useSession, actions }) {
+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 AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) {
++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 shown = useCards?.((state) => state[sessionId]) ?? NONE;
+- const loading = useSession?.(historyLoading) === true;
++ const data = node.data;
++ const steps = useTurnSteps(String(data.turn));
++ const contexts = useTurnContext(String(data.turn));
++ const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData });
+ const held = groupPart === "reasoning" ? void 0 : heldReplyOf(roster.byId[sessionId], sessionId, data.turn);
+ const closing = useClosingText(node, useTurnData);
- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn);
-- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text);
++ const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn);
+ const person = typedBy(roster, sessionId, data.turn);
+ const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block, index) => index === 0 ? stripTrace(block.text) : block.text).filter((text) => text.trim() !== "");
- const running = data.status === "running";
- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null);
- if (hidden) return null;
-@@ -3924,13 +3924,14 @@
- const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : [];
- if (texts2.length === 0 && !reasoning && relays.length === 0) return null;
- const selfId = roster.byId[sessionId]?.id ?? sessionId;
-- return (0, import_react6.createElement)(
-- import_react6.Fragment,
++ const running = data.status === "running";
++ useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null);
++ if (hidden) return null;
++ const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data);
++ const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : [];
++ if (texts2.length === 0 && !reasoning && relays.length === 0) return null;
++ const selfId = roster.byId[sessionId]?.id ?? sessionId;
+ const bubbles = texts2.length ? person ? (0, import_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_react6.createElement)(EventLine, {
++ null,
++ relays.map((relay) => {
++ const source = relay.source;
++ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
+ return (0, import_react8.createElement)(EventLine, {
- key: relay.seq,
- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"),
- bots: [sender],
-@@ -3940,61 +3940,230 @@
- whole: true
- });
- }),
-- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null,
-- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null
++ key: relay.seq,
++ prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"),
++ bots: [sender],
++ onOpen: () => {
++ if (sender.id) actions.openExchange(selfId, sender.id);
++ },
++ whole: true
++ });
++ }),
+ reasoning ? (0, import_react8.createElement)(ThinkingRow, { text: reasoning }) : null,
+ held && bubbles ? (0, import_react8.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles
- );
- }
++ );
++}
+function HeldReply({ held, roster, sessionId, actions, closing, children }) {
-+ const bot = roster.byId[sessionId];
+ 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 })));
+ 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)(
@@ -4162,55 +5023,68 @@ diff --git a/client.js b/client.js
+ children,
+ closing ? (0, import_react8.createElement)(HeldReplyActions, { held: { ...held, toName, from: bot?.name }, actions }) : null
+ );
-+}
+ }
+-function ScopeEditor({ secret, roster, onSave }) {
+- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some");
+- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope);
+- const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]);
+- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join();
+- return (0, import_react5.createElement)(
+function 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)(
-+ "div",
+ "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)(
+ { 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 })
+ );
+}
- function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) {
- if (whole && bots.length === 1) {
-- return (0, import_react6.createElement)(
++function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) {
++ if (whole && bots.length === 1) {
+ return (0, import_react8.createElement)(
"div",
- { className: "bt-event" },
-- (0, import_react6.createElement)(
+- { 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)(
++ { className: "bt-event" },
+ (0, import_react8.createElement)(
- "button",
- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) },
-- (0, import_react6.createElement)("span", null, prefix),
-- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }),
-- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name)
++ "button",
++ { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) },
+ (0, import_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)
- )
- );
- }
- const parts = [];
-- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix));
++ )
++ );
++ }
++ const parts = [];
+ parts.push((0, import_react8.createElement)("span", { key: "p" }, prefix));
- bots.forEach((bot, index) => {
-- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner));
-- parts.push((0, import_react6.createElement)(
++ bots.forEach((bot, index) => {
+ if (index > 0) parts.push((0, import_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_react6.createElement)(BotMark, { bot, size: 16 }),
++ "button",
++ { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) },
+ (0, import_react8.createElement)(BotMark, { bot, size: 16 }),
- bot.name
- ));
- });
-- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts);
++ bot.name
++ ));
++ });
+ return (0, import_react8.createElement)("div", { className: "bt-event" }, parts);
- }
--function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) {
-- const lead = (0, import_react6.createElement)(
++}
+var heldForm = (text) => String(text ?? "").replace(/\s+/g, " ").trim().replace(/^(to (the )?face[ -]?room|to the room)\s*:\s*/i, "").trim();
+var sameName = (a, b) => String(a ?? "").replace(/\s+/g, " ").trim().toLowerCase() === String(b ?? "").replace(/\s+/g, " ").trim().toLowerCase();
+function isHeldLine(room2, text, bot) {
@@ -4239,7 +5113,10 @@ diff --git a/client.js b/client.js
+ const held = heldLineState(room2, { seq, botId: bot.id, text });
+ if (held === "rejected") {
+ return (0, import_react8.createElement)(
-+ "div",
+ "div",
+- { className: "bt-secret-row-actions" },
+- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access"))
+- )
+ { className: "bt-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 }))
@@ -4247,12 +5124,10 @@ diff --git a/client.js b/client.js
+ }
+ 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_react6.createElement)(BotMark, { bot, size: 22 })
++ "button",
++ { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) },
+ (0, import_react8.createElement)(BotMark, { bot, size: 22 })
- );
-- return (0, import_react6.createElement)(
++ );
+ const name = (0, import_react8.createElement)(
+ "span",
+ { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } },
@@ -4260,13 +5135,28 @@ diff --git a/client.js b/client.js
+ room2?.admin === bot.id ? (0, import_react8.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null
+ );
+ return (0, import_react8.createElement)(
- "div",
- { className: "bt-group-msg" },
-- (0, import_react6.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 })
-+ );
-+}
+ );
+ }
+-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);
+function remoteSpeaker(source, roster, sessionId) {
+ const handle = source.remote.handle;
+ const member = (roster.remoteBots ?? []).find((bot2) => bot2.handle === handle);
@@ -4327,18 +5217,15 @@ diff --git a/client.js b/client.js
+ { className: "bt-group-msg bt-remote-msg", "data-remote": handle },
+ // A face room's header is the one name alone; the handle is on hover only (#457).
+ face ? (0, import_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)) } },
-- bot.name,
-- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null
++ "span",
++ { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } },
+ name,
+ (0, import_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_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead })
++ ),
+ (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 FirstContact({ source, roster, sessionId }) {
+ const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : "";
+ const who = person || remoteSpeaker(source, roster).name;
@@ -4372,9 +5259,19 @@ diff --git a/client.js b/client.js
+ } finally {
+ setBusy("");
+ }
-+ };
+ };
+- return (0, import_react5.createElement)(
+ return (0, import_react8.createElement)(
-+ "div",
+ "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
+ { className: "bt-file-chips" },
+ files.map((file) => (0, import_react8.createElement)(
+ "button",
@@ -4382,231 +5279,317 @@ diff --git a/client.js b/client.js
+ `📎 ${file.name} · ${fileSize(file.size)}`
+ )),
+ error ? (0, import_react8.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null
-+ );
-+}
- function GroupEvent({ prefix, room: room2, name, roster, actions }) {
-- return (0, import_react6.createElement)(
+ );
+ }
+-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)(
++function GroupEvent({ prefix, room: room2, name, roster, actions }) {
+ return (0, import_react8.createElement)(
"div",
- { className: "bt-event" },
-- (0, import_react6.createElement)("span", null, prefix),
-- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null
+- { className: "bt-secret-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
++ { 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
);
}
- function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) {
-@@ -4005,14 +4005,14 @@
- const target = other && other.id !== self?.id ? other : self;
- if (!target) return null;
- const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name });
-- return (0, import_react6.createElement)(
+-function 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();
+- }
++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_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label)
++ "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 PartLine({ time }) {
-- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null);
++ );
++}
++function PartLine({ time }) {
+ return (0, import_react8.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react8.createElement)("span", null, dayLabel(time)) : null);
- }
- var scrollParent = (node) => {
- for (let at = node.parentElement; at; at = at.parentElement) {
-@@ -4021,11 +4021,11 @@
- return null;
- };
- function EarlierParts({ sessionId, roster, actions }) {
-- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null });
-- const top = (0, import_react6.useRef)(null);
-- const busy = (0, import_react6.useRef)(false);
-- const alive = (0, import_react6.useRef)(true);
-- const restore = (0, import_react6.useRef)(null);
++}
++var scrollParent = (node) => {
++ for (let at = node.parentElement; at; at = at.parentElement) {
++ if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at;
++ }
++ return null;
++};
++function EarlierParts({ sessionId, roster, actions }) {
+ const [view, setView] = (0, import_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;
-@@ -4041,19 +4041,19 @@
- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true }));
++ const load = (cursor) => {
++ if (busy.current) return;
++ busy.current = true;
++ setView((current) => ({ ...current, loading: true, failed: false }));
++ actions.earlier(sessionId, cursor).then((page) => {
++ busy.current = false;
++ if (!alive.current) return;
++ const scroller = top.current && scrollParent(top.current);
++ if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop };
++ setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt }));
++ }, () => {
++ busy.current = false;
++ if (alive.current) setView((current) => ({ ...current, loading: false, failed: true }));
});
};
-- (0, import_react6.useEffect)(() => {
+- return (0, import_react5.createElement)(
+ (0, import_react8.useEffect)(() => {
- alive.current = true;
- load(void 0);
- return () => {
- alive.current = false;
- };
- }, [sessionId]);
-- (0, import_react6.useLayoutEffect)(() => {
++ alive.current = true;
++ load(void 0);
++ return () => {
++ alive.current = false;
++ };
++ }, [sessionId]);
+ (0, import_react8.useLayoutEffect)(() => {
- const pending = restore.current;
- restore.current = null;
- if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd;
- }, [view.items]);
-- (0, import_react6.useEffect)(() => {
++ const pending = restore.current;
++ restore.current = null;
++ if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd;
++ }, [view.items]);
+ (0, import_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) => {
-@@ -4069,35 +4069,35 @@
- if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text);
- else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item);
- }
-- return (0, import_react6.createElement)(
++ const node = top.current;
++ if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0;
++ const observer = new IntersectionObserver((entries2) => {
++ if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor);
++ }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" });
++ observer.observe(node);
++ return () => observer.disconnect();
++ }, [view.cursor, view.loading, view.failed]);
++ const selfId = roster.byId[sessionId]?.id;
++ const rows = [];
++ for (const item of view.items) {
++ const last = rows.at(-1);
++ if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text);
++ else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item);
++ }
+ return (0, import_react8.createElement)(
"div",
- { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading },
-- (0, import_react6.createElement)(
+- { 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)(
++ { 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_react6.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null
+- { className: "bt-secret-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"))
+- )
++ { 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_react6.createElement)(PartLine, { key, time: row.time });
++ ),
++ 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_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)(
++ case "user":
+ return (0, import_react8.createElement)("div", { key, className: "bt-urow" }, (0, import_react8.createElement)(
- "div",
- { className: "bt-ububble" },
-- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null,
-- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null
++ "div",
++ { className: "bt-ububble" },
+ row.text ? (0, import_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_react6.createElement)(AnsweredCell, { key, node: { data: row } });
++ ));
++ case "answer":
+ return (0, import_react8.createElement)(AnsweredCell, { key, node: { data: row } });
- case "bot":
-- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions });
++ case "bot":
+ return (0, import_react8.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions });
- case "event": {
-- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions });
++ case "event": {
+ if (row.role === "report") return (0, import_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_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => {
++ const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" };
++ const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from");
+ return (0, import_react8.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => {
- if (selfId && other.id) actions.openExchange(selfId, other.id);
- }, whole: true });
- }
-@@ -4105,7 +4105,7 @@
- return null;
- }
- }),
-- (0, import_react6.createElement)(PartLine, { time: view.startedAt })
++ if (selfId && other.id) actions.openExchange(selfId, other.id);
++ }, whole: true });
++ }
++ default:
++ return null;
++ }
++ }),
+ (0, import_react8.createElement)(PartLine, { time: view.startedAt })
);
}
- function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) {
-@@ -4116,7 +4116,7 @@
- const deferred = source.kind === "bot" && source.deferred === true;
- const relayed = isRelayContext(data);
- const turn = node.location?.turn?.turn ?? data.turn;
-- (0, import_react6.useLayoutEffect)(() => {
+-function 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")))
+- ];
++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) => {
-@@ -4135,23 +4135,31 @@
- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : "";
- });
- const body = triggerBody();
-- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body;
++ if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId);
++ }, [relayed, sessionId, turn, source.senderSessionId]);
++ const order = useSessions((list) => {
++ const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline;
++ return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : "";
++ });
++ const peers = useTurnClock((state) => {
++ if (!relayed) return "";
++ const turns = state[sessionId] ?? {};
++ return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(",");
++ });
++ const visit = useUi((state) => state.visits?.[sessionId]);
++ const hiddenTurns = roster.exchangeTurns[sessionId];
++ const unreadOpening = useTurnClock((state) => {
++ const first = openingTurn(state[sessionId], order, hiddenTurns);
++ return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : "";
++ });
++ const body = triggerBody();
+ return unreadOpening ? (0, import_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 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;
+ }
- if (source.kind === "bot") {
-- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions });
-- if (source.role === "kickoff" || source.role === "room") return null;
++ if (source.kind === "bot") {
+ if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react8.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 === "secret" || deferred) return null;
++ if (source.role === "post") {
++ if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null;
+ if (source.notice === true || source.senderSessionId === "face-notice") return null;
- const room2 = roster.roomsById[sessionId];
- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
-- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi });
++ const room2 = roster.roomsById[sessionId];
++ const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
+ return (0, import_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_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions });
++ }
++ if (source.role === "report") {
+ return (0, import_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_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_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)])),
-@@ -4165,7 +4165,7 @@
- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) {
- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer);
- }
-- return (0, import_react6.createElement)(EventLine, {
++ const run = relayed ? commRunOf(
++ order.split(",").filter(Boolean).map(Number),
++ Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])),
++ turn,
++ hiddenTurns
++ ) : null;
++ if (run === "folded") return null;
++ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
++ const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1);
++ const bots = [sender];
++ for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) {
++ if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer);
++ }
+ return (0, import_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),
-@@ -4174,24 +4174,24 @@
- }
- if (source.kind === "schedule") {
- const prompts = reminderPrompts(contentText(data.content));
-- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started"));
++ prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"),
++ bots,
++ onOpen: (bot) => actions.openExchange(selfId, bot.id),
++ whole: true
++ });
++ }
++ if (source.kind === "schedule") {
++ const prompts = reminderPrompts(contentText(data.content));
+ return (0, import_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_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update"));
++ }
++ if (source.kind === "user-question-reply") return null;
+ return (0, import_react8.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update"));
- }
- }
- function NewDivider() {
-- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New")));
++ }
++}
++function NewDivider() {
+ return (0, import_react8.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react8.createElement)("span", null, t("New")));
- }
- function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) {
- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open;
-- (0, import_react6.useEffect)(() => {
++}
++function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) {
++ const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open;
+ (0, import_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_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_react8.useEffect)(() => {
- actions.noteTurnTime(sessionId, turn, "first", start);
- }, [sessionId, turn, start]);
- const visit = useUi((state) => state.visits?.[sessionId]);
-@@ -4199,11 +4199,32 @@
- const own = state[sessionId]?.[turn];
- return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered;
- });
-- return fresh ? (0, import_react6.createElement)(NewDivider) : null;
++ actions.noteTurnTime(sessionId, turn, "first", start);
++ }, [sessionId, turn, start]);
++ const visit = useUi((state) => state.visits?.[sessionId]);
++ const fresh = useTurnClock((state) => {
++ const own = state[sessionId]?.[turn];
++ return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered;
++ });
+ return fresh ? (0, import_react8.createElement)(NewDivider) : null;
- }
- function Nothing() {
- return null;
- }
++}
++function Nothing() {
++ return null;
++}
+var HIDDEN_REMOVED_TOOLS = /* @__PURE__ */ new Set(["ask_user_question"]);
+var isConnectorTool = (name) => typeof name === "string" && /^mcp__(github|composio)__/.test(name);
+var isHiddenNotice = (block) => block?.type === "tool-removal" && (HIDDEN_REMOVED_TOOLS.has(block.toolName) || isConnectorTool(block.toolName) || isPluginTool(block.toolName)) || block?.type === "tool-addition" && (isConnectorTool(block.toolName) || isPluginTool(block.toolName));
@@ -4628,13 +5611,153 @@ diff --git a/client.js b/client.js
+ if (parts.length === 0) return null;
+ return (0, import_react8.createElement)("div", { className: "bt-event" }, parts.join(" · "));
+}
- var SEPARATOR_GAP_MS = 30 * 60 * 1e3;
- function clockLabel(time, now = Date.now()) {
- const date = new Date(time);
-@@ -4233,10 +4233,19 @@
- return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null;
++var SEPARATOR_GAP_MS = 30 * 60 * 1e3;
++function clockLabel(time, now = Date.now()) {
++ const date = new Date(time);
++ const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime();
++ const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5);
++ const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" });
++ if (days === 0) return t("Today {time}", { time: clock4 });
++ if (days === 1) return t("Yesterday {time}", { time: clock4 });
++ const sameYear = date.getFullYear() === new Date(now).getFullYear();
++ return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`;
++}
++function nextTurnStart(turns, turn) {
++ let next;
++ for (const key in turns) {
++ const candidate = Number(key);
++ if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate;
++ }
++ return next === void 0 ? void 0 : turns[next].first;
+ }
+-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 openingTurn(turns, firstTurns, hiddenTurns) {
++ if (!turns || !firstTurns) return null;
++ const start = nextTurnStart(turns, -Infinity);
++ if (start === void 0) return null;
++ const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]);
++ const listed = firstTurns.split(",").map(Number);
++ const earlier = listed.filter((entry) => entry < turn);
++ return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null;
}
- var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered;
+-var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "");
+-var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning");
+-var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim();
+-
+-// src/client/cells.js
+-var import_react6 = require("react");
+-var import_react_dom = require("react-dom");
+-var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
+-var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") });
+-function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) {
+- const onClick = (event) => {
+- const target = mentionTarget(event);
+- if (target === null) return;
+- event.preventDefault();
+- event.stopPropagation();
+- actions.openSession(target);
+- };
+- const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null;
+- return (0, import_react6.createElement)(
+- "div",
+- { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick },
+- texts2.map((text, index) => {
+- const key = msgKey ? `${msgKey}:${index}` : void 0;
+- const picked = key ? reactions?.[index] ?? [] : [];
+- const last = index === texts2.length - 1;
+- const live = streaming && last;
+- return (0, import_react6.createElement)(
+- "div",
+- { key: index, className: "bt-msg" },
+- (0, import_react6.createElement)(
+- "div",
+- { className: "bt-msg-line" },
+- lead && last ? lead : null,
+- (0, import_react6.createElement)(
+- "div",
+- { className: "bt-bubble" },
+- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions })
+- ),
+- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions })
+- ),
+- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", {
+- key: emoji,
+- type: "button",
+- className: "bt-react",
+- "aria-label": t("Remove reaction {emoji}", { emoji }),
+- onClick: () => actions.toggleReaction(msgKey, index, emoji)
+- }, emoji))) : null
+- );
+- })
++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 ?? {}) {
@@ -4644,105 +5767,320 @@ diff --git a/client.js b/client.js
+ return false;
+}
+var settledVisit = (visit) => typeof visit?.seen === "number" && visit.seen < visit.entered ? { ...visit, seen: visit.entered } : visit;
- function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) {
- const turn = location2.turn;
- const time = location2.end.time;
-- (0, import_react6.useEffect)(() => {
++function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) {
++ const turn = location2.turn;
++ const time = location2.end.time;
+ (0, import_react8.useEffect)(() => {
- actions.noteTurnTime(sessionId, turn, "end", time);
- }, [sessionId, turn, time]);
- const label = useTurnClock((state) => {
-@@ -4250,15 +4250,15 @@
- return start !== void 0 && start > visit.seen && start <= visit.entered;
- });
- if (!label && !fresh) return null;
-- return (0, import_react6.createElement)(
-- import_react6.Fragment,
++ actions.noteTurnTime(sessionId, turn, "end", time);
++ }, [sessionId, turn, time]);
++ const label = useTurnClock((state) => {
++ const start = nextTurnStart(state[sessionId] ?? {}, turn);
++ return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : "";
++ });
++ const visit = useUi((state) => state.visits?.[sessionId]);
++ const fresh = useTurnClock((state) => {
++ if (typeof visit?.seen !== "number" || time > visit.seen) return false;
++ const start = nextTurnStart(state[sessionId] ?? {}, turn);
++ return start !== void 0 && start > visit.seen && start <= visit.entered;
++ });
++ if (!label && !fresh) return null;
+ return (0, import_react8.createElement)(
+ import_react8.Fragment,
- null,
-- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null,
-- fresh ? (0, import_react6.createElement)(NewDivider) : null
++ null,
+ label ? (0, import_react8.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null,
+ fresh ? (0, import_react8.createElement)(NewDivider) : null
);
}
- function Shimmer({ children }) {
-- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children);
+-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 Shimmer({ children }) {
+ return (0, import_react8.createElement)("span", { className: "bt-shimmer" }, children);
}
- var PHRASES = {
- thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"],
-@@ -4320,19 +4320,22 @@
+-function 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;
++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" };
+ }
+- const rect = event.currentTarget.getBoundingClientRect();
+- const below = rect.bottom + 220 < window.innerHeight;
+- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } });
+- setPanel(kind);
+- };
+- const copy2 = () => {
+- void navigator.clipboard?.writeText(text).then(() => {
+- setCopied(true);
+- setTimeout(() => setCopied(false), 1e3);
+- });
+- };
+- const download2 = () => {
+- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" }));
+- const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` });
+- link.click();
+- setTimeout(() => URL.revokeObjectURL(url), 1e3);
+- };
+- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => {
+- close();
+- run();
+- } }, (0, import_react6.createElement)(Icon), label);
+- return (0, import_react6.createElement)(
+- "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
+- );
++ 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 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 workPhrase(tool) {
++ if (!tool) return t("Thinking about it");
+ return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool });
}
+-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 workText(tool, step) {
+ return step ? t("{action}: {detail}", { action: workPhrase(tool), detail: step }) : `${workPhrase(tool)}…`;
-+}
- function useLiveNote(actions, sessionId, id, entry) {
- const key = entry ? JSON.stringify(entry) : "";
-- (0, import_react6.useEffect)(() => {
+ }
+-function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) {
+- const roster = useRoster((value) => value);
+- const data = node.data;
+- const steps = useTurnSteps(String(data.turn));
+- const contexts = useTurnContext(String(data.turn));
+- const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData });
+- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn);
+- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text);
+- const running = data.status === "running";
+- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null);
+- if (hidden) return null;
+- const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data);
+- const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : [];
+- if (texts2.length === 0 && !reasoning && relays.length === 0) return null;
+- const selfId = roster.byId[sessionId]?.id ?? sessionId;
+- return (0, import_react6.createElement)(
+- import_react6.Fragment,
+- null,
+- relays.map((relay) => {
+- const source = relay.source;
+- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
+- return (0, import_react6.createElement)(EventLine, {
+- key: relay.seq,
+- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"),
+- bots: [sender],
+- onOpen: () => {
+- if (sender.id) actions.openExchange(selfId, sender.id);
+- },
+- whole: true
+- });
+- }),
+- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null,
+- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null
+- );
++function 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]);
++ 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 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)
+- )
+- );
+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;
-@@ -4341,10 +4341,10 @@
- const result = await actions.roomProgress(sessionId);
- if (stop) return;
- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0];
-- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : "";
++ if (done) return void 0;
++ let stop = false;
++ let timer;
++ let seen = "";
++ const tick = async () => {
++ const result = await actions.roomProgress(sessionId);
++ if (stop) return;
++ const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0];
+ const key = entry ? JSON.stringify(entry.replies) : "";
- if (entry && key !== seen) {
- seen = key;
-- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null });
++ if (entry && key !== seen) {
++ seen = key;
+ setLive({ replies: entry.replies ?? [] });
- }
- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3);
- };
-@@ -4362,71 +4362,88 @@
- 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 = [];
++ }
}
-- const pending = done || !live.speaking ? [] : [live.speaking];
-- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0;
-- if (replies.length === 0 && pending.length === 0) return null;
-+ if (replies.length === 0) return null;
- const room2 = roster.roomsById[sessionId];
+- const 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);
+-}
+-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)(
++ if (replies.length === 0) return null;
++ const room2 = roster.roomsById[sessionId];
+ return (0, import_react8.createElement)(
"div",
- { className: "bt-group-replies" },
- replies.map((reply, index) => {
-- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" };
+- { className: "bt-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 })
++ { className: "bt-group-replies" },
++ replies.map((reply, index) => {
+ const stored = roster.byId[reply.botId];
+ const resolved = reply.remote ? stored : rosterBot(roster, reply.botId) ?? stored;
+ const bot = resolved ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" };
- if (reply.kind === "user") {
-- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)(
++ if (reply.kind === "user") {
+ const who = reply.name && reply.name !== "User" ? reply.name : reply.authorId;
+ if (reply.other) {
+ return (0, import_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_react8.createElement)(
- "div",
-- { className: "bt-ububble" },
-- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" })
-- ));
++ "div",
+ { key: `user-${index}`, className: "bt-urow", "data-author": reply.authorId, "data-author-name": who },
+ who ? (0, import_react8.createElement)("span", { className: "bt-uauthor" }, who) : null,
+ (0, import_react8.createElement)(
@@ -4752,67 +6090,45 @@ diff --git a/client.js b/client.js
+ ),
+ 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
+ );
- }
- if (reply.kind === "failed") {
-- return (0, import_react6.createElement)(
++ }
++ if (reply.kind === "failed") {
+ return (0, import_react8.createElement)(
- "div",
-- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") },
++ "div",
+ { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status" },
- t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) })
- );
- }
- if (reply.kind === "lookup") {
-- return (0, import_react6.createElement)(
++ t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) })
++ );
++ }
++ if (reply.kind === "lookup") {
+ return (0, import_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_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name }));
++ "div",
++ { key: `${reply.botId}-${index}`, className: "bt-event" },
++ reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name })
++ );
++ }
++ if (reply.kind === "missed") {
+ return (0, import_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_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name }));
++ }
++ if (reply.kind === "resumed") {
+ return (0, import_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_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name }));
++ }
++ if (reply.kind === "cut") {
+ return (0, import_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_react6.createElement)(
++ }
++ if (reply.kind === "approval" || reply.kind === "working") {
+ return (0, import_react8.createElement)(
- "div",
- { key: `${reply.botId}-${index}`, className: "bt-event" },
-- (0, import_react6.createElement)(
++ "div",
++ { key: `${reply.botId}-${index}`, className: "bt-event" },
+ (0, import_react8.createElement)(
- "button",
- { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) },
-- (0, import_react6.createElement)(BotMark, { bot, size: 16 }),
-- (0, import_react6.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name }))
++ "button",
++ { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) },
+ (0, import_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_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name }));
++ )
++ );
++ }
++ if (reply.kind === "stopped") {
+ return (0, import_react8.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name }));
- }
-- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi });
-- }),
-- pending.map((id) => {
-- const bot = roster.byId[id];
-- if (!bot) return null;
-- return (0, import_react6.createElement)(
-- "div",
-- { key: `pending-${id}`, className: "bt-activity" },
-- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }),
-- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name }))
-- );
++ }
+ if (reply.kind === "notice" && room2?.origin === "face-to-face" && String(reply.text ?? "").trim() === WAITING_OTHERS_NOTICE) return null;
+ if (reply.kind === "notice" || reply.kind === "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));
@@ -4831,176 +6147,172 @@ diff --git a/client.js b/client.js
+ });
+ }
+ 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 });
- })
++ })
);
}
-@@ -4435,52 +4435,52 @@
- function callTree(root) {
- const calls = [];
- const seen = /* @__PURE__ */ new Set();
-- const visit = (call2) => {
-- const id = call2.callId ?? call2;
+-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
+- );
++var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]);
++var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]);
++function callTree(root) {
++ const calls = [];
++ const seen = /* @__PURE__ */ new Set();
+ const visit = (call4) => {
+ const id = call4.callId ?? call4;
- if (seen.has(id)) return;
- seen.add(id);
-- calls.push(call2);
-- for (const child of call2.subCalls ?? []) visit(child);
++ if (seen.has(id)) return;
++ seen.add(id);
+ calls.push(call4);
+ for (const child of call4.subCalls ?? []) visit(child);
- };
- visit(root);
- return calls;
++ };
++ visit(root);
++ return calls;
}
- function useShownCards(actions, sessionId, callIds) {
- const key = JSON.stringify(callIds);
-- (0, import_react6.useLayoutEffect)(() => {
+-function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) {
+- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null;
++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 renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) {
-- const tool = toolName(call2);
-- const done = call2.kind === "tool-result";
-- const { text: arg, value: argValue } = toolArgs(call2);
++ const ids = JSON.parse(key);
++ if (ids.length === 0 || !actions.noteCards) return void 0;
++ actions.noteCards(sessionId, ids, 1);
++ return () => actions.noteCards(sessionId, ids, -1);
++ }, [sessionId, key]);
++}
+function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) {
+ const tool = toolName(call4);
+ const done = call4.kind === "tool-result";
+ const { text: arg, value: argValue } = toolArgs(call4);
const self = roster.byId[sessionId];
- switch (tool) {
- case "message_bot": {
- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" };
-- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
-- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
+- const 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)
+- );
++ 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 && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
-- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
++ }
++ case "stop_bot": {
++ const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" };
+ if (done && call4.isError) return (0, import_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_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) });
++ }
++ case "create_bot": {
++ const created = findBot(arg("name"));
++ if (!done || created === void 0) return null;
+ return (0, import_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_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null;
++ }
++ case "update_bot": {
++ const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self);
+ return done && target ? (0, import_react8.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null;
- }
- case "group_relay":
-- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork });
++ }
++ case "group_relay":
+ return (0, import_react8.createElement)(GroupReplies, { sessionId, callId: call4.callId, done, content: call4.content, roster, actions, useUi });
- case "create_group":
- case "update_group":
- case "delete_group":
-@@ -4492,64 +4492,72 @@
- post_to_group: ["Posted in", "Couldn't post in the group"]
- }[tool];
- if (!done) return null;
-- const result = contentText(call2.content);
++ case "create_group":
++ case "update_group":
++ case "delete_group":
++ case "post_to_group": {
++ const verbs = {
++ create_group: ["Created group", "Couldn't create the group"],
++ update_group: ["Updated group", "Couldn't update the group"],
++ delete_group: ["Deleted group", "Couldn't delete the group"],
++ post_to_group: ["Posted in", "Couldn't post in the group"]
++ }[tool];
++ if (!done) return null;
+ const result = contentText(call4.content);
- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result);
-- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1]));
++ const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result);
+ if (call4.isError || !named) return (0, import_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_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions });
++ const name = named[2];
++ const room2 = roster.rooms.find((entry) => entry.name === name);
+ return (0, import_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 !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true });
-- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true });
++ }
++ case "ask_user": {
++ const question = roster.questions?.[sessionId];
+ if (!question || question.callId !== call4.callId) return (0, import_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_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions });
++ }
++ 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 {
-- parsed = JSON.parse(contentText(call2.content));
++ case "ask_user_question": {
++ if (!done) return null;
++ let parsed = null;
++ try {
+ parsed = JSON.parse(contentText(call4.content));
- } catch {
- parsed = null;
- }
- const answer = parsed?.answers?.[0];
- const question = argValue("questions")?.[0];
- if (!answer) return null;
-- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer }));
++ } catch {
++ parsed = null;
++ }
++ const answer = parsed?.answers?.[0];
++ const question = argValue("questions")?.[0];
++ if (!answer) return null;
+ return (0, import_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_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null;
++ }
++ case "read_group_chat": {
++ if (!done) return null;
++ const room2 = roster.rooms.find((entry) => entry.name === arg("group"));
+ return room2 ? (0, import_react8.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null;
- }
- case "remember":
- case "forget":
-- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null;
++ }
++ case "remember":
++ case "forget":
+ return done && !call4.isError ? (0, import_react8.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null;
- default:
- return null;
- }
- }
--function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) {
++ default:
++ return null;
++ }
++}
+function ToolCell({ node, sessionId, useRoster, useUi, actions }) {
- const roster = useRoster((value) => value);
- const root = node.data.root;
- const turn = node.location?.turn?.turn ?? root.turn;
- const hidden = isHiddenTurn(roster, sessionId, turn);
-- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase());
++ const roster = useRoster((value) => value);
++ const root = node.data.root;
++ const turn = node.location?.turn?.turn ?? root.turn;
++ const hidden = isHiddenTurn(roster, sessionId, turn);
+ const findBot = (nameOrId) => roster.byId[nameOrId] ?? botByName(roster.bots, nameOrId);
- const calls = hidden ? [] : callTree(root);
-- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2)));
-- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result");
++ const calls = hidden ? [] : callTree(root);
+ const drawn = calls.filter((call4) => DRAWN.has(toolName(call4)));
+ const running = root.kind === "tool-result" ? void 0 : calls.findLast((call4) => call4.kind !== "tool-result");
- const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null;
- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note);
-- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId));
++ const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null;
++ useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note);
+ useShownCards(actions, sessionId, drawn.filter((call4) => CARDS.has(toolName(call4)) && call4.callId).map((call4) => call4.callId));
- if (drawn.length === 0) return null;
-- const env = { sessionId, roster, actions, useUi, useWork, findBot };
-- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env))));
++ if (drawn.length === 0) return null;
+ const env = { sessionId, roster, actions, useUi, findBot };
+ return (0, import_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 PartLine({ time }) {
+- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null);
++
+// src/client/awaiting.js
+var AWAIT_REPLY_MS = 10 * 60 * 1e3;
+function awaitingReply(roster, sessionId, now = Date.now()) {
+ const at = roster?.remoteAwaiting?.[sessionId];
+ if (typeof at !== "number" || roster?.byId?.[sessionId]?.remote === void 0) return false;
+ return now - at < AWAIT_REPLY_MS;
-+}
+ }
+-var scrollParent = (node) => {
+- for (let at = node.parentElement; at; at = at.parentElement) {
+- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at;
+- }
+- return null;
+
- // src/client/activity-row.js
--var import_react7 = require("react");
++// 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))]);
-@@ -4559,9 +4559,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 import_react_dom2 = require("react-dom");
++var PHRASE_HOLD_MS = 800;
++var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]);
++function elapsedLabel(ms) {
++ const minutes = Math.floor(ms / 6e4);
++ if (minutes < 60) return `${Math.max(1, minutes)}m`;
++ const hours = Math.floor(minutes / 60);
++ return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`;
++}
+var RUNNING_HOST = "[data-chat-flow] > [data-chat-running]";
+var TAIL_HOST = "tail";
+var REMOTE_GRACE_MS = 1500;
@@ -5028,7 +6340,26 @@ diff --git a/client.js b/client.js
+ let top = -1;
+ for (const node of document.querySelectorAll(`[data-chat-flow-kind${kind ? `="${kind}"` : ""}][data-chat-turn]`)) top = Math.max(top, Number(node.dataset.chatTurn));
+ return top;
-+};
+ };
+-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 useReplyAfterSend(remote, sending) {
+ const sendTurn = (0, import_react9.useRef)(-1);
+ const wasSending = (0, import_react9.useRef)(false);
@@ -5053,58 +6384,225 @@ diff --git a/client.js b/client.js
+ };
+ const observer = new MutationObserver(() => {
+ if (!frame) frame = requestAnimationFrame(check);
-+ });
+ });
+- };
+- (0, import_react6.useEffect)(() => {
+- alive.current = true;
+- load(void 0);
+ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true });
+ check();
-+ return () => {
+ 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)(
+- "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 })
+- );
+ }, [remote, sending]);
+ return replied;
-+}
+ }
+-function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) {
+- const roster = useRoster((value) => value);
+- const data = node.data;
+- const source = data.source !== null && typeof data.source === "object" ? data.source : {};
+- const selfId = roster.byId[sessionId]?.id ?? sessionId;
+- const deferred = source.kind === "bot" && source.deferred === true;
+- const relayed = isRelayContext(data);
+- const turn = node.location?.turn?.turn ?? data.turn;
+- (0, import_react6.useLayoutEffect)(() => {
+- if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId);
+- }, [relayed, sessionId, turn, source.senderSessionId]);
+- const order = useSessions((list) => {
+- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline;
+- return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : "";
+- });
+- const peers = useTurnClock((state) => {
+- if (!relayed) return "";
+- const turns = state[sessionId] ?? {};
+- return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(",");
+- });
+- const visit = useUi((state) => state.visits?.[sessionId]);
+- const hiddenTurns = roster.exchangeTurns[sessionId];
+- const unreadOpening = useTurnClock((state) => {
+- const first = openingTurn(state[sessionId], order, hiddenTurns);
+- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : "";
+- });
+- const body = triggerBody();
+- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body;
+- function triggerBody() {
+- if (source.kind === "bot") {
+- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions });
+- if (source.role === "kickoff" || source.role === "room") return null;
+- if (source.role === "secret" || deferred) return null;
+- if (source.role === "post") {
+- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null;
+- const room2 = roster.roomsById[sessionId];
+- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
+- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi });
+- }
+- if (source.role === "report") {
+- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions });
+- }
+- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart"));
+- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text"));
+- const run = relayed ? commRunOf(
+- order.split(",").filter(Boolean).map(Number),
+- Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])),
+- turn,
+- hiddenTurns
+- ) : null;
+- if (run === "folded") return null;
+- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
+- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1);
+- const bots = [sender];
+- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) {
+- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer);
+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;
-@@ -4569,7 +4569,7 @@
- let frame = 0;
- const find = () => {
- frame = 0;
-- const node = document.querySelector("[data-chat-flow] > [data-chat-running]");
++ if (!active) {
++ setHost(null);
++ return void 0;
++ }
++ let frame = 0;
++ const find = () => {
++ frame = 0;
+ const node = findHost(where);
- setHost((prev) => prev === node ? prev : node);
- };
- const observer = new MutationObserver(() => {
-@@ -4581,13 +4581,13 @@
- cancelAnimationFrame(frame);
- observer.disconnect();
- };
-- }, [active]);
++ setHost((prev) => prev === node ? prev : node);
++ };
++ const observer = new MutationObserver(() => {
++ if (!frame) frame = requestAnimationFrame(find);
++ });
++ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true });
++ find();
++ return () => {
++ cancelAnimationFrame(frame);
++ observer.disconnect();
++ };
+ }, [active, where]);
- return host;
- }
- function useRowEntrances(replacing) {
-- const check = (0, import_react7.useRef)(replacing);
++ return host;
++}
++function useRowEntrances(replacing) {
+ const check = (0, import_react9.useRef)(replacing);
- check.current = replacing;
-- (0, import_react7.useEffect)(() => {
++ check.current = replacing;
+ (0, import_react9.useEffect)(() => {
- const seen = /* @__PURE__ */ new WeakSet();
- const started = Date.now();
- let column = null;
-@@ -4638,21 +4638,30 @@
- const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time)));
- const bot = roster.byId[sessionId];
- const room2 = roster.roomsById[sessionId];
-- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]);
++ const seen = /* @__PURE__ */ new WeakSet();
++ const started = Date.now();
++ let column = null;
++ let frame = 0;
++ const settle = (event) => {
++ if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return;
++ delete event.currentTarget.dataset.btEnter;
++ event.currentTarget.removeEventListener("animationend", settle);
++ };
++ const scan = () => {
++ frame = 0;
++ const next = document.querySelector("[data-chat-flow]");
++ if (next !== column) {
++ observer.disconnect();
++ column = next;
++ if (column) observer.observe(column, { childList: true, subtree: true });
+ }
+- 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 (!column) return;
++ const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")];
++ let last = -1;
++ items.forEach((item, index) => {
++ if (seen.has(item)) last = index;
+ });
++ const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0);
++ for (const item of fresh) seen.add(item);
++ if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return;
++ for (const item of fresh) {
++ if (items.indexOf(item) < last) continue;
++ const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind);
++ item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new";
++ item.addEventListener("animationend", settle);
++ }
++ };
++ const observer = new MutationObserver(() => {
++ if (!frame) frame = requestAnimationFrame(scan);
++ });
++ scan();
++ return () => {
++ cancelAnimationFrame(frame);
++ observer.disconnect();
++ };
++ }, []);
++}
++function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) {
++ const roster = useRoster((value) => value);
++ const running = useSessionStatus((map) => map.get(sessionId)?.running === true);
++ const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {}));
++ const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time)));
++ const bot = roster.byId[sessionId];
++ const room2 = roster.roomsById[sessionId];
+ const entries2 = (0, import_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);
-- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0);
++ const streaming = entries2.some((entry) => entry.text);
++ const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at);
++ const relaying = tools.some((entry) => entry.relay);
+ const awaiting = useAwaitingReply(roster, sessionId);
+ const sendRun = (0, import_react9.useRef)(false);
+ if (!running) sendRun.current = false;
@@ -5115,47 +6613,42 @@ diff --git a/client.js b/client.js
+ const bridging = bot?.remote !== void 0 && !streaming && lastRun.current.running && lastRun.current.send && !running;
+ lastRun.current = { running, send: sendRun.current };
+ const steady = bridging;
- const tool = tools.at(-1);
-- const inputRef = (0, import_react7.useRef)(0);
++ const tool = tools.at(-1);
+ const inputRef = (0, import_react9.useRef)(0);
- const inputAt = Math.max(inputRef.current, pendingInput);
- inputRef.current = inputAt;
- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" };
-- const [shown, setShown] = (0, import_react7.useState)(null);
++ const inputAt = Math.max(inputRef.current, pendingInput);
++ inputRef.current = inputAt;
++ const 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_react9.useState)(null);
- const wantKey = want?.key ?? null;
-- const markState = (0, import_react7.useRef)("thinking");
++ const wantKey = want?.key ?? null;
+ const markState = (0, import_react9.useRef)("thinking");
- if (want) markState.current = want.state;
-- (0, import_react7.useEffect)(() => {
++ 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;
-@@ -4663,24 +4663,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);
++ 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;
+ }
+- 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"));
++ 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_react7.useEffect)(() => {
++ leaving.current = phase !== null && !visible;
+ (0, import_react9.useEffect)(() => {
- if (visible) {
- setPhase("in");
- return void 0;
++ if (visible) {
++ setPhase("in");
++ return void 0;
}
-- setPhase((current) => current === "in" ? "out" : current);
-- const timer = setTimeout(() => {
-- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null;
-- setPhase((current) => current === "out" ? null : current);
-- }, 140);
+- 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"));
+- }
+ let timer;
+ const leave = () => {
+ setPhase((current) => current === "in" ? "out" : current);
@@ -5166,31 +6659,29 @@ diff --git a/client.js b/client.js
+ };
+ if (steady) timer = setTimeout(leave, REMOTE_GRACE_MS);
+ else leave();
- return () => clearTimeout(timer);
- }, [visible]);
-- (0, import_react7.useLayoutEffect)(() => {
++ return () => clearTimeout(timer);
++ }, [visible]);
+ (0, import_react9.useLayoutEffect)(() => {
- if (phase !== null) return;
- endedAt.current = Date.now();
- setShown(null);
-@@ -4691,31 +4691,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)(() => {
++ 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_react7.useState)(Date.now);
-- (0, import_react7.useEffect)(() => {
++ if (!running) actions.clearLive(sessionId);
++ }, [running, sessionId]);
++ useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300);
+ const [now, setNow] = (0, import_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]);
-- const host = useRunningHost(running);
++ if (phase !== "in") return void 0;
++ const timer = setInterval(() => setNow(Date.now()), 1e4);
++ return () => clearInterval(timer);
++ }, [phase]);
+ const remote = bot?.remote !== void 0;
+ const host = useRunningHost(running || remote && phase !== null, running || !remote ? RUNNING_HOST : TAIL_HOST);
+ const drawn = (0, import_react9.useRef)(false);
@@ -5198,34 +6689,54 @@ diff --git a/client.js b/client.js
+ (0, import_react9.useEffect)(() => {
+ drawn.current = Boolean(host && phase !== null && shown);
+ }, [host, phase, shown]);
- if (!host || phase === null || !shown) return null;
- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null;
-- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)(
++ if (!host || phase === null || !shown) return null;
++ const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null;
+ return (0, import_react_dom2.createPortal)((0, import_react9.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)(
++ "div",
+ { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : 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_react7.createElement)(
++ "span",
++ { className: "bt-typing-text" },
+ (0, import_react9.createElement)(
- "span",
- { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" },
-- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`),
-- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null
++ "span",
++ { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" },
+ (0, import_react9.createElement)(Shimmer, null, `${shown.text}…`),
+ elapsed ? (0, import_react9.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null
- )
- )
- ), host);
-@@ -4731,49 +4731,755 @@
- return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified);
++ )
++ )
++ ), host);
}
-
+-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")));
++
++// src/client/dsh-offer.js
++var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]);
++var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0];
++var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b);
++function showsDshRow(dsh, app) {
++ if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false;
++ if (dsh.kind === "npm") return true;
++ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified);
+ }
+-function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) {
+- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open;
+- (0, import_react6.useEffect)(() => {
+- if (!open) turnProcess.setOpen(true);
+- }, [open, turnProcess]);
+- const turn = node?.location?.turn?.turn ?? node?.data?.turn;
+- const start = node?.location?.turn?.start?.time;
+- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn));
+- (0, import_react6.useEffect)(() => {
+- actions.noteTurnTime(sessionId, turn, "first", start);
+- }, [sessionId, turn, start]);
+- const visit = useUi((state) => state.visits?.[sessionId]);
+- const fresh = useTurnClock((state) => {
+- const own = state[sessionId]?.[turn];
+- return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered;
+- });
+- return fresh ? (0, import_react6.createElement)(NewDivider) : null;
++
+// src/client/overlay-hooks.js
+var import_react10 = require("react");
+var escapeLayers = [];
@@ -5243,7 +6754,9 @@ diff --git a/client.js b/client.js
+ if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey);
+ };
+ }, []);
-+}
+ }
+-function Nothing() {
+- return null;
+function usePaneLeft() {
+ const [paneLeft, setPaneLeft] = (0, import_react10.useState)(280);
+ (0, import_react10.useLayoutEffect)(() => {
@@ -5251,7 +6764,17 @@ diff --git a/client.js b/client.js
+ if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right));
+ }, []);
+ return paneLeft;
-+}
+ }
+-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}`;
+
+// src/client/member-status.js
+var import_react11 = require("react");
@@ -5274,22 +6797,59 @@ diff --git a/client.js b/client.js
+ const approvals = askedIn(room2, work, id, doing);
+ return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" };
+ });
-+}
+ }
+-function nextTurnStart(turns, turn) {
+- let next;
+- for (const key in turns) {
+- const candidate = Number(key);
+- if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate;
+- }
+- return next === void 0 ? void 0 : turns[next].first;
+function 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 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 splitBusy(busy) {
+ const here = busy.filter((entry) => entry.state === "work" && entry.here);
+ const rows = [...here.filter((entry) => entry.doing.speaking === true), ...here.filter((entry) => entry.doing.speaking !== true)].slice(0, ROWS_MAX);
+ return { rows, rest: busy.filter((entry) => !rows.includes(entry)) };
-+}
+ }
+-var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered;
+-function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) {
+- const turn = location2.turn;
+- const time = location2.end.time;
+- (0, import_react6.useEffect)(() => {
+- actions.noteTurnTime(sessionId, turn, "end", time);
+- }, [sessionId, turn, time]);
+- const label = useTurnClock((state) => {
+- const start = nextTurnStart(state[sessionId] ?? {}, turn);
+- return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : "";
+- });
+- const visit = useUi((state) => state.visits?.[sessionId]);
+- const fresh = useTurnClock((state) => {
+- if (typeof visit?.seen !== "number" || time > visit.seen) return false;
+- const start = nextTurnStart(state[sessionId] ?? {}, turn);
+- return start !== void 0 && start > visit.seen && start <= visit.entered;
+function botGroupWork(botId, work, roster) {
+ return Object.entries(work.rooms ?? {}).flatMap(([roomId, entries2]) => {
+ const entry = entries2.find((item) => item.botId === botId);
+ const room2 = roster.roomsById[roomId];
+ return entry && room2 ? [{ room: room2, tool: entry.tool, step: stepIn(work, botId, roomId, entry.tool), since: entry.since }] : [];
-+ });
+ });
+- 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 summaryText(busy) {
+ const asks = busy.filter((entry) => entry.state === "ask");
@@ -5305,8 +6865,10 @@ diff --git a/client.js b/client.js
+ "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 ?? "") })
-+ );
-+}
+ );
+ }
+-function Shimmer({ children }) {
+- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children);
+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)}`;
@@ -5318,7 +6880,63 @@ diff --git a/client.js b/client.js
+ (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
+ );
-+}
+ }
+-var PHRASES = {
+- thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"],
+- working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"]
+-};
+-var TOOL_PHRASES = [
+- [/web_?search|search_?web/i, "Searching online"],
+- [/fetch|browse|browser|web/i, "Reading a page"],
+- [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"],
+- [/^(read|view|cat)\b|read_/i, "Opening a file"],
+- [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"],
+- [/write|edit|patch|apply|replace/i, "Writing changes"],
+- [/image|photo|draw/i, "Working on an image"]
+-];
+-var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" };
+-function toolActivity(name, arg, findBot, self) {
+- switch (name) {
+- case "ask_user":
+- return null;
+- case "request_secret":
+- return null;
+- case "list_secrets":
+- return { label: t("Checking the keys"), state: "searching" };
+- case "ask_user_question":
+- return { label: t("Waiting for your answer"), state: "alert" };
+- case "group_relay":
+- return { relay: true, state: "orbit" };
+- case "message_bot": {
+- const target = findBot(arg("to"));
+- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" };
+- }
+- case "stop_bot": {
+- const target = findBot(arg("bot"));
+- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" };
+- }
+- case "create_bot":
+- return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" };
+- case "update_bot": {
+- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self);
+- return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" };
+- }
+- case "list_bots":
+- return { label: t("Checking the team"), state: "searching" };
+- case "read_group_chat":
+- return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" };
+- case "read_own_chat":
+- return { label: t("Looking back through the chat"), state: "searching" };
+- case "remember":
+- return { label: t("Saving to memory"), state: "working" };
+- case "forget":
+- return { label: t("Updating memory"), state: "working" };
+- case "recall":
+- return { label: t("Checking memory"), state: "searching" };
+- default:
+- if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" };
+- const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1];
+- return { label: phrase ? t(phrase) : null, state: toolState(name) };
+function SummaryLine({ busy, now, onOpen }) {
+ const { waiting, doing } = summaryText(busy);
+ const elapsed = doing ? elapsedSince(now, busy.find((entry) => entry.state === "work")?.doing.since) : "";
@@ -5358,7 +6976,7 @@ diff --git a/client.js b/client.js
+ 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
+ );
-+ }
+ }
+ return (0, import_react11.createElement)("div", { ref, className: "bt-mw-dock-stack" }, groups.slice(0, ROWS_MAX).map((item) => (0, import_react11.createElement)(WorkRow, {
+ key: item.room.id,
+ bot,
@@ -5386,7 +7004,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]);
-+}
+ }
+-function workPhrase(tool) {
+- if (!tool) return t("Thinking about it");
+- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it");
+function useNow(active) {
+ const [now, setNow] = (0, import_react11.useState)(Date.now);
+ (0, import_react11.useEffect)(() => {
@@ -5395,7 +7016,14 @@ diff --git a/client.js b/client.js
+ return () => clearInterval(timer);
+ }, [active]);
+ return now;
-+}
+ }
+-function useLiveNote(actions, sessionId, id, entry) {
+- const key = entry ? JSON.stringify(entry) : "";
+- (0, import_react6.useEffect)(() => {
+- if (!key) return void 0;
+- actions.noteLive(sessionId, id, JSON.parse(key));
+- return () => actions.noteLive(sessionId, id, null);
+- }, [sessionId, id, key]);
+var since = (now, at) => typeof at === "number" && now - at >= 6e4 ? ` · ${elapsedLabel(now - at)}` : "";
+var alwaysChoices = (approval, bot) => bot?.remote ? [] : [
+ ["always-bot", "Always allow for this Bot"],
@@ -5426,7 +7054,12 @@ diff --git a/client.js b/client.js
+ (0, import_react11.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name }))
+ )
+ );
-+}
+ }
+-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;
+function MemberWork({ entry, bot, room: room2, actions, now }) {
+ const [stopping, setStopping] = (0, import_react11.useState)(false);
+ if (entry.state === "ask") {
@@ -5539,7 +7172,20 @@ diff --git a/client.js b/client.js
+ (0, import_react12.useEffect)(() => {
+ taskListeners.add(setTasks);
+ let stopped = false;
-+ let timer;
+ let timer;
+- let seen = "";
+- const tick = async () => {
+- const result = await actions.roomProgress(sessionId);
+- if (stop) return;
+- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0];
+- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : "";
+- if (entry && key !== seen) {
+- seen = key;
+- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null });
+- }
+- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3);
+- };
+- void tick();
+ const load = () => Promise.resolve(actions?.tasks?.()).then((list) => {
+ if (!stopped && list !== void 0) publishTasks(list);
+ }).catch(() => {
@@ -5547,14 +7193,95 @@ diff --git a/client.js b/client.js
+ if (!stopped) timer = setTimeout(load, POLL_MS2);
+ });
+ load();
-+ return () => {
+ return () => {
+- stop = true;
+ stopped = true;
-+ clearTimeout(timer);
+ clearTimeout(timer);
+ taskListeners.delete(setTasks);
-+ };
+ };
+- }, [done, sessionId, callId]);
+- let replies = live.replies;
+- if (done) {
+- try {
+- replies = JSON.parse(contentText(content)).replies ?? [];
+- } catch {
+- replies = [];
+- }
+- }
+- 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 }))
+- );
+- })
+- );
+ }, []);
+ return tasks;
-+}
+ }
+-var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]);
+-var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]);
+-function callTree(root) {
+- const calls = [];
+var ownBot = (roster, id) => {
+ const bot = roster.byId?.[id];
+ if (bot === void 0 || bot.remote) return void 0;
@@ -5655,7 +7382,13 @@ diff --git a/client.js b/client.js
+ const count = room2.pendingCount ?? 0;
+ if (count > 0) held(room2.id, count, { room: room2, place: placeOf(roster, room2.id), ...people(roster, null) });
+ }
-+ const seen = /* @__PURE__ */ new Set();
+ const seen = /* @__PURE__ */ new Set();
+- const visit = (call2) => {
+- const id = call2.callId ?? call2;
+- if (seen.has(id)) return;
+- seen.add(id);
+- calls.push(call2);
+- for (const child of call2.subCalls ?? []) visit(child);
+ const running = (botId, entry, roomId) => {
+ const bot = ownBot(roster, botId);
+ const key = `${botId}|${roomId ?? ""}`;
@@ -5673,7 +7406,88 @@ diff --git a/client.js b/client.js
+ chat: roomId ?? bot.id,
+ ...people(roster, bot.id)
+ });
-+ };
+ };
+- visit(root);
+- return calls;
+-}
+-function useShownCards(actions, sessionId, callIds) {
+- const key = JSON.stringify(callIds);
+- (0, import_react6.useLayoutEffect)(() => {
+- const ids = JSON.parse(key);
+- if (ids.length === 0 || !actions.noteCards) return void 0;
+- actions.noteCards(sessionId, ids, 1);
+- return () => actions.noteCards(sessionId, ids, -1);
+- }, [sessionId, key]);
+-}
+-function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) {
+- const tool = toolName(call2);
+- const done = call2.kind === "tool-result";
+- const { text: arg, value: argValue } = toolArgs(call2);
+- const self = roster.byId[sessionId];
+- switch (tool) {
+- case "message_bot": {
+- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" };
+- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
+- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
+- }
+- case "stop_bot": {
+- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" };
+- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
+- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
+- }
+- case "create_bot": {
+- const created = findBot(arg("name"));
+- if (!done || created === void 0) return null;
+- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) });
+- }
+- case "update_bot": {
+- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self);
+- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null;
+- }
+- 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;
+ for (const [botId, entry] of Object.entries(work.bots ?? {})) running(botId, entry, entry.roomId);
+ for (const [roomId, entries2] of Object.entries(work.rooms ?? {})) for (const entry of entries2) running(entry.botId, entry, roomId);
+ const lastEnded = /* @__PURE__ */ new Map();
@@ -5730,18 +7544,37 @@ 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 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 work = useWork ? useWork((value) => value) : {};
+ const open = useUi ? useUi((value) => Boolean(value.inbox)) : false;
+ const tasks = useTasks(actions);
@@ -5888,7 +7721,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)}
@@ -5926,95 +7759,737 @@ 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"];
++var COLUMN_OF = {
++ open: "not-started",
++ claimed: "not-started",
++ working: "in-progress",
++ blocked: "in-progress",
++ "result-ready": "in-progress",
++ accepted: "done",
++ cancelled: "done",
++ failed: "done"
++};
++function columnOf(task) {
++ return COLUMN_OF[task?.status] ?? "not-started";
+ }
+-function useRunningHost(active) {
+- const [host, setHost] = (0, import_react7.useState)(null);
+- (0, import_react7.useEffect)(() => {
+- if (!active) {
+- setHost(null);
+- return void 0;
++function priorityOf(task) {
++ return PRIORITIES.includes(task?.priority) ? task.priority : "normal";
++}
++function progressOf(task) {
++ const value = task?.progress;
++ if (typeof value !== "number" || !Number.isFinite(value)) return columnOf(task) === "done" && task?.status !== "cancelled" && task?.status !== "failed" ? 100 : 0;
++ return Math.max(0, Math.min(100, Math.round(value)));
++}
+
- // src/client/sidebar.js
--var import_react8 = require("react");
++// src/client/kanban.js
+var import_react13 = 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)(() => {
++var POLL_MS3 = 15e3;
++function BoardIcon() {
++ return (0, import_react13.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 })
++ );
++}
++function shortId(id) {
++ const hex = String(id ?? "").replace(/^task-/, "");
++ return `#${hex.slice(-6)}`;
++}
++function boardColumns(tasks) {
++ const columns = { "not-started": [], "in-progress": [], done: [] };
++ for (const task of tasks ?? []) {
++ const column = columnOf(task);
++ (columns[column] ?? columns["not-started"]).push(task);
++ }
++ for (const list of Object.values(columns)) list.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
++ return columns;
++}
++function adminOf(roster, room2) {
++ const me = roster?.self?.memberId;
++ if (!me) return room2?.origin !== "face-to-face";
++ const human = (room2?.participants ?? []).find((person) => person.kind === "human" && person.id === me && person.leftSeq === void 0);
++ if (human?.role === "admin") return true;
++ const admin = roster?.byId?.[room2?.admin];
++ return Boolean(admin && !admin.remote && admin.ownerId === me);
++}
++function boardRail(roster) {
++ const me = roster?.self?.memberId;
++ const bots = (roster?.bots ?? []).filter((bot) => bot && !bot.remote && (!me || !bot.ownerId || bot.ownerId === me));
++ const ids = new Set(bots.map((bot) => bot.id));
++ const children = /* @__PURE__ */ new Map();
++ for (const bot of bots) {
++ const parent = bot.createdBy && ids.has(bot.createdBy) ? bot.createdBy : "";
++ const list = children.get(parent) ?? [];
++ list.push(bot);
++ children.set(parent, list);
++ }
++ for (const list of children.values()) list.sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0) || String(a.name).localeCompare(String(b.name)));
++ const rows = [];
++ const walk = (parent, depth) => {
++ 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 }));
++ return { bots: rows, groups };
++}
++function canMoveTask(roster, task) {
++ const me = roster?.self?.memberId;
++ if (!me) return true;
++ const bot = roster?.byId?.[task?.ownerBotId];
++ return Boolean(bot && !bot.remote && (!bot.ownerId || bot.ownerId === me));
++}
++function priorityLabel(priority) {
++ if (priority === "urgent") return t("Highest");
++ if (priority === "high") return t("High");
++ if (priority === "low") return t("Low");
++ return t("Normal");
++}
++function statusLabel(status) {
++ switch (status) {
++ case "claimed":
++ return t("Claimed");
++ case "working":
++ return t("In progress");
++ case "blocked":
++ return t("Blocked");
++ case "result-ready":
++ return t("Result ready");
++ case "accepted":
++ return t("Done");
++ case "cancelled":
++ return t("Cancelled");
++ case "failed":
++ return t("Failed");
++ default:
++ return t("Not started");
++ }
++}
++function dropLabel(column) {
++ if (column === "in-progress") return t("Start task");
++ if (column === "done") return t("Complete task");
++ return t("Not started");
++}
++function columnTitle(column) {
++ if (column === "in-progress") return t("In progress");
++ if (column === "done") return t("Done");
++ return t("Not started");
++}
++function botName(roster, task) {
++ return roster?.byId?.[task.ownerBotId]?.name || task.ownerBotName || "";
++}
++function lanesOf(tasks, roster) {
++ const order = [];
++ const map = /* @__PURE__ */ new Map();
++ for (const task of tasks) {
++ const id = task.ownerBotId ?? "";
++ if (!map.has(id)) {
++ map.set(id, []);
++ order.push(id);
++ }
++ map.get(id).push(task);
++ }
++ 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 key = scope?.kind === "group" ? `group:${scope.roomId}` : "own";
+ (0, import_react13.useEffect)(() => {
- if (active && marked) actions.clearUnread(id);
- }, [active, marked]);
++ let stopped = false;
++ const load = () => {
++ const job = scope?.kind === "group" ? Promise.resolve(actions?.tasks?.({ roomId: scope.roomId })) : Promise.resolve(actions?.tasks?.());
++ void job.then((list) => {
++ if (stopped || !Array.isArray(list)) return;
++ 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();
++ 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) => {
++ const write = (list) => list.some((item) => item.id === task.id) ? list.map((item) => item.id === task.id ? task : item) : [...list, task];
++ if (scope?.kind === "group") setGroupTasks(write);
++ else setOwn(write);
++ };
++ return { own, tasks, replace };
}
--var PinMark = () => (0, import_react8.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react8.createElement)(PinMarkIcon));
-+var PinMark = () => (0, import_react13.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react13.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_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_react13.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);
-- const preview = useSessions((list) => previewOf(list, part, roster.exchangeTurns[part]));
-+ const preview = useSessions((list) => nameMentions(previewOf(list, part, roster.exchangeTurns[part], unsentReplyTurns(bot, part)), roster));
- const running = useSessionStatus((map) => map.get(part)?.running === true);
- const active = useSessionRetainInfo(part, (info) => (info?.retainedBy?.mainView ?? 0) > 0);
- const unread = useSessionStatus((map) => map.get(part)?.completionUnread === true || map.get(part)?.pendingInteraction !== void 0) || marked && !active;
- useMarkedUnread(id, active, marked, actions);
- const main = isMainOf(roster, id);
-- const tile = roster.mainBotId === id;
-+ const tile = roster.mainBotId === id && !asRow;
- const approval = useApproval(useWork, (entry) => entry.botId === id);
-- const alert = roster.questions?.[part] !== void 0 || approval;
-+ const waiting = approval || (bot.pendingCount ?? 0) > 0;
-+ const alert = roster.questions?.[part] !== void 0 || waiting;
- const doing = useActivity((map) => map[id]);
- const groups = workKeys(useGroupWork(useWork, (rooms) => Object.entries(rooms).flatMap(([roomId, entries2]) => entries2.filter((entry) => entry.botId === id).map((entry) => `${roomId}|${entry.tool ?? ""}`))));
- const away = !running && groups.length > 0;
- const state = botState(running || away, doing, alert);
-- const awayLine = away ? workLine(roster.roomsById[groups[0].id]?.name ?? t("a group chat"), groups[0].tool, groups.length - 1) : null;
-- if (bot.hidden && !active && !main) return null;
-+ const shownAway = away ? awayGroups(groups, roster.roomsById) : [];
-+ const awayLine = shownAway.length > 0 ? workLine(roster.roomsById[shownAway[0].id]?.name ?? t("a group chat"), shownAway[0].tool, shownAway.length - 1) : null;
-+ if (!isShownRemoteChat(bot) && !active && !main) return null;
- const onContextMenu = (event) => {
- event.preventDefault();
- actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY });
- };
- const pinned = bot.pinned === true && !tile;
-- const label = [bot.name, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", approval ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", ");
-+ const remote = Boolean(bot.remote);
-+ const shown = remote ? friendBotTitle(bot) : bot.name;
-+ 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_react8.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react8.createElement)(
-+ return (0, import_react13.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react13.createElement)(
- "button",
- {
- type: "button",
-@@ -4785,55 +4785,130 @@
- 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_react13.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
-+ (0, import_react13.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_react13.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null,
-+ bot.role ? (0, import_react13.createElement)("span", { className: "bt-chip" }, bot.role) : null
- ));
- }
-- return (0, import_react8.createElement)(
-+ const unreadMark = wide && waiting && !active ? (0, import_react13.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react13.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null;
-+ const row = (0, import_react13.createElement)(
- "button",
- {
- type: "button",
- className: "bt-row",
- "aria-label": label,
- "aria-current": active ? "page" : void 0,
-+ // An own Bot's hover text names its model (#366); a friend's Bot runs on its owner's model.
- "data-bot-id": id,
+-function useRowEntrances(replacing) {
+- const check = (0, import_react7.useRef)(replacing);
+- check.current = replacing;
+- (0, import_react7.useEffect)(() => {
+- const seen = /* @__PURE__ */ new WeakSet();
+- const started = Date.now();
+- let column = null;
+- let frame = 0;
+- const settle = (event) => {
+- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return;
+- delete event.currentTarget.dataset.btEnter;
+- event.currentTarget.removeEventListener("animationend", settle);
+- };
+- const scan = () => {
+- frame = 0;
+- const next = document.querySelector("[data-chat-flow]");
+- if (next !== column) {
+- observer.disconnect();
+- column = next;
+- if (column) observer.observe(column, { childList: true, subtree: true });
+- }
+- if (!column) return;
+- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")];
+- let last = -1;
+- items.forEach((item, index) => {
+- if (seen.has(item)) last = index;
+- });
+- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0);
+- for (const item of fresh) seen.add(item);
+- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return;
+- for (const item of fresh) {
+- if (items.indexOf(item) < last) continue;
+- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind);
+- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new";
+- item.addEventListener("animationend", settle);
++function RailButton({ row, selected, meta, onSelect }) {
++ return (0, import_react13.createElement)(
++ "button",
++ {
++ type: "button",
++ className: "bt-board-rail-item",
++ "data-kind": row.kind,
++ "data-id": row.id,
++ "data-selected": selected || void 0,
++ "aria-current": selected ? "true" : void 0,
++ 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
++ );
++}
++function BoardCard({ task, roster, movable, onMove, onDrag }) {
++ const column = columnOf(task);
++ const priority = priorityOf(task);
++ const progress = progressOf(task);
++ const who = botName(roster, task);
++ return (0, import_react13.createElement)(
++ "article",
++ {
++ className: "bt-board-card",
++ "data-task": task.id,
++ "data-column": column,
++ "data-priority": priority,
++ draggable: movable || void 0,
++ onDragStart: (event) => {
++ event.dataTransfer?.setData("text/plain", task.id);
++ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
++ onDrag?.(task.id);
++ },
++ onDragEnd: () => onDrag?.(null)
++ },
++ (0, import_react13.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_react13.createElement)("span", { className: "bt-board-title" }, task.title || t("Untitled task")),
++ task.note ? (0, import_react13.createElement)("span", { className: "bt-board-note" }, task.note) : null,
++ (0, import_react13.createElement)(
++ "span",
++ { className: "bt-board-foot" },
++ who ? (0, import_react13.createElement)("span", { className: "bt-board-who" }, who) : null,
++ (0, import_react13.createElement)(
++ "span",
++ { className: "bt-board-progress" },
++ (0, import_react13.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_react13.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
++ );
++}
++function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag }) {
++ return (0, import_react13.createElement)(
++ "section",
++ {
++ className: "bt-board-column",
++ "data-column": column,
++ "aria-label": columnTitle(column),
++ onDragOver: (event) => {
++ if (!dragging) return;
++ event.preventDefault();
++ },
++ onDrop: (event) => {
++ event.preventDefault();
++ 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)(
++ "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))
++ ),
++ 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.")) : lanes ? lanesOf(tasks, roster).map((lane) => (0, import_react13.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, task, roster, movable: canMoveTask(roster, task), onMove, onDrag }))
++ )) : tasks.map((task) => (0, import_react13.createElement)(BoardCard, { key: task.id, task, roster, movable: canMoveTask(roster, task), 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)(
++ "div",
++ { className: "bt-board-body" },
++ (0, import_react13.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) => {
++ const count = counts2?.[row.id] ?? { done: 0, total: 0 };
++ return (0, import_react13.createElement)(RailButton, {
++ key: row.id,
++ row,
++ selected: row.id === selectedBot,
++ onSelect,
++ 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 }))
++ ),
++ (0, import_react13.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)(
++ "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"))
++ ) : null,
++ (0, import_react13.createElement)(
++ "div",
++ { className: "bt-board-columns" },
++ ["not-started", "in-progress", "done"].map((column) => (0, import_react13.createElement)(BoardColumn, {
++ key: column,
++ column,
++ tasks: columns[column],
++ roster,
++ lanes,
++ dragging,
++ onMove,
++ onDrag
++ }))
++ )
++ )
++ );
++}
++function KanbanRow({ wide, useUi, actions }) {
++ const open = useUi ? useUi((value) => Boolean(value.board)) : false;
++ const label = t("Tasks");
++ return (0, import_react13.createElement)(
++ "button",
++ {
++ type: "button",
++ className: "bt-row bt-board-row",
++ "aria-label": label,
++ title: wide ? void 0 : label,
++ "aria-haspopup": "dialog",
++ "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
++ );
++}
++function scopeName(roster, scope) {
++ if (scope?.kind === "group") return roster.roomsById?.[scope.roomId]?.name || t("Group");
++ if (scope?.kind === "bot") return roster.byId?.[scope.botId]?.name || "";
++ return "";
++}
++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 { own, tasks, replace } = useBoardTasks(actions, scope);
++ const close = (0, import_react13.useCallback)(() => actions.closeBoard(), []);
++ useEscape(close);
++ const counts2 = {};
++ for (const task of own) {
++ const id = task.ownerBotId;
++ const bucket = counts2[id] ?? (counts2[id] = { done: 0, total: 0 });
++ if (task.status === "cancelled" || task.status === "failed") continue;
++ bucket.total += 1;
++ if (columnOf(task) === "done") bucket.done += 1;
++ }
++ const move = async (id, column) => {
++ const task = tasks.find((item) => item.id === id) ?? own.find((item) => item.id === id);
++ if (!task || !canMoveTask(roster, task) || columnOf(task) === column) return;
++ setError("");
++ try {
++ 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();
++ if (!text || scope?.kind !== "bot") return;
++ setError("");
++ try {
++ replace(await actions.taskCreate({ botId: scope.botId, title: text, column: "not-started" }));
++ 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
+- )
++ { 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)(
++ "div",
++ { className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" },
++ (0, import_react13.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_react13.createElement)(BoardView, {
++ roster,
++ tasks,
++ rail,
++ scope,
++ counts: counts2,
++ dragging,
++ error,
++ draft,
++ onDraft: setDraft,
++ onSelect: setScope,
++ onMove: move,
++ 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)}
++.bt-board-mark svg{width:18px;height:18px}
++.bt-board-label{flex:1 1 auto;min-width:0;font-size:14px;line-height:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
++.bt-mem.bt-board{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))}
++.bt-board-body{flex:1 1 auto;min-height:0;display:flex;gap:0}
++.bt-board-rail{flex:none;width:232px;overflow:auto;border-right:.5px solid var(--bt-line);padding:8px 8px 16px;display:flex;flex-direction:column;gap:2px}
++.bt-board-rail-head{margin:10px 8px 4px;font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3)}
++.bt-board-rail-item{display:flex;align-items:center;gap:8px;width:100%;min-height:36px;padding:6px 10px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer}
++.bt-board-rail-item:hover{background:var(--bt-hover)}
++.bt-board-rail-item[data-selected]{background:var(--bt-active,var(--bt-hover))}
++.bt-board-rail-name{flex:1 1 auto;min-width:0;font-size:13.5px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
++.bt-board-rail-meta{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
++.bt-board-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:10px;padding:12px 14px 16px}
++.bt-board-add{display:flex;gap:8px}
++.bt-board-add input{flex:1 1 auto;min-width:0;height:32px;padding:0 10px;border:.5px solid var(--bt-line-2);border-radius:10px;background:var(--bt-card,var(--bt-main));color:var(--bt-ink);font:inherit}
++.bt-board-columns{flex:1 1 auto;min-height:0;display:flex;gap:10px}
++.bt-board-column{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:8px;padding:8px;border-radius:14px;background:var(--bt-hover);overflow:auto}
++.bt-board-column-head{display:flex;align-items:center;gap:6px;margin:0;padding:2px 4px;font-size:13px;line-height:18px;font-weight:600;color:var(--bt-ink-2)}
++.bt-board-count{font-weight:500;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
++.bt-board-drop{margin:0;padding:18px 8px;border:1px dashed var(--bt-line-2);border-radius:12px;text-align:center;font-size:13px;line-height:18px;color:var(--bt-ink-3)}
++.bt-board-empty{margin:0;padding:8px 4px;font-size:13px;line-height:18px;color:var(--bt-ink-3)}
++.bt-board-error{margin:0;font-size:13px;line-height:18px;color:#c21d2e}
++.bt-board-lane{display:flex;flex-direction:column;gap:6px}
++.bt-board-lane-name{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3);padding:0 2px}
++.bt-board-card{display:flex;flex-direction:column;gap:6px;padding:10px;border:.5px solid var(--bt-line-2);border-radius:12px;background:var(--bt-card,var(--bt-main));cursor:grab}
++.bt-board-card-top{display:flex;align-items:center;gap:6px;min-width:0}
++.bt-board-id{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
++.bt-board-priority{flex:none;height:18px;padding:0 6px;border-radius:999px;font-size:11px;line-height:18px;font-weight:600}
++.bt-board-priority[data-priority=urgent]{background:rgba(224,33,53,.12);color:#c21d2e}
++.bt-board-priority[data-priority=high]{background:rgba(217,119,6,.14);color:#b45309}
++.bt-board-priority[data-priority=normal]{background:color-mix(in srgb,var(--bt-accent) 14%,transparent);color:color-mix(in srgb,var(--bt-accent) 80%,var(--bt-ink))}
++.bt-board-priority[data-priority=low]{background:color-mix(in srgb,var(--bt-green) 16%,transparent);color:color-mix(in srgb,var(--bt-green) 70%,var(--bt-ink))}
++.bt-board-status{margin-left:auto;font-size:11.5px;line-height:16px;color:var(--bt-ink-3);white-space:nowrap}
++.bt-board-title{font-size:13.5px;line-height:19px;color:var(--bt-ink)}
++.bt-board-note{font-size:12px;line-height:16px;color:var(--bt-ink-2)}
++.bt-board-foot{display:flex;align-items:center;gap:8px}
++.bt-board-who{flex:none;max-width:40%;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
++.bt-board-progress{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:6px}
++.bt-board-progress-bar{flex:1 1 auto;height:4px;border-radius:999px;background:var(--bt-line);overflow:hidden}
++.bt-board-progress-bar>span{display:block;height:100%;background:var(--bt-accent)}
++.bt-board-percent{flex:none;font-size:11.5px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
++.bt-board-card .bt-soft{align-self:flex-start}
++@media (max-width:800px){
++ .bt-mem.bt-board{width:min(100vw,calc(100vw - 16px));height:100%}
++ .bt-board-body{flex-direction:column}
++ .bt-board-rail{width:auto;max-height:180px;border-right:0;border-bottom:.5px solid var(--bt-line)}
++ .bt-board-columns{overflow:auto}
++ .bt-board-column{min-width:220px}
++}
++@media (max-width:640px){
++ .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_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)(() => {
+ 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 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_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}` : ""}`);
++function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked, asRow = false }) {
+ const id = bot.id;
+ const part = currentPart(bot);
+- const preview = useSessions((list) => previewOf(list, part, roster.exchangeTurns[part]));
++ const preview = useSessions((list) => nameMentions(previewOf(list, part, roster.exchangeTurns[part], unsentReplyTurns(bot, part)), roster));
+ const running = useSessionStatus((map) => map.get(part)?.running === true);
+ const active = useSessionRetainInfo(part, (info) => (info?.retainedBy?.mainView ?? 0) > 0);
+ const unread = useSessionStatus((map) => map.get(part)?.completionUnread === true || map.get(part)?.pendingInteraction !== void 0) || marked && !active;
+ useMarkedUnread(id, active, marked, actions);
+ const main = isMainOf(roster, id);
+- const tile = roster.mainBotId === id;
++ const tile = roster.mainBotId === id && !asRow;
+ const approval = useApproval(useWork, (entry) => entry.botId === id);
+- const alert = roster.questions?.[part] !== void 0 || approval;
++ const waiting = approval || (bot.pendingCount ?? 0) > 0;
++ const alert = roster.questions?.[part] !== void 0 || waiting;
+ const doing = useActivity((map) => map[id]);
+ const groups = workKeys(useGroupWork(useWork, (rooms) => Object.entries(rooms).flatMap(([roomId, entries2]) => entries2.filter((entry) => entry.botId === id).map((entry) => `${roomId}|${entry.tool ?? ""}`))));
+ const away = !running && groups.length > 0;
+ const state = botState(running || away, doing, alert);
+- const awayLine = away ? workLine(roster.roomsById[groups[0].id]?.name ?? t("a group chat"), groups[0].tool, groups.length - 1) : null;
+- if (bot.hidden && !active && !main) return null;
++ const shownAway = away ? awayGroups(groups, roster.roomsById) : [];
++ const awayLine = shownAway.length > 0 ? workLine(roster.roomsById[shownAway[0].id]?.name ?? t("a group chat"), shownAway[0].tool, shownAway.length - 1) : null;
++ if (!isShownRemoteChat(bot) && !active && !main) return null;
+ const onContextMenu = (event) => {
+ event.preventDefault();
+ actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY });
+ };
+ const pinned = bot.pinned === true && !tile;
+- const label = [bot.name, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", approval ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", ");
++ const remote = Boolean(bot.remote);
++ const shown = remote ? friendBotTitle(bot) : bot.name;
++ 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_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)(
+ "button",
+ {
+ type: "button",
+@@ -4785,55 +4785,130 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
+ 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),
++ // 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
+ ));
+ }
+- 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)(
+ "button",
+ {
+ type: "button",
+ className: "bt-row",
+ "aria-label": label,
+ "aria-current": active ? "page" : void 0,
++ // An own Bot's hover text names its model (#366); a friend's Bot runs on its owner's model.
+ "data-bot-id": id,
- title: wide ? void 0 : bot.name,
+ title: remote ? shown : botRowTitle(shown, modelOf(bot, roster)?.name, wide),
onClick: () => actions.openSession(id),
@@ -6023,27 +8498,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_react13.createElement)(BotAvatar, { bot, size: 36, state, main }),
-+ pinned && !wide ? (0, import_react13.createElement)(PinMark) : null,
-+ wide ? (0, import_react13.createElement)(
++ (0, import_react14.createElement)(BotAvatar, { bot, size: 36, state, main }),
++ pinned && !wide ? (0, import_react14.createElement)(PinMark) : null,
++ wide ? (0, import_react14.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_react13.createElement)(
++ (0, import_react14.createElement)(
+ "span",
+ { className: "bt-row-name" },
-+ (0, import_react13.createElement)("span", { className: "bt-row-title" }, shown),
-+ bot.remote ? (0, import_react13.createElement)(RemoteMark, { remote: bot.remote }) : null,
-+ pinned ? (0, import_react13.createElement)(PinMark) : null
++ (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
+ ),
-+ secondLine ? (0, import_react13.createElement)("span", { className: "bt-row-preview" }, secondLine) : null
++ secondLine ? (0, import_react14.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_react13.createElement)("button", {
++ const settings = wide ? (0, import_react14.createElement)("button", {
+ type: "button",
+ className: "bt-row-settings",
+ "aria-label": settingsLabel,
@@ -6054,10 +8529,10 @@ diff --git a/client.js b/client.js
+ keepSidebarStill(event.currentTarget);
+ actions.openSettings("bots", id);
+ }
-+ }, (0, import_react13.createElement)(GearIcon)) : null;
-+ if (settings) return (0, import_react13.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
++ }, (0, import_react14.createElement)(GearIcon)) : null;
++ if (settings) return (0, import_react14.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
+ return row;
- }
++}
+function keepSidebarStill(node) {
+ const reset = () => {
+ for (let at = node?.parentElement; at; at = at.parentElement) if (at.scrollLeft > 0) at.scrollLeft = 0;
@@ -6068,14 +8543,14 @@ diff --git a/client.js b/client.js
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset));
+}
+function RemoteMark({ remote }) {
-+ return (0, import_react13.createElement)(
++ return (0, import_react14.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_react13.createElement)(
++ (0, import_react14.createElement)(
+ "svg",
+ { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
-+ (0, import_react13.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
-+ (0, import_react13.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
++ (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" })
+ )
+ );
+}
@@ -6087,18 +8562,18 @@ 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_react13.createElement)(
++ return (0, import_react14.createElement)(
+ "span",
+ { className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") },
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
+ "svg",
+ { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
-+ (0, import_react13.createElement)("circle", { cx: 9, cy: 8, r: 3 }),
-+ (0, import_react13.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }),
-+ (0, import_react13.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" })
++ (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" })
+ )
-+ );
-+}
+ );
+ }
function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) {
const id = room2.id;
- const fallback = useSessions((list) => previewOf(list, id));
@@ -6124,7 +8599,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_react13.createElement)(
++ return (0, import_react14.createElement)(
"button",
{
type: "button",
@@ -6136,139 +8611,139 @@ diff --git a/client.js b/client.js
"data-room-id": id,
onClick: () => actions.openSession(id),
onContextMenu: (event) => {
-@@ -4841,20 +4841,20 @@
+@@ -4841,20 +4841,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus,
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_react13.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
-+ pinned && !wide ? (0, import_react13.createElement)(PinMark) : null,
-+ wide ? (0, import_react13.createElement)(
++ (0, import_react14.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
++ pinned && !wide ? (0, import_react14.createElement)(PinMark) : null,
++ wide ? (0, import_react14.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_react13.createElement)("span", { className: "bt-row-name" }, (0, import_react13.createElement)("span", { className: "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react13.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react13.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react13.createElement)(PinMark) : null),
-+ workingLine ? (0, import_react13.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react13.createElement)("span", { className: "bt-row-preview" }, preview) : null
++ (0, import_react14.createElement)("span", { className: "bt-row-name" }, (0, import_react14.createElement)("span", { className: "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
) : 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_react13.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react13.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
);
}
function AgentSessionRow({ session, agent, actions, renaming, index }) {
- const input = (0, import_react8.useRef)(null);
- (0, import_react8.useLayoutEffect)(() => {
-+ const input = (0, import_react13.useRef)(null);
-+ (0, import_react13.useLayoutEffect)(() => {
++ const input = (0, import_react14.useRef)(null);
++ (0, import_react14.useLayoutEffect)(() => {
if (!renaming) return;
input.current?.focus();
input.current?.select();
-@@ -4875,10 +4875,10 @@
+@@ -4875,10 +4875,10 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
}
})();
};
- return (0, import_react8.createElement)(
-+ return (0, import_react13.createElement)(
++ return (0, import_react14.createElement)(
"div",
{ className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } },
- (0, import_react8.createElement)("input", {
-+ (0, import_react13.createElement)("input", {
++ (0, import_react14.createElement)("input", {
ref: input,
defaultValue: session.title,
"aria-label": t("Rename"),
-@@ -4891,7 +4891,7 @@
+@@ -4891,7 +4891,7 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
})
);
}
- return (0, import_react8.createElement)(
-+ return (0, import_react13.createElement)(
++ return (0, import_react14.createElement)(
"button",
{
type: "button",
-@@ -4905,8 +4905,8 @@
+@@ -4905,8 +4905,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_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_react13.createElement)("span", { className: "bt-agent-sub-title" }, session.title),
-+ (0, import_react13.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) : "")
);
}
function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) {
-@@ -4941,10 +4941,10 @@
+@@ -4941,10 +4941,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
const preview = sessions[0]?.title ?? t("New session");
const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", ");
const open = active && wide;
- return (0, import_react8.createElement)(
-+ return (0, import_react13.createElement)(
++ return (0, import_react14.createElement)(
"div",
{ className: "bt-agent" },
- (0, import_react8.createElement)(
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
"button",
{
type: "button",
-@@ -4959,30 +4959,30 @@
+@@ -4959,30 +4959,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY });
}
},
- (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_react13.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react13.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })),
-+ wide ? (0, import_react13.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)(
"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_react13.createElement)("span", { className: "bt-row-name" }, "DSH Agent"),
-+ preview ? (0, import_react13.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
) : null,
- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
-+ wide && unread && !active ? (0, import_react13.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
++ wide && unread && !active ? (0, import_react14.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
),
- (0, import_react8.createElement)(
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
"div",
{ className: "bt-agent-sub", "data-open": open || void 0 },
- (0, import_react8.createElement)(
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
"div",
{ className: "bt-agent-sub-in" },
- (0, import_react8.createElement)(
-+ (0, import_react13.createElement)(
++ (0, import_react14.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_react13.createElement)(PlusIcon),
-+ (0, import_react13.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"))
),
- sessions.map((session, index) => (0, import_react8.createElement)(AgentSessionRow, {
-+ sessions.map((session, index) => (0, import_react13.createElement)(AgentSessionRow, {
++ sessions.map((session, index) => (0, import_react14.createElement)(AgentSessionRow, {
key: session.id,
session,
agent,
-@@ -4996,8 +4996,8 @@
+@@ -4996,8 +4996,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
}
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_react13.useRef)({ layout, map: /* @__PURE__ */ new Map() });
-+ (0, import_react13.useLayoutEffect)(() => {
++ const tops = (0, import_react14.useRef)({ layout, map: /* @__PURE__ */ new Map() });
++ (0, import_react14.useLayoutEffect)(() => {
const list = listRef.current;
if (!list) return;
const next = /* @__PURE__ */ new Map();
-@@ -5014,9 +5014,44 @@
+@@ -5014,9 +5014,44 @@ function useListGlide(listRef, layout, key) {
tops.current = { layout, map: next };
}, [layout, key]);
}
-+var NodeActions = class extends import_react13.Component {
++var NodeActions = class extends import_react14.Component {
+ constructor(props) {
+ super(props);
+ this.state = { failed: false };
@@ -6280,42 +8755,42 @@ diff --git a/client.js b/client.js
+ console.error("flybot: nodeActions failed", error);
+ }
+ render() {
-+ return this.state.failed ? null : (0, import_react13.createElement)(this.props.component, { bot: this.props.bot });
++ return this.state.failed ? null : (0, import_react14.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_react13.createElement)(
-+ import_react13.Fragment,
++ return (0, import_react14.createElement)(
++ import_react14.Fragment,
+ null,
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
+ "div",
+ { className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } },
-+ children.length ? (0, import_react13.createElement)(
++ children.length ? (0, import_react14.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_react13.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾")
-+ ) : (0, import_react13.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }),
-+ (0, import_react13.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }),
-+ typeof actions === "function" ? (0, import_react13.createElement)(NodeActions, { component: actions, bot }) : null
++ (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
+ ),
-+ closed ? null : children.map((child) => (0, import_react13.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions }))
++ closed ? null : children.map((child) => (0, import_react14.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_react13.useRef)(null);
++ const listRef = (0, import_react14.useRef)(null);
const roster = useRoster((value) => value);
const markedUnread = useUi((value) => value.unread);
props.useRegistry((value) => value);
-@@ -5025,17 +5025,26 @@
+@@ -5025,17 +5025,26 @@ function TeamSidebar(props) {
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_react13.useMemo)(() => {
++ const order = (0, import_react14.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 })),
@@ -6333,7 +8808,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_react13.useState)({});
++ const [collapsed, setCollapsed] = (0, import_react14.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;
@@ -6341,16 +8816,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,41 @@
+@@ -5045,39 +5045,42 @@ function TeamSidebar(props) {
event.preventDefault();
actions.openPalette(event.key);
};
- return (0, import_react8.createElement)(
-+ return (0, import_react13.createElement)(
++ return (0, import_react14.createElement)(
"div",
{ className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch },
- wide ? (0, import_react8.createElement)(
-+ wide ? (0, import_react13.createElement)(
++ wide ? (0, import_react14.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)),
@@ -6359,30 +8834,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_react13.createElement)("span", { className: "bt-brand" }, (0, import_react13.createElement)(BrandMark, { size: 22 }), (0, import_react13.createElement)(BrandName)),
-+ (0, import_react13.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
-+ (0, import_react13.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react13.createElement)(SearchIcon)),
-+ (0, import_react13.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react13.createElement)(PlusIcon))
-+ ) : (0, import_react13.createElement)(
-+ import_react13.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,
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_react13.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
-+ (0, import_react13.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react13.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))
),
- 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_react13.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
-+ (0, import_react13.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
-+ (0, import_react13.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)(
"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_react13.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react13.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react13.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react13.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_react13.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react13.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react13.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
-+ wide ? (0, import_react13.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react13.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null
++ 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
)
);
}
@@ -6394,16 +8870,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_react13.useState)(false);
-+ const [box, setBox] = (0, import_react13.useState)(null);
-+ const [error, setError] = (0, import_react13.useState)("");
-+ const [dshError, setDshError] = (0, import_react13.useState)("");
-+ const button = (0, import_react13.useRef)(null);
-+ (0, import_react13.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)(() => {
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 @@
+@@ -5116,14 +5116,14 @@ function UpdateButton({ useUpdate, actions }) {
setDshError("");
void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure))));
};
@@ -6415,73 +8891,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_react13.createElement)(
-+ import_react13.Fragment,
++ return (0, import_react14.createElement)(
++ import_react14.Fragment,
null,
- (0, import_react8.createElement)("button", {
-+ (0, import_react13.createElement)("button", {
++ (0, import_react14.createElement)("button", {
ref: button,
type: "button",
className: "bt-round bt-update",
-@@ -5133,21 +5133,21 @@
+@@ -5133,21 +5133,21 @@ function UpdateButton({ useUpdate, actions }) {
"aria-haspopup": "dialog",
"aria-expanded": open,
onClick: toggle
- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)(
-+ }, (0, import_react13.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react13.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
-+ open && box ? (0, import_react_dom3.createPortal)((0, import_react13.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)(
"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_react13.createElement)(
-+ import_react13.Fragment,
++ update?.available || phase !== "idle" ? (0, import_react14.createElement)(
++ import_react14.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_react13.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })),
-+ (0, import_react13.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_react13.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
-+ (0, import_react13.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)(
"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_react13.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
-+ phase === "installed" ? null : (0, import_react13.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", {
type: "button",
className: "bt-update-go",
disabled: phase === "installing",
-@@ -5155,17 +5155,17 @@
+@@ -5155,17 +5155,17 @@ function UpdateButton({ useUpdate, actions }) {
}, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update"))
)
) : null,
- dshRow ? (0, import_react8.createElement)(
-+ dshRow ? (0, import_react13.createElement)(
++ dshRow ? (0, import_react14.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_react13.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
-+ (0, import_react13.createElement)("div", { className: "bt-update-text" }, dshText),
-+ dshMessage ? (0, import_react13.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
-+ (0, import_react13.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)(
"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_react13.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_react13.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", {
type: "button",
className: "bt-update-go",
disabled: dshPhase === "installing",
-@@ -5176,61 +5176,25 @@
+@@ -5176,61 +5176,25 @@ function UpdateButton({ useUpdate, actions }) {
), document.body) : null
);
}
@@ -6509,11 +8985,11 @@ diff --git a/client.js b/client.js
- setTip(false);
- };
- return (0, import_react8.createElement)(
-+ return (0, import_react13.createElement)(
++ return (0, import_react14.createElement)(
"span",
{ className: "bt-connect-wrap" },
- (0, import_react8.createElement)(
-+ (0, import_react13.createElement)(
++ (0, import_react14.createElement)(
"button",
- {
- ref: button,
@@ -6536,31 +9012,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_react13.createElement)(PlugIcon),
++ (0, import_react14.createElement)(PlugIcon),
+ t("Connect apps")
+ )
);
}
function YouAvatar() {
- return (0, import_react8.createElement)(
-+ return (0, import_react13.createElement)(
++ return (0, import_react14.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_react13.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react13.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })),
-+ (0, import_react13.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"))
);
}
var openShellSettings = null;
-@@ -5259,6 +5259,18 @@
+@@ -5259,6 +5259,18 @@ function openHarnessOnboarding(id) {
else openHarnessSettings();
});
}
+var MODELS_STEP_ID = "flybot-models";
+var MODELS_STEP_ORDER = 1e3;
+function ModelsSettingsStep({ explicit, complete, openSection }) {
-+ (0, import_react13.useLayoutEffect)(() => {
++ (0, import_react14.useLayoutEffect)(() => {
+ if (explicit) openSection("models");
+ complete();
+ }, []);
@@ -6572,14 +9048,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 @@
+@@ -5288,32 +5288,32 @@ function pressSettingsShortcut() {
}));
}
function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) {
- const [open, setOpen] = (0, import_react8.useState)(false);
- (0, import_react8.useLayoutEffect)(() => {
-+ const [open, setOpen] = (0, import_react13.useState)(false);
-+ (0, import_react13.useLayoutEffect)(() => {
++ const [open, setOpen] = (0, import_react14.useState)(false);
++ (0, import_react14.useLayoutEffect)(() => {
if (!seat) return void 0;
actions.seatAccount(true);
return () => actions.seatAccount(false);
@@ -6587,13 +9063,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_react13.useState)(null);
-+ const button = (0, import_react13.useRef)(null);
-+ (0, import_react13.useEffect)(() => {
++ const [box, setBox] = (0, import_react14.useState)(null);
++ const button = (0, import_react14.useRef)(null);
++ (0, import_react14.useEffect)(() => {
if (settingsOpen) setOpen(false);
}, [settingsOpen]);
- (0, import_react8.useEffect)(() => {
-+ (0, import_react13.useEffect)(() => {
++ (0, import_react14.useEffect)(() => {
if (!openSettings) return void 0;
openShellSettings = openSettings;
return () => {
@@ -6601,7 +9077,7 @@ diff --git a/client.js b/client.js
};
}, [openSettings]);
- (0, import_react8.useEffect)(() => {
-+ (0, import_react13.useEffect)(() => {
++ (0, import_react14.useEffect)(() => {
if (!openOnboarding) return void 0;
openShellOnboarding = openOnboarding;
return () => {
@@ -6609,11 +9085,11 @@ diff --git a/client.js b/client.js
};
}, [openOnboarding]);
- (0, import_react8.useEffect)(() => {
-+ (0, import_react13.useEffect)(() => {
++ (0, import_react14.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 @@
+@@ -5341,30 +5341,32 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi
const groups = [
[
[t("Usage"), GaugeIcon, () => actions.openSettings("usage")],
@@ -6631,54 +9107,54 @@ diff --git a/client.js b/client.js
];
- return (0, import_react8.createElement)(
- import_react8.Fragment,
-+ return (0, import_react13.createElement)(
-+ import_react13.Fragment,
++ return (0, import_react14.createElement)(
++ import_react14.Fragment,
null,
- (0, import_react8.createElement)(
-+ (0, import_react13.createElement)(
++ (0, import_react14.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_react13.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react13.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 }))
),
- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)(
-+ open && box ? (0, import_react_dom3.createPortal)((0, import_react13.createElement)(
++ open && box ? (0, import_react_dom3.createPortal)((0, import_react14.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_react13.createElement)(FlyBotAccountHeader, { actions }),
-+ groups.map((group, index) => (0, import_react13.createElement)(
-+ import_react13.Fragment,
++ (0, import_react14.createElement)(FlyBotAccountHeader, { actions }),
++ groups.map((group, index) => (0, import_react14.createElement)(
++ import_react14.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_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
-+ group.map(([label, Icon, run, hint]) => (0, import_react13.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", {
key: label,
type: "button",
role: "menuitem",
-@@ -5372,7 +5372,7 @@
+@@ -5372,7 +5372,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi
setOpen(false);
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null))
-+ }, (0, import_react13.createElement)(Icon), (0, import_react13.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react13.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))
))
), document.body) : null
);
-@@ -5382,8 +5382,8 @@
+@@ -5382,8 +5382,8 @@ function useLandOnMainBot(roster, useSessions, actions) {
const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0);
return row ? `${row.id}|${row.blank ? 1 : 0}` : "";
});
- const lastBot = (0, import_react8.useRef)(null);
- (0, import_react8.useEffect)(() => {
-+ const lastBot = (0, import_react13.useRef)(null);
-+ (0, import_react13.useEffect)(() => {
++ const lastBot = (0, import_react14.useRef)(null);
++ (0, import_react14.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 @@
+@@ -5397,40 +5397,40 @@ function useLandOnMainBot(roster, useSessions, actions) {
return void 0;
}
const last = lastBot.current;
@@ -6691,20 +9167,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_react13.createElement)("span", { className: "bt-foot-account" }, (0, import_react13.createElement)(AccountLauncher, { ...props, seat: false }));
++ return (0, import_react14.createElement)("span", { className: "bt-foot-account" }, (0, import_react14.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_react13.createElement)(
++ return (0, import_react14.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_react13.createElement)(FlyBotMark, { size: 20 }),
-+ wide ? (0, import_react13.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
++ (0, import_react14.createElement)(FlyBotMark, { size: 20 }),
++ wide ? (0, import_react14.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
);
}
var RETURN_CSS = `
@@ -6721,15 +9197,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_react13.createElement)("span", { className: "bt-brand-mark" }, (0, import_react13.createElement)(FlyBotMark, { size }));
++ return (0, import_react14.createElement)("span", { className: "bt-brand-mark" }, (0, import_react14.createElement)(FlyBotMark, { size }));
}
function BrandName() {
- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot");
-+ return (0, import_react13.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot"));
++ return (0, import_react14.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 @@
+@@ -5491,18 +5491,28 @@ var SIDEBAR_CSS = `
.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)}
@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}}
@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}}
@@ -6747,53 +9223,53 @@ diff --git a/client.js b/client.js
// src/client/header.js
-var import_react9 = require("react");
-+var import_react14 = require("react");
++var import_react15 = 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_react14.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_react14.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 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_react14.useState)(false);
-+ (0, import_react14.useEffect)(() => {
++ const [shown, setShown] = (0, import_react15.useState)(false);
++ (0, import_react15.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 @@
+@@ -5512,16 +5512,16 @@ function useMemoryNotice(ids, useMemoryNews) {
return shown;
}
function MemoryNotice({ shown }) {
- const inner = (0, import_react9.useRef)(null);
- const [width, setWidth] = (0, import_react9.useState)(0);
-+ const inner = (0, import_react14.useRef)(null);
-+ const [width, setWidth] = (0, import_react14.useState)(0);
++ const inner = (0, import_react15.useRef)(null);
++ const [width, setWidth] = (0, import_react15.useState)(0);
const label = t("Memory updated");
- (0, import_react9.useLayoutEffect)(() => {
-+ (0, import_react14.useLayoutEffect)(() => {
++ (0, import_react15.useLayoutEffect)(() => {
if (shown) setWidth(inner.current?.scrollWidth ?? 0);
}, [label, shown]);
- return (0, import_react9.createElement)(
-+ return (0, import_react14.createElement)(
++ return (0, import_react15.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_react14.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react14.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react14.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)
);
}
function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) {
-@@ -5537,7 +5537,7 @@
+@@ -5537,7 +5537,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_react9.useEffect)(() => {
-+ (0, import_react14.useEffect)(() => {
++ (0, import_react15.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,38 @@
+@@ -5548,52 +5548,38 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe
const bot = roster.byId[sessionId];
const room2 = roster.roomsById[sessionId];
const detailsOpen = useUi((state) => state.details === sessionId);
@@ -6821,11 +9297,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_react14.createElement)(
++ return (0, import_react15.createElement)(
"div",
{ className: "bt-head" },
- (0, import_react9.createElement)(
-+ (0, import_react14.createElement)(
++ (0, import_react15.createElement)(
"button",
{
type: "button",
@@ -6841,28 +9317,28 @@ 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_react14.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react14.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null,
-+ (0, import_react14.createElement)("span", { className: "bt-pill-name" }, name || title || t("New chat")),
-+ (0, import_react14.createElement)(MemoryNotice, { shown: memoryNotice }),
-+ (0, import_react14.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react14.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: "bt-pill-name" }, 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_react14.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
-+ (0, import_react14.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }),
-+ (0, import_react14.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 })
);
}
function useTranscriptView() {
- const [view, setView] = (0, import_react9.useState)(null);
- (0, import_react9.useEffect)(() => {
-+ const [view, setView] = (0, import_react14.useState)(null);
-+ (0, import_react14.useEffect)(() => {
++ const [view, setView] = (0, import_react15.useState)(null);
++ (0, import_react15.useEffect)(() => {
let frame = 0;
let watched = null;
const observer = new MutationObserver(() => schedule());
-@@ -5642,7 +5642,7 @@
+@@ -5642,7 +5642,7 @@ function useTranscriptView() {
}, []);
return view;
}
@@ -6871,76 +9347,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 @@
+@@ -5658,18 +5658,22 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) {
});
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_react14.useState)({ base: null, dismissed: false });
-+ const [above, setAbove] = (0, import_react14.useState)({ seen: false, dismissed: false });
-+ (0, import_react14.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)(() => {
setRelease({ base: unpinned ? replies : null, dismissed: false });
}, [unpinned]);
- (0, import_react9.useEffect)(() => {
-+ (0, import_react14.useEffect)(() => {
++ (0, import_react15.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_react14.useEffect)(() => {
++ (0, import_react15.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_react14.useEffect)(() => {
++ (0, import_react15.useEffect)(() => {
const scroller = document.querySelector("[data-conversation-scroll]");
if (!scroller) return void 0;
scroller.toggleAttribute("data-bt-news", showDown);
-@@ -5677,16 +5677,16 @@
+@@ -5677,16 +5677,16 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) {
}, [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_react14.createElement)(
++ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react15.createElement)(
"div",
{ className: "bt-news", "data-direction": direction, style },
- (0, import_react9.createElement)(
-+ (0, import_react14.createElement)(
++ (0, import_react15.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_react14.createElement)("span", { className: "bt-news-ico" }, (0, import_react14.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
++ (0, import_react15.createElement)("span", { className: "bt-news-ico" }, (0, import_react15.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
label(count)
),
- (0, import_react9.createElement)("button", {
-+ (0, import_react14.createElement)("button", {
++ (0, import_react15.createElement)("button", {
type: "button",
className: "bt-news-x",
"aria-label": t("Dismiss new messages"),
-@@ -5694,7 +5694,7 @@
+@@ -5694,7 +5694,7 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) {
event.stopPropagation();
onDismiss();
}
- }, (0, import_react9.createElement)(CloseIcon))
-+ }, (0, import_react14.createElement)(CloseIcon))
++ }, (0, import_react15.createElement)(CloseIcon))
);
const toLatest = () => {
const scroller = document.querySelector("[data-conversation-scroll]");
-@@ -5704,8 +5704,8 @@
+@@ -5704,8 +5704,8 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) {
};
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_react14.createElement)(
-+ import_react14.Fragment,
++ (0, import_react15.createElement)(
++ import_react15.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 @@
+@@ -5714,19 +5714,117 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) {
);
}
@@ -7044,7 +9520,7 @@ diff --git a/client.js b/client.js
+
// src/client/composer.js
-var import_react10 = require("react");
-+var import_react15 = require("react");
++var import_react16 = require("react");
var import_react_dom5 = require("react-dom");
var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition;
var keepComposerFocus = (event) => event.preventDefault();
@@ -7053,19 +9529,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_react15.useState)(false);
-+ const [note, setNote] = (0, import_react15.useState)("");
-+ const recognition = (0, import_react15.useRef)(null);
-+ const latest = (0, import_react15.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);
latest.current = onText;
- (0, import_react10.useEffect)(() => () => recognition.current?.abort(), []);
- (0, import_react10.useEffect)(() => {
-+ (0, import_react15.useEffect)(() => () => recognition.current?.abort(), []);
-+ (0, import_react15.useEffect)(() => {
++ (0, import_react16.useEffect)(() => () => recognition.current?.abort(), []);
++ (0, import_react16.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
-@@ -5774,19 +5774,38 @@
+@@ -5774,19 +5774,38 @@ function useDictation(onText) {
const stop = () => recognition.current?.stop();
return { listening, note, start, stop };
}
@@ -7075,14 +9551,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_react15.createElement)(
++ return (0, import_react16.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_react15.createElement)(
++ return (0, import_react16.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 })
@@ -7094,7 +9570,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_react15.useEffect)(() => {
++ (0, import_react16.useEffect)(() => {
if (!quote || !inputActions) return;
actions.clearQuote(quote.token);
const excerpt = quote.text.replace(/\s+/g, " ").trim();
@@ -7107,28 +9583,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 @@
+@@ -5797,10 +5797,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) {
}, [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_react15.createElement)(
-+ import_react15.Fragment,
++ return (0, import_react16.createElement)(
++ import_react16.Fragment,
null,
- pluginMic ? null : (0, import_react10.createElement)("button", {
-+ pluginMic ? null : (0, import_react15.createElement)("button", {
++ pluginMic ? null : (0, import_react16.createElement)("button", {
type: "button",
className: "bt-mic",
"aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"),
-@@ -5808,16 +5808,16 @@
+@@ -5808,16 +5808,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_react10.createElement)(MicIcon)),
- (0, import_react10.createElement)("button", {
-+ }, (0, import_react15.createElement)(MicIcon)),
-+ (0, import_react15.createElement)("button", {
++ }, (0, import_react16.createElement)(MicIcon)),
++ (0, import_react16.createElement)("button", {
type: "button",
className: "bt-voice",
"aria-label": t("Start voice chat"),
@@ -7137,73 +9613,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_react15.createElement)(WaveIcon, { size: 18 })),
-+ dictation.note ? (0, import_react15.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
);
}
function menuIcons(roster) {
-@@ -5825,17 +5825,17 @@
+@@ -5825,17 +5825,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_react10.createElement)(BotMark, { bot: roster.getSnapshot().byId[id] ?? row.bot, size }));
-+ if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react15.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: roster.getSnapshot().byId[id] ?? row.bot, 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_react15.useState)(null);
++ const [open, setOpen] = (0, import_react16.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_react15.useRef)(null);
-+ const menu = (0, import_react15.useRef)(null);
-+ (0, import_react15.useEffect)(() => {
++ const button = (0, import_react16.useRef)(null);
++ const menu = (0, import_react16.useRef)(null);
++ (0, import_react16.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 @@
+@@ -5876,10 +5876,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
actions.referenceChat(sessionId, inputActions?.captureInsertion?.());
};
const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat();
- return (0, import_react10.createElement)(
- import_react10.Fragment,
-+ return (0, import_react15.createElement)(
-+ import_react15.Fragment,
++ return (0, import_react16.createElement)(
++ import_react16.Fragment,
null,
- (0, import_react10.createElement)("button", {
-+ (0, import_react15.createElement)("button", {
++ (0, import_react16.createElement)("button", {
ref: button,
type: "button",
className: "bt-plus",
-@@ -5889,8 +5889,8 @@
+@@ -5889,8 +5889,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
"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_react15.createElement)(PlusIcon)),
-+ shown ? (0, import_react_dom5.createPortal)((0, import_react15.createElement)(
++ }, (0, import_react16.createElement)(PlusIcon)),
++ shown ? (0, import_react_dom5.createPortal)((0, import_react16.createElement)(
"div",
{
ref: menu,
-@@ -5900,9 +5900,9 @@
+@@ -5900,9 +5900,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
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_react15.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react15.createElement)(PaperclipIcon), (0, import_react15.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
-+ canReference ? (0, import_react15.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react15.createElement)(ChatsIcon), (0, import_react15.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
-+ (0, import_react15.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react15.createElement)(ClockIcon), (0, import_react15.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")))
), document.body) : null
);
}
-@@ -5922,15 +5922,55 @@
+@@ -5922,15 +5922,55 @@ var conversationOf = (roster, sessionId) => {
function mentionRows(roster, sessionId, query = "") {
const here = conversationOf(roster, sessionId);
if (here === null) return null;
@@ -7263,7 +9739,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 @@
+@@ -5959,14 +5959,31 @@ function atSources({ roster, sessions, launched, iconFor }) {
order: -20,
showGroupTitle: false,
async candidates(session, { query }) {
@@ -7299,7 +9775,7 @@ diff --git a/client.js b/client.js
},
onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0
};
-@@ -5995,6 +5995,83 @@
+@@ -5995,6 +5995,83 @@ function atSources({ roster, sessions, launched, iconFor }) {
return [bots, chats];
}
@@ -7383,10 +9859,61 @@ diff --git a/client.js b/client.js
// src/client/speech.js
var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle";
var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]);
-@@ -6121,232 +6121,8 @@
- };
- }
-
+@@ -6070,283 +6070,59 @@ async function openCapture() {
+ analyser.getFloatTimeDomainData(frame);
+ let sum = 0;
+ for (const sample of frame) sum += sample * sample;
+- return Math.min(1, Math.sqrt(sum / frame.length) * 4);
+- },
+- cancel: close,
+- async finish(maxSeconds) {
+- if (closed || recorder.state === "inactive") {
+- close();
+- return null;
+- }
+- try {
+- await new Promise((resolve) => {
+- 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";
+- };
+-}
+-
-// src/client/overlay-hooks.js
-var import_react11 = require("react");
-var escapeLayers = [];
@@ -7609,15 +10136,64 @@ diff --git a/client.js b/client.js
- )
- )
- );
--}
--
++ return Math.min(1, Math.sqrt(sum / frame.length) * 4);
++ },
++ cancel: close,
++ async finish(maxSeconds) {
++ if (closed || recorder.state === "inactive") {
++ close();
++ return null;
++ }
++ try {
++ await new Promise((resolve) => {
++ 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";
++ };
+ }
+
// src/client/model-menu.js
-var import_react13 = require("react");
-+var import_react16 = require("react");
++var import_react17 = require("react");
var import_react_dom6 = require("react-dom");
var MENU_EXIT_MS = 120;
function groupModels(models) {
-@@ -6367,17 +6367,10 @@
+@@ -6367,17 +6367,10 @@ function groupModels(models) {
});
}
var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref);
@@ -7632,83 +10208,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_react16.useState)(void 0);
-+ (0, import_react16.useEffect)(() => {
++ const [fetched, setFetched] = (0, import_react17.useState)(void 0);
++ (0, import_react17.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 @@
+@@ -6402,12 +6402,12 @@ function effortRows(reasoning) {
];
}
function EffortRows({ reasoning, current, onPick }) {
- return (0, import_react13.createElement)(
- import_react13.Fragment,
-+ return (0, import_react16.createElement)(
-+ import_react16.Fragment,
++ return (0, import_react17.createElement)(
++ import_react17.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_react16.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ (0, import_react16.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
-+ effortRows(reasoning).map((row) => (0, import_react16.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)(
"button",
{
key: row.effort ?? "default",
-@@ -6416,17 +6416,17 @@
+@@ -6416,17 +6416,17 @@ function EffortRows({ reasoning, current, onPick }) {
"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_react16.createElement)("span", { className: "bt-menu-label" }, row.label),
-+ (0, import_react16.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react16.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)
))
);
}
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_react16.createElement)(
-+ import_react16.Fragment,
++ return groupModels(roster.models).map(([provider, list]) => (0, import_react17.createElement)(
++ import_react17.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_react16.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
-+ list.map((model) => (0, import_react16.createElement)(
++ (0, import_react17.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
++ list.map((model) => (0, import_react17.createElement)(
"button",
{
key: model.ref,
-@@ -6436,20 +6436,20 @@
+@@ -6436,20 +6436,20 @@ function ModelRows({ roster, current, onPick }) {
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_react16.createElement)("span", { className: "bt-menu-label" }, model.name),
-+ model.ref === roster.defaultModel ? (0, import_react16.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null,
-+ (0, import_react16.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react16.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)
))
));
}
-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_react16.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_react16.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" }));
function useAnchoredMenu() {
- const [place, setPlace] = (0, import_react13.useState)(null);
-+ const [place, setPlace] = (0, import_react16.useState)(null);
++ const [place, setPlace] = (0, import_react17.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_react16.useRef)(null);
-+ const menu = (0, import_react16.useRef)(null);
++ const button = (0, import_react17.useRef)(null);
++ const menu = (0, import_react17.useRef)(null);
const close = () => setPlace(null);
- (0, import_react13.useEffect)(() => {
-+ (0, import_react16.useEffect)(() => {
++ (0, import_react17.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 @@
+@@ -6484,6 +6484,7 @@ function useAnchoredMenu() {
return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close };
}
function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) {
@@ -7716,53 +10292,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 @@
+@@ -6511,10 +6511,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_react13.createElement)(
- import_react13.Fragment,
-+ return (0, import_react16.createElement)(
-+ import_react16.Fragment,
++ return (0, import_react17.createElement)(
++ import_react17.Fragment,
null,
- (0, import_react13.createElement)(
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
"button",
{
ref: button,
-@@ -6527,37 +6527,38 @@
+@@ -6527,37 +6527,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_react13.createElement)(
-+ (0, import_react16.createElement)(
++ (0, import_react17.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_react16.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")),
-+ shown ? (0, import_react16.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_react13.createElement)(Caret)
-+ (0, import_react16.createElement)(Caret)
++ (0, import_react17.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom6.createPortal)((0, import_react13.createElement)(
-+ shownPlace ? (0, import_react_dom6.createPortal)((0, import_react16.createElement)(
++ shownPlace ? (0, import_react_dom6.createPortal)((0, import_react17.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_react16.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react16.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")),
-+ effortRows(reasoning).length > 0 ? (0, import_react16.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
-+ (0, import_react16.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ (0, import_react16.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: () => {
close();
- openHarnessSettings();
- } }, (0, import_react13.createElement)(SlidersIcon), (0, import_react13.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…")))
+ openHarnessModels();
-+ } }, (0, import_react16.createElement)(SlidersIcon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…")))
++ } }, (0, import_react17.createElement)(SlidersIcon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…")))
), document.body) : null
);
}
@@ -7771,94 +10347,94 @@ 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_react16.useState)("");
-+ const [error, setError] = (0, import_react16.useState)("");
++ const [busy, setBusy] = (0, import_react17.useState)("");
++ const [error, setError] = (0, import_react17.useState)("");
const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready;
- (0, import_react13.useEffect)(() => {
-+ (0, import_react16.useEffect)(() => {
++ (0, import_react17.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_react16.createElement)(RemoteGoneDock, { bot }) : null;
++ if (bot.remote) return bot.remote.gone ? (0, import_react17.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 @@
+@@ -6570,12 +6570,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_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_react16.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react16.createElement)(KeyHelpLink, { key: "key-help" })] : [
-+ ...groupModels(models).map(([provider, list]) => (0, import_react16.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)(
"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_react16.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
-+ (0, import_react16.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react16.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", {
key: model.ref,
type: "button",
className: "bt-model-chip",
-@@ -6584,50 +6584,115 @@
+@@ -6584,50 +6584,115 @@ 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_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)))
-+ }, model.name, busy === model.ref ? (0, import_react16.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)))
)),
- (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_react16.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react16.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"))
];
- return (0, import_react13.createElement)(
-+ return (0, import_react16.createElement)(
++ return (0, import_react17.createElement)(
"div",
{ className: "bt-model-dock" },
- (0, import_react13.createElement)(
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
"div",
{ className: "bt-astack", "data-lead": "" },
- (0, import_react13.createElement)(
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
"div",
{ className: "bt-msg" },
- (0, import_react13.createElement)(
-+ (0, import_react16.createElement)(
++ (0, import_react17.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_react16.createElement)(BotMark, { bot, size: 22 }),
-+ (0, import_react16.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_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_react16.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices),
-+ error ? (0, import_react16.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
++ );
++}
+function RemoteGoneDock({ bot }) {
-+ return (0, import_react16.createElement)(
++ return (0, import_react17.createElement)(
+ "div",
+ { className: "bt-model-dock", "data-flybot": "remote-gone" },
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
+ "div",
+ { className: "bt-astack", "data-lead": "" },
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
+ "div",
+ { className: "bt-msg" },
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
+ "div",
+ { className: "bt-msg-line" },
-+ (0, import_react16.createElement)(BotMark, { bot, size: 22 }),
-+ (0, import_react16.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_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 }))
+ )
+ )
+ )
-+ );
-+}
+ );
+ }
function setupChoices(actions) {
return [
actions.canSignIn?.() ? [t("Sign in to DeepSeek"), () => actions.signIn()] : null,
@@ -7869,7 +10445,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_react16.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_react17.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 ?? "");
@@ -7893,22 +10469,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_react16.createElement)(
++ return (0, import_react17.createElement)(
+ "div",
+ { className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 },
-+ (0, import_react16.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
++ (0, import_react17.createElement)(
+ "div",
+ { className: "bt-turn-error-copy" },
-+ (0, import_react16.createElement)("span", { className: "bt-turn-error-title" }, title),
-+ (0, import_react16.createElement)("span", { className: "bt-turn-error-message" }, message),
-+ code ? (0, import_react16.createElement)("code", { className: "bt-turn-error-code" }, code) : null
++ (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
+ ),
-+ choices.length > 0 ? (0, import_react16.createElement)(
++ choices.length > 0 ? (0, import_react17.createElement)(
+ "div",
+ { className: "bt-turn-error-actions" },
-+ choices.map(([label, run]) => (0, import_react16.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)),
-+ deepseek ? (0, import_react16.createElement)(KeyHelpLink, { key: "key-help" }) : null
++ 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
+ ) : null
+ );
+}
@@ -7917,22 +10493,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_react16.createElement)(SignInHost, { key: open, entry, actions });
++ return (0, import_react17.createElement)(SignInHost, { key: open, entry, actions });
}
function SignInHost({ entry, actions }) {
- const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {});
-+ const [ops] = (0, import_react16.useState)(() => entry.inject?.() ?? {});
++ const [ops] = (0, import_react17.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_react16.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot()));
-+ const [hooks] = (0, import_react16.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) }));
-+ return (0, import_react16.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, {
...ops,
...hooks,
t: actions.accountT(),
-@@ -6639,6 +6639,16 @@
+@@ -6639,6 +6639,16 @@ function SignInHost({ entry, actions }) {
});
}
var MODEL_CSS = `
@@ -7949,12 +10525,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 @@
+@@ -6675,18 +6675,88 @@ var MODEL_CSS = `
@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_react17 = require("react");
++var import_react18 = 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.",
@@ -7979,12 +10555,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_react17.useState)({ busy: false, outcome: void 0 });
-+ const live = (0, import_react17.useRef)(true);
-+ (0, import_react17.useEffect)(() => () => {
++ const [state, setState] = (0, import_react18.useState)({ busy: false, outcome: void 0 });
++ const live = (0, import_react18.useRef)(true);
++ (0, import_react18.useEffect)(() => () => {
+ live.current = false;
+ }, []);
-+ (0, import_react17.useEffect)(() => {
++ (0, import_react18.useEffect)(() => {
+ setState({ busy: false, outcome: void 0 });
+ }, [bot.id, bot.model]);
+ const start = () => {
@@ -7998,21 +10574,21 @@ diff --git a/client.js b/client.js
+ });
+ };
+ const { busy, outcome } = state;
-+ return (0, import_react17.createElement)(
++ return (0, import_react18.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row bt-model-test" },
-+ (0, import_react17.createElement)(
++ (0, import_react18.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react17.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")),
-+ (0, import_react17.createElement)(
++ (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)(
+ "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_react17.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
++ outcome?.code ? (0, import_react18.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
+ )
+ ),
-+ (0, import_react17.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection"))
++ (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"))
+ );
+}
+var MODEL_TEST_CSS = `
@@ -8025,11 +10601,11 @@ diff --git a/client.js b/client.js
+
// src/client/folder-menu.js
-var import_react14 = require("react");
-+var import_react18 = require("react");
++var import_react19 = 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_react18.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react18.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);
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);
@@ -8037,174 +10613,174 @@ 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_react18.useState)(null);
-+ const [view, setView] = (0, import_react18.useState)("list");
-+ const [draft, setDraft] = (0, import_react18.useState)("");
-+ const [listing, setListing] = (0, import_react18.useState)(null);
-+ const [problem, setProblem] = (0, import_react18.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 fallback = group ? t("Each Bot's own folder") : t("Team workspace");
const toggle = () => {
if (isOpen) {
-@@ -6719,35 +6719,35 @@
+@@ -6719,35 +6719,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_react14.createElement)(
-+ const listView = (0, import_react18.createElement)(
++ const listView = (0, import_react19.createElement)(
"div",
{ key: "list", className: "bt-folder-view" },
- (0, import_react14.createElement)(
-+ (0, import_react18.createElement)(
++ (0, import_react19.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_react18.createElement)("span", { className: "bt-menu-label" }, fallback),
-+ (0, import_react18.createElement)(Tick, { on: !value })
++ (0, import_react19.createElement)("span", { className: "bt-menu-label" }, fallback),
++ (0, import_react19.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_react18.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null,
-+ folders.map((folder) => (0, import_react18.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)(
"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_react18.createElement)("span", { className: "bt-folder-copy" }, (0, import_react18.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react18.createElement)("span", { className: "bt-folder-path" }, folder.path)),
-+ (0, import_react18.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_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_react18.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ info?.picker === "native" ? (0, import_react18.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react18.createElement)(FolderIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null,
-+ info?.picker === "browse" ? (0, import_react18.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react18.createElement)(FolderIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null,
-+ (0, import_react18.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: () => {
setDraft(value ?? "");
setView("type");
- } }, (0, import_react14.createElement)(PencilIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…")))
-+ } }, (0, import_react18.createElement)(PencilIcon), (0, import_react18.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…")))
);
- const typeView = (0, import_react14.createElement)(
-+ const typeView = (0, import_react18.createElement)(
++ const typeView = (0, import_react19.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_react18.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react18.createElement)(ChevronLeftIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
-+ (0, import_react18.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", {
className: "bt-folder-input",
autoFocus: true,
value: draft,
-@@ -6759,33 +6759,33 @@
+@@ -6759,33 +6759,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false }
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_react18.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"))
);
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_react18.createElement)(
++ const browseView = listing ? (0, import_react19.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_react18.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react18.createElement)(ChevronLeftIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
-+ (0, import_react18.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path),
-+ parent ? (0, import_react18.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react18.createElement)(FolderIcon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null,
-+ inside.map((entry) => (0, import_react18.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)(
"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_react18.createElement)(FolderIcon),
-+ (0, import_react18.createElement)("span", { className: "bt-menu-label" }, entry.name),
-+ (0, import_react18.createElement)(ChevronRightIcon)
++ (0, import_react19.createElement)(FolderIcon),
++ (0, import_react19.createElement)("span", { className: "bt-menu-label" }, entry.name),
++ (0, import_react19.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_react18.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null,
-+ listing.truncated ? (0, import_react18.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null,
-+ (0, import_react18.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ (0, import_react18.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"))
) : null;
const shownView = view === "type" ? typeView : view === "browse" && browseView ? browseView : listView;
- return (0, import_react14.createElement)(
- import_react14.Fragment,
-+ return (0, import_react18.createElement)(
-+ import_react18.Fragment,
++ return (0, import_react19.createElement)(
++ import_react19.Fragment,
null,
- (0, import_react14.createElement)(
-+ (0, import_react18.createElement)(
++ (0, import_react19.createElement)(
"button",
{
ref: button,
-@@ -6798,19 +6798,19 @@
+@@ -6798,19 +6798,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false }
title: value,
onClick: toggle
},
- (0, import_react14.createElement)(
-+ (0, import_react18.createElement)(
++ (0, import_react19.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_react18.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback),
-+ value ? (0, import_react18.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_react14.createElement)(Caret)
-+ (0, import_react18.createElement)(Caret)
++ (0, import_react19.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react14.createElement)(
-+ shownPlace ? (0, import_react_dom7.createPortal)((0, import_react18.createElement)(
++ shownPlace ? (0, import_react_dom7.createPortal)((0, import_react19.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_react18.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
++ problem ? (0, import_react19.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
), document.body) : null
);
}
-@@ -6830,8 +6830,8 @@
+@@ -6830,8 +6830,8 @@ var FOLDER_CSS = `
`;
// src/client/schedules.js
-var import_react15 = require("react");
-var POLL_MS = 3e4;
-+var import_react19 = require("react");
-+var POLL_MS3 = 3e4;
++var import_react20 = require("react");
++var POLL_MS4 = 3e4;
var shared = null;
var listeners = /* @__PURE__ */ new Set();
function publish(view) {
-@@ -6839,8 +6839,8 @@
+@@ -6839,8 +6839,8 @@ function publish(view) {
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_react19.useState)(shared);
-+ (0, import_react19.useEffect)(() => {
++ const [view, setView] = (0, import_react20.useState)(shared);
++ (0, import_react20.useEffect)(() => {
listeners.add(setView);
let stopped = false;
let timer;
-@@ -6848,7 +6848,7 @@
+@@ -6848,7 +6848,7 @@ function useSchedules(actions) {
if (!stopped) publish(next);
}).catch(() => {
}).finally(() => {
- if (!stopped) timer = setTimeout(load, POLL_MS);
-+ if (!stopped) timer = setTimeout(load, POLL_MS3);
++ if (!stopped) timer = setTimeout(load, POLL_MS4);
});
load();
return () => {
-@@ -6896,11 +6896,25 @@
+@@ -6896,11 +6896,25 @@ function timingText(task) {
return "";
}
}
@@ -8229,13 +10805,13 @@ diff --git a/client.js b/client.js
+ }
+}
+function TaskRow({ task, actions, onView, onEdit, showBot, roster }) {
-+ const [confirming, setConfirming] = (0, import_react19.useState)(false);
-+ const [busy, setBusy] = (0, import_react19.useState)(false);
-+ (0, import_react19.useEffect)(() => {
++ const [confirming, setConfirming] = (0, import_react20.useState)(false);
++ const [busy, setBusy] = (0, import_react20.useState)(false);
++ (0, import_react20.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
-@@ -6913,20 +6913,48 @@
+@@ -6913,20 +6913,48 @@ function TaskRow({ task, actions, onView, showBot, roster }) {
setBusy(true);
void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false));
};
@@ -8247,7 +10823,7 @@ diff --git a/client.js b/client.js
+ onView(next);
+ }, () => setBusy(false));
+ };
-+ const [retryError, setRetryError] = (0, import_react19.useState)("");
++ const [retryError, setRetryError] = (0, import_react20.useState)("");
+ const retry = () => {
+ setBusy(true);
+ setRetryError("");
@@ -8264,45 +10840,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_react19.createElement)(
++ return (0, import_react20.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_react19.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
-+ (0, import_react19.createElement)(
++ showBot ? (0, import_react20.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
++ (0, import_react20.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_react19.createElement)("span", { className: "bt-task-title" }, task.title),
-+ (0, import_react19.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_react19.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null,
-+ last ? (0, import_react19.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null,
-+ retryError ? (0, import_react19.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : 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_react15.createElement)("span", { className: "bt-task-next" }, nextText(task)),
- (0, import_react15.createElement)("button", {
-+ (0, import_react19.createElement)("span", { className: "bt-task-next" }, nextText(task)),
-+ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react19.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_react19.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react19.createElement)(PencilIcon)) : null,
-+ stoppable ? (0, import_react19.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null,
-+ (0, import_react19.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", {
type: "button",
className: "bt-mini bt-task-remove",
"data-confirm": confirming || void 0,
-@@ -6934,7 +6934,7 @@
+@@ -6934,7 +6934,7 @@ function TaskRow({ task, actions, onView, 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_react15.createElement)(TrashIcon))
-+ }, confirming ? t("Delete") : (0, import_react19.createElement)(TrashIcon))
++ }, confirming ? t("Delete") : (0, import_react20.createElement)(TrashIcon))
);
}
var pad = (number) => String(number).padStart(2, "0");
-@@ -6945,22 +6945,44 @@
+@@ -6945,22 +6945,44 @@ function inAnHour() {
}
var KINDS = [["at", "Once"], ["daily", "Every day"], ["weekly", "Every week"], ["every", "Repeat"]];
var UNITS = [["minutes", 1, "minutes"], ["hours", 60, "hours"], ["days", 1440, "days"]];
@@ -8340,17 +10916,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_react19.useState)(task ? task.botId ?? task.roomId : start ?? roster.bots[0]?.id ?? "");
-+ const [title, setTitle] = (0, import_react19.useState)(first.title);
-+ const [prompt, setPrompt] = (0, import_react19.useState)(first.prompt);
-+ const [kind, setKind] = (0, import_react19.useState)(first.kind);
-+ const [at, setAt] = (0, import_react19.useState)(first.at);
-+ const [time, setTime] = (0, import_react19.useState)(first.time);
-+ const [weekdays, setWeekdays] = (0, import_react19.useState)(first.weekdays);
-+ const [count, setCount] = (0, import_react19.useState)(first.count);
-+ const [unit, setUnit] = (0, import_react19.useState)(first.unit);
-+ const [busy, setBusy] = (0, import_react19.useState)(false);
-+ const [error, setError] = (0, import_react19.useState)("");
++ 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 timing = () => {
+ if (kind === "cron") return { kind: "keep" };
if (kind === "at") return { kind, at: new Date(at).toISOString() };
@@ -8360,7 +10936,7 @@ diff --git a/client.js b/client.js
};
const submit = (event) => {
event.preventDefault();
-@@ -6975,146 +6975,185 @@
+@@ -6975,146 +6975,185 @@ function TaskForm({ roster, actions, start, onDone, onCancel }) {
setBusy(false);
return;
}
@@ -8373,60 +10949,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_react19.createElement)(
++ return (0, import_react20.createElement)(
"form",
{ className: "bt-set-card bt-task-form", onSubmit: submit },
- (0, import_react15.createElement)(
-+ (0, import_react19.createElement)(
++ (0, import_react20.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_react19.createElement)("span", { className: "bt-set-copy" }, (0, import_react19.createElement)("span", { className: "bt-bs-label" }, t("Send to"))),
-+ (0, import_react19.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)(
"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_react19.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react19.createElement)("option", { key: bot.id, value: bot.id }, bot.name))),
-+ roster.rooms.length > 0 ? (0, import_react19.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react19.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_react15.createElement)(
-+ (0, import_react19.createElement)(
++ (0, import_react20.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_react19.createElement)("span", { className: "bt-set-copy" }, (0, import_react19.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
-+ (0, import_react19.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_react15.createElement)(
-+ (0, import_react19.createElement)(
++ (0, import_react20.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_react19.createElement)("span", { className: "bt-set-copy" }, (0, import_react19.createElement)("span", { className: "bt-bs-label" }, t("What to do"))),
-+ (0, import_react19.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_react15.createElement)(
-+ (0, import_react19.createElement)(
++ (0, import_react20.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_react19.createElement)("span", { className: "bt-set-copy" }, (0, import_react19.createElement)("span", { className: "bt-bs-label" }, t("When"))),
-+ (0, import_react19.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react19.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))),
-+ (0, import_react19.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)(
"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_react19.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null,
-+ kind === "at" ? (0, import_react19.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_react19.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react19.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", {
key: day,
type: "button",
className: "bt-task-day",
@@ -8436,34 +11012,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_react19.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_react19.createElement)(
-+ import_react19.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,
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_react19.createElement)("span", { className: "bt-task-every" }, t("Every")),
-+ (0, import_react19.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_react19.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)(
"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_react19.createElement)("option", { key: id, value: id }, t(label)))
++ UNITS.map(([id, , label]) => (0, import_react20.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_react19.createElement)("div", { className: "bt-set-row" }, (0, import_react19.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
-+ (0, import_react19.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)(
"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_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
-+ (0, import_react19.createElement)(
++ (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
++ (0, import_react20.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")
@@ -8478,17 +11054,17 @@ diff --git a/client.js b/client.js
+ actions.selectPanel("plugins");
+}
+function AutomationOff({ actions }) {
-+ return (0, import_react19.createElement)(
++ return (0, import_react20.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_react19.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react19.createElement)(ClockIcon)),
-+ (0, import_react19.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")),
-+ (0, import_react19.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")),
-+ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
++ (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"))
);
}
function SchedulesPage({ roster, actions, start }) {
@@ -8496,69 +11072,69 @@ 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_react19.useState)(Boolean(start));
-+ const [editing, setEditing] = (0, import_react19.useState)(null);
-+ if (view === null) return (0, import_react19.createElement)("div", { className: "bt-set-hint" }, t("Loading…"));
-+ if (!view.available) return (0, import_react19.createElement)(AutomationOff, { 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 shown = (next) => {
publish(next);
setAdding(false);
+ setEditing(null);
- };
++ };
+ const edit = (task) => {
+ setAdding(false);
+ setEditing(task);
-+ };
+ };
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_react19.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_react19.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)])
].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_react19.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react19.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => {
++ 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: () => {
+ setEditing(null);
+ setAdding(true);
-+ } }, (0, import_react19.createElement)(PlusIcon), t("New task"))),
-+ byChat.length === 0 && !adding ? (0, import_react19.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react19.createElement)("div", { className: "bt-set-row" }, (0, import_react19.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react19.createElement)(
++ } }, (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)(
"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_react19.createElement)("h3", { className: "bt-task-head" }, avatar, name),
-+ (0, import_react19.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) })
++ (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) })
))
];
}
+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_react19.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit }));
-+ return (0, import_react19.createElement)(
-+ import_react19.Fragment,
++ 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,
+ null,
-+ open ? (0, import_react19.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null,
-+ rows.length + extra.length > 0 ? (0, import_react19.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : 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
+ );
+}
function BotSchedules({ bot, roster, actions }) {
const view = useSchedules(actions);
-+ const [editing, setEditing] = (0, import_react19.useState)(null);
++ const [editing, setEditing] = (0, import_react20.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_react19.createElement)(
++ return (0, import_react20.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react15.createElement)("h3", null, t("Scheduled tasks")),
- (0, import_react15.createElement)(
-+ (0, import_react19.createElement)("h3", null, t("Scheduled tasks")),
-+ !view.available ? (0, import_react19.createElement)(
++ (0, import_react20.createElement)("h3", null, t("Scheduled tasks")),
++ !view.available ? (0, import_react20.createElement)(
"div",
{ className: "bt-set-card" },
- !view.available ? (0, import_react15.createElement)(
-+ (0, import_react19.createElement)(
++ (0, import_react20.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."))),
@@ -8568,10 +11144,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_react19.createElement)("span", { className: "bt-set-copy" }, (0, import_react19.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))),
-+ (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
++ (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_react19.createElement)(TaskList, {
++ ) : (0, import_react20.createElement)(TaskList, {
+ tasks,
+ editing,
+ roster,
@@ -8582,7 +11158,7 @@ diff --git a/client.js b/client.js
+ setEditing(null);
+ },
+ onCancel: () => setEditing(null),
-+ extra: [(0, import_react19.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react19.createElement)(PlusIcon), t("New task"))]
++ 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"))]
+ })
);
}
@@ -8590,25 +11166,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_react19.createElement)(
++ return (0, import_react20.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_react19.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")),
-+ tasks.length === 0 ? (0, import_react19.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_react19.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react19.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)(
"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_react19.createElement)(ClockIcon),
-+ (0, import_react19.createElement)("span", { className: "bt-task-line-title" }, task.title),
-+ (0, import_react19.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))
)))
);
}
-@@ -7127,6 +7127,12 @@
+@@ -7127,6 +7127,12 @@ var SCHEDULE_CSS = `
.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}
@@ -8621,40 +11197,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 @@
+@@ -7158,7 +7158,7 @@ var SCHEDULE_CSS = `
`;
// src/client/capability-settings.js
-var import_react16 = require("react");
-+var import_react20 = require("react");
++var import_react21 = 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 @@
+@@ -7172,11 +7172,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_react16.createElement)(
-+ return (0, import_react20.createElement)(
++ return (0, import_react21.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_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, t(label))),
-+ (0, import_react20.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", {
className: multiline ? "bt-input bt-input-area" : "bt-input",
value,
disabled,
-@@ -7188,7 +7188,7 @@
+@@ -7188,7 +7188,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl
);
}
function CapabilityForm({ draft, setDraft, items, actions, onClose }) {
- const [error, setError] = (0, import_react16.useState)("");
-+ const [error, setError] = (0, import_react20.useState)("");
++ const [error, setError] = (0, import_react21.useState)("");
const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value }));
const submit = async (event) => {
event.preventDefault();
-@@ -7199,7 +7199,7 @@
+@@ -7199,7 +7199,7 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) {
if (draft.kind === "mcp" && draft.transport === "stdio") {
payload.transport = "stdio";
payload.command = draft.command;
@@ -8663,94 +11239,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 @@
+@@ -7219,95 +7219,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_react16.createElement)(
-+ return (0, import_react20.createElement)(
++ return (0, import_react21.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_react20.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }),
-+ (0, import_react20.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)(
"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_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, t("Type"))),
-+ (0, import_react20.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)(
"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_react20.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")))
)
),
- (0, import_react16.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
- draft.kind === "mcp" ? (0, import_react16.createElement)(
-+ (0, import_react20.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
-+ draft.kind === "mcp" ? (0, import_react20.createElement)(
++ (0, import_react21.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
++ draft.kind === "mcp" ? (0, import_react21.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_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, t("Connection"))),
-+ (0, import_react20.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)(
"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_react20.createElement)("option", { value: "stdio" }, t("Local command")),
-+ (0, import_react20.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"))
)
) : null,
- draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react16.createElement)(
-+ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react20.createElement)(
++ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react21.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_react20.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }),
-+ (0, import_react20.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }),
-+ (0, import_react20.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"}') })
) : null,
- draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react16.createElement)(
-+ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react20.createElement)(
++ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react21.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_react20.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }),
-+ (0, import_react20.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"}') })
) : 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_react20.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null,
-+ draft.kind === "pack" ? (0, import_react20.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)(
"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_react20.createElement)("div", { className: "bt-set-row" }, (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react20.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)(
"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_react20.createElement)("span", { className: "bt-set-copy" }, (0, import_react20.createElement)("span", { className: "bt-bs-label" }, item.name)),
-+ (0, import_react20.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)) })
))
) : null,
- error ? (0, import_react16.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
- (0, import_react16.createElement)(
-+ error ? (0, import_react20.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
-+ (0, import_react20.createElement)(
++ error ? (0, import_react21.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
++ (0, import_react21.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_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")),
-+ (0, import_react20.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"))
)
);
}
@@ -8758,32 +11334,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_react20.createElement)(
++ return (0, import_react21.createElement)(
"div",
{ className: "bt-connector" },
- (0, import_react16.createElement)(
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)(
"div",
{ className: "bt-connector-head" },
- (0, import_react16.createElement)(
-+ (0, import_react20.createElement)(
++ (0, import_react21.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_react20.createElement)("span", { className: "bt-connector-name" }, item.name),
-+ (0, import_react20.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_react16.createElement)(
-+ (0, import_react20.createElement)(
++ (0, import_react21.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_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null,
-+ (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")),
-+ (0, import_react20.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"))
)
)
);
@@ -8793,59 +11369,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_react20.useState)(null);
-+ const [confirming, setConfirming] = (0, import_react20.useState)("");
-+ (0, import_react20.useEffect)(() => {
++ const [draft, setDraft] = (0, import_react21.useState)(null);
++ const [confirming, setConfirming] = (0, import_react21.useState)("");
++ (0, import_react21.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(""), 4e3);
return () => clearTimeout(timer);
-@@ -7330,20 +7330,20 @@
+@@ -7330,20 +7330,20 @@ function CapabilitiesPage({ roster, actions, run }) {
setConfirming("");
void run(actions.deleteCapability(item.name));
};
- return (0, import_react16.createElement)(
-+ return (0, import_react20.createElement)(
++ return (0, import_react21.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react16.createElement)("h3", null, t("Shared capabilities")),
- (0, import_react16.createElement)(
-+ (0, import_react20.createElement)("h3", null, t("Shared capabilities")),
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)("h3", null, t("Shared capabilities")),
++ (0, import_react21.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_react20.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"))
),
- draft ? (0, import_react16.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
- (0, import_react16.createElement)(
-+ draft ? (0, import_react20.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
-+ (0, import_react20.createElement)(
++ draft ? (0, import_react21.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
++ (0, import_react21.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_react20.createElement)("div", { className: "bt-set-row" }, (0, import_react20.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react20.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, {
key: item.name,
item,
confirming: confirming === item.name,
-@@ -7359,10 +7359,10 @@
+@@ -7359,10 +7359,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_react16.createElement)(
-+ return (0, import_react20.createElement)(
++ return (0, import_react21.createElement)(
Card2,
{ title: t("Capabilities") },
- items.map((item) => (0, import_react16.createElement)(SwitchRow2, {
-+ items.map((item) => (0, import_react20.createElement)(SwitchRow2, {
++ items.map((item) => (0, import_react21.createElement)(SwitchRow2, {
key: item.name,
label: item.name,
hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0,
-@@ -7372,9 +7372,55 @@
+@@ -7372,9 +7372,55 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow
);
}
+// src/client/connector-bots.js
-+var import_react21 = require("react");
++var import_react22 = 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));
@@ -8861,31 +11437,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_react21.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_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)) });
+}
+function ConnectorBots({ connector, roster, actions, run, SwitchRow: SwitchRow2 }) {
+ const bots = ownBots(roster);
-+ return (0, import_react21.createElement)(
++ return (0, import_react22.createElement)(
+ "div",
+ { className: "bt-connector-bots" },
-+ bots.length === 0 ? (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react21.createElement)(
++ bots.length === 0 ? (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react22.createElement)(
+ "div",
+ { key: bot.id, className: "bt-connector-bot" },
-+ (0, import_react21.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
++ (0, import_react22.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
+ )),
-+ (0, import_react21.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_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."))
+ );
+}
+function BotConnectorsCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) {
+ const connected = connectedConnectors(roster);
+ if (connected.length === 0) return null;
-+ return (0, import_react21.createElement)(
++ return (0, import_react22.createElement)(
+ Card2,
+ { title: t("Connectors") },
-+ connected.map((connector) => (0, import_react21.createElement)(
++ connected.map((connector) => (0, import_react22.createElement)(
+ "div",
+ { key: connector.id, className: "bt-connector-bot" },
-+ (0, import_react21.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
++ (0, import_react22.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
+ ))
+ );
+}
@@ -8893,19 +11469,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_react22 = require("react");
-+var POLL_MS4 = 1500;
++var import_react23 = 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 @@
+@@ -7384,13 +7384,13 @@ function noteCount(botId, count) {
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_react22.useState)(counts.get(botId) ?? null);
-+ (0, import_react22.useEffect)(() => {
++ const [count, setCount] = (0, import_react23.useState)(counts.get(botId) ?? null);
++ (0, import_react23.useEffect)(() => {
const listener = () => setCount(counts.get(botId) ?? null);
countListeners.add(listener);
listener();
@@ -8915,207 +11491,207 @@ diff --git a/client.js b/client.js
if (!stop) noteCount(botId, view.entries.length);
}).catch(() => {
});
-@@ -7404,16 +7404,16 @@
+@@ -7404,16 +7404,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_react17.createElement)(
-+ return (0, import_react22.createElement)(
++ return (0, import_react23.createElement)(
"div",
null,
- (0, import_react17.createElement)("div", { className: "bt-section-title" }, t("Memory")),
- (0, import_react17.createElement)(
-+ (0, import_react22.createElement)("div", { className: "bt-section-title" }, t("Memory")),
-+ (0, import_react22.createElement)(
++ (0, import_react23.createElement)("div", { className: "bt-section-title" }, t("Memory")),
++ (0, import_react23.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_react22.createElement)(MemoryIcon),
-+ (0, import_react22.createElement)("span", { className: "bt-mem-row-copy" }, label),
-+ (0, import_react22.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react22.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))
)
);
}
-@@ -7428,45 +7428,45 @@
+@@ -7428,45 +7428,45 @@ function ago(at, now) {
return format.format(-Math.round(hours / 24), "day");
}
function Skeleton({ label }) {
- return (0, import_react17.createElement)(
-+ return (0, import_react22.createElement)(
++ return (0, import_react23.createElement)(
"div",
{ className: "bt-mem-skel", "aria-busy": true },
- [0, 1].map((block) => (0, import_react17.createElement)(
-+ [0, 1].map((block) => (0, import_react22.createElement)(
++ [0, 1].map((block) => (0, import_react23.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_react22.createElement)("span", { className: "bt-mem-skel-head" }),
-+ (0, import_react22.createElement)("span", null),
-+ (0, import_react22.createElement)("span", null),
-+ (0, import_react22.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%" } })
)),
- label ? (0, import_react17.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
-+ label ? (0, import_react22.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
++ label ? (0, import_react23.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_react22.createElement)(
++ return (0, import_react23.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_react22.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react22.createElement)(MemoryIcon)),
-+ (0, import_react22.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })),
-+ (0, import_react22.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 }))
);
}
- const failed = view.error && !view.summarizing ? (0, import_react17.createElement)(
-+ const failed = view.error && !view.summarizing ? (0, import_react22.createElement)(
++ const failed = view.error && !view.summarizing ? (0, import_react23.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_react22.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_react22.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"))
) : 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_react22.createElement)(import_react22.Fragment, null, failed, failed ? null : (0, import_react22.createElement)(Skeleton, { label: t("Writing the summary…") }));
-+ return (0, import_react22.createElement)(
-+ import_react22.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,
null,
failed,
- (0, import_react17.createElement)(
-+ (0, import_react22.createElement)(
++ (0, import_react23.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_react22.createElement)("h3", { key: index }, block.text) : (0, import_react22.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))
)
);
}
-@@ -7478,9 +7478,9 @@
+@@ -7478,9 +7478,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => {
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_react22.useState)(false);
-+ const [busy, setBusy] = (0, import_react22.useState)(false);
-+ (0, import_react22.useEffect)(() => {
++ const [confirming, setConfirming] = (0, import_react23.useState)(false);
++ const [busy, setBusy] = (0, import_react23.useState)(false);
++ (0, import_react23.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
-@@ -7495,16 +7495,16 @@
+@@ -7495,16 +7495,16 @@ function EntryRow({ entry, remove }) {
setBusy(true);
if (!await remove(entry)) setBusy(false);
};
- return (0, import_react17.createElement)(
-+ return (0, import_react22.createElement)(
++ return (0, import_react23.createElement)(
"li",
{ className: "bt-mem-entry", "data-busy": busy || void 0 },
- (0, import_react17.createElement)(
-+ (0, import_react22.createElement)(
++ (0, import_react23.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_react22.createElement)("span", { className: "bt-mem-entry-text" }, entry.text),
-+ meta ? (0, import_react22.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_react17.createElement)("button", {
-+ (0, import_react22.createElement)("button", {
++ (0, import_react23.createElement)("button", {
type: "button",
className: "bt-mem-remove",
"data-confirm": confirming || void 0,
-@@ -7512,7 +7512,7 @@
+@@ -7512,7 +7512,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_react17.createElement)(TrashIcon))
-+ }, confirming ? t("Remove") : (0, import_react22.createElement)(TrashIcon))
++ }, confirming ? t("Remove") : (0, import_react23.createElement)(TrashIcon))
);
}
function Entries({ bot, view, remove }) {
-@@ -7521,25 +7521,25 @@
+@@ -7521,25 +7521,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_react17.createElement)(
-+ return (0, import_react22.createElement)(
++ return (0, import_react23.createElement)(
"div",
{ className: "bt-mem-entries" },
- sections.map((section) => (0, import_react17.createElement)(
-+ sections.map((section) => (0, import_react22.createElement)(
++ sections.map((section) => (0, import_react23.createElement)(
"section",
{ key: section.scope, className: "bt-mem-section" },
- (0, import_react17.createElement)(
-+ (0, import_react22.createElement)(
++ (0, import_react23.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_react22.createElement)("h3", null, section.title),
-+ section.hint ? (0, import_react22.createElement)("span", null, section.hint) : null,
-+ (0, import_react22.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()) }))
),
- 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_react22.createElement)(
-+ import_react22.Fragment,
++ groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react23.createElement)(
++ import_react23.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_react22.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null,
-+ (0, import_react22.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react22.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove })))
-+ )) : (0, import_react22.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"))
))
);
}
-@@ -7551,52 +7551,52 @@
+@@ -7551,52 +7551,52 @@ 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_react17.createElement)(
-+ return (0, import_react22.createElement)(
++ return (0, import_react23.createElement)(
"div",
{ className: "bt-mem-reply", role: "status" },
- (0, import_react17.createElement)(
-+ (0, import_react22.createElement)(
++ (0, import_react23.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_react22.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked),
-+ answer.pending ? null : (0, import_react22.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react22.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))
),
- 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_react22.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react22.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react22.createElement)(
-+ import_react22.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,
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_react22.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null,
-+ answer.changes.length > 0 ? (0, import_react22.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react22.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)(
"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_react22.createElement)(CheckIcon) : (0, import_react22.createElement)(CloseIcon),
-+ (0, import_react22.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))
))) : null
)
);
@@ -9127,18 +11703,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_react22.useState)(null);
-+ const [error, setError] = (0, import_react22.useState)("");
-+ const timer = (0, import_react22.useRef)(0);
-+ const latest = (0, import_react22.useRef)(0);
-+ const alive = (0, import_react22.useRef)(true);
-+ const show = (0, import_react22.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) => {
setView(next);
setError("");
noteCount(botId, next.entries.length);
}, [botId]);
- const load = (0, import_react17.useCallback)(async (flags = {}) => {
-+ const load = (0, import_react22.useCallback)(async (flags = {}) => {
++ const load = (0, import_react23.useCallback)(async (flags = {}) => {
clearTimeout(timer.current);
const ticket = ++latest.current;
try {
@@ -9146,17 +11722,17 @@ diff --git a/client.js b/client.js
if (!alive.current || ticket !== latest.current) return;
show(next);
- if (next.summarizing) timer.current = setTimeout(() => void load({ summarize: flags.summarize || flags.regenerate }), POLL_MS2);
-+ if (next.summarizing) timer.current = setTimeout(() => void load({ summarize: flags.summarize || flags.regenerate }), POLL_MS4);
++ if (next.summarizing) timer.current = setTimeout(() => void load({ summarize: flags.summarize || flags.regenerate }), POLL_MS5);
} catch (failure) {
if (alive.current && ticket === latest.current) setError(failure?.message ?? String(failure));
}
}, [botId]);
- (0, import_react17.useEffect)(() => {
-+ (0, import_react22.useEffect)(() => {
++ (0, import_react23.useEffect)(() => {
alive.current = true;
void load({ summarize: true });
return () => {
-@@ -7606,9 +7606,9 @@
+@@ -7606,38 +7606,331 @@ function useMemoryView(botId, actions) {
}, [botId]);
return { view, error, load, show };
}
@@ -9164,20 +11740,18 @@ 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_react22.useState)(Date.now());
-+ (0, import_react22.useEffect)(() => {
++ const [now, setNow] = (0, import_react23.useState)(Date.now());
++ (0, import_react23.useEffect)(() => {
const id = setInterval(() => setNow(Date.now()), every);
return () => clearInterval(id);
}, [every]);
-@@ -7616,18 +7616,18 @@
+ return now;
}
- function MemoryDialog({ bot, actions, leaving = false }) {
- const { view, error, load, show } = useMemoryView(bot.id, actions);
+-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_react22.useState)("summary");
-+ const [menu, setMenu] = (0, import_react22.useState)(false);
- const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2);
+- const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2);
- const [text, setText] = (0, import_react17.useState)("");
- const [answer, setAnswer] = (0, import_react17.useState)(null);
- const menuRef = (0, import_react17.useRef)(null);
@@ -9185,114 +11759,152 @@ 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_react22.useState)("");
-+ const [answer, setAnswer] = (0, import_react22.useState)(null);
-+ const menuRef = (0, import_react22.useRef)(null);
-+ const dotsRef = (0, import_react22.useRef)(null);
-+ const inputRef = (0, import_react22.useRef)(null);
-+ const now = useNow3(3e4);
-+ const close = (0, import_react22.useCallback)(() => actions.closeMemory(), []);
- useEscape(close);
+- useEscape(close);
- (0, import_react17.useEffect)(() => {
-+ (0, import_react22.useEffect)(() => {
- if (!menu) return void 0;
- const onDown = (event) => {
- if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return;
-@@ -7689,52 +7689,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_react22.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_react22.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
-+ (0, import_react22.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_react22.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_react22.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react22.createElement)(ChevronLeftIcon)) : null,
-+ (0, import_react22.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
-+ (0, import_react22.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
-+ (0, import_react22.createElement)("span", { className: "bt-mem-head-gap" }),
-+ (0, import_react22.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_react22.createElement)(DotsIcon)),
-+ (0, import_react22.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react22.createElement)(CloseIcon)),
-+ menuShown ? (0, import_react22.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_react22.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_react22.createElement)("span", { className: "bt-menu-label" }, t("All entries")),
-+ (0, import_react22.createElement)("span", { className: "bt-menu-hint" }, String(count))
- ),
-- (0, import_react17.createElement)(
-+ (0, import_react22.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_react22.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary"))
- )
- ) : null
- ),
-- (0, import_react17.createElement)(
-+ (0, import_react22.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_react22.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null,
-+ view === null ? error ? null : (0, import_react22.createElement)(Skeleton) : page === "entries" ? (0, import_react22.createElement)(Entries, { bot, view, remove }) : (0, import_react22.createElement)(Summary, { bot, view, regenerate })
- ),
-- (0, import_react17.createElement)(
-+ (0, import_react22.createElement)(
- "div",
- { className: "bt-mem-foot" },
-- answer ? (0, import_react17.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
-- (0, import_react17.createElement)(
-+ answer ? (0, import_react22.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
-+ (0, import_react22.createElement)(
- "form",
- { className: "bt-mem-ask", onSubmit: ask },
-- (0, import_react17.createElement)("input", {
-+ (0, import_react22.createElement)("input", {
- ref: inputRef,
- className: "bt-mem-input",
- value: text,
-@@ -7744,10 +7744,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_react22.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_react22.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react22.createElement)(ArrowUpIcon)
- )
- )
- )
-@@ -7755,8 +7755,40 @@
- );
- }
-
+- if (!menu) return void 0;
++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 [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 now = useNow3(3e4);
++ const close = (0, import_react23.useCallback)(() => actions.closeMemory(), []);
++ useEscape(close);
++ (0, import_react23.useEffect)(() => {
++ if (!menu) return void 0;
++ const onDown = (event) => {
++ 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)
++ )
++ )
++ )
++ )
++ );
++}
++
+// src/client/standing-approvals.js
-+var import_react23 = require("react");
++var import_react24 = require("react");
+var standingOf = (target, roster) => {
+ const me = roster?.self?.memberId;
+ return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me);
@@ -9301,19 +11913,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_react23.createElement)(
++ return (0, import_react24.createElement)(
+ Card2,
+ { title: t("Always allowed") },
-+ grants.map((grant) => (0, import_react23.createElement)(
++ grants.map((grant) => (0, import_react24.createElement)(
+ "div",
+ { key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind },
-+ (0, import_react23.createElement)(
++ (0, import_react24.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react23.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind),
-+ (0, import_react23.createElement)("span", { className: "bt-set-hint" }, hint)
++ (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_react23.createElement)("button", {
++ (0, import_react24.createElement)("button", {
+ type: "button",
+ className: "bt-soft bt-soft-sm",
+ "aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }),
@@ -9323,424 +11935,568 @@ diff --git a/client.js b/client.js
+ );
+}
+
- // src/client/bot-settings.js
--var import_react18 = require("react");
-+var import_react24 = 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 +7780,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_react24.useState)(null);
-+ const reverting = (0, import_react24.useRef)(false);
- const commit = () => {
- const next = draft?.trim();
- setDraft(null);
-@@ -7792,7 +7792,7 @@
- if (next === void 0 || next === (value ?? "") || required && next === "") return;
- void onSave(next);
- };
-- return (0, import_react18.createElement)(multiline ? "textarea" : "input", {
-+ return (0, import_react24.createElement)(multiline ? "textarea" : "input", {
- className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input",
- value: draft ?? value ?? "",
- placeholder,
-@@ -7813,29 +7813,29 @@
- });
- }
- function Row({ label, hint, stack = false, children }) {
-- return (0, import_react18.createElement)(
-+ return (0, import_react24.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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react24.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_react24.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react24.createElement)("h3", null, title) : null, (0, import_react24.createElement)("div", { className: "bt-set-card" }, children));
- }
- function Switch({ on, label, disabled, onChange }) {
-- return (0, import_react18.createElement)(
-+ return (0, import_react24.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_react24.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true })
- );
- }
--function SwitchRow({ label, hint, on, disabled, onChange }) {
-- return (0, import_react18.createElement)(
++// 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 })
++ );
++}
+function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) {
-+ return (0, import_react24.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_react24.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react24.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react24.createElement)("span", { className: "bt-set-hint" }, hint) : null),
-+ (0, import_react24.createElement)(Switch, { on, label, disabled, onChange })
- );
- }
- function modelHint(bot, roster) {
-@@ -7846,22 +7846,22 @@
- return t("Applies from the next message.");
- }
- function ShapeRing({ shape, size }) {
-- const maskId = (0, import_react18.useRef)("");
-+ const maskId = (0, import_react24.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_react24.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_react24.createElement)(
-+ import_react24.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_react24.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react24.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react24.createElement)("button", {
- key: id,
- type: "button",
- className: "bt-shape",
-@@ -7869,8 +7869,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_react24.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react24.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))),
-+ (0, import_react24.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react24.createElement)("button", {
- key: color,
- type: "button",
- className: "bt-color",
-@@ -7878,19 +7878,19 @@
- title: t(capitalize(color)),
- "aria-pressed": look.color === color,
- onClick: () => onColor(color)
-- }, (0, import_react18.createElement)("span", { style: { background: paintCss(color) } }))))
-+ }, (0, import_react24.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_react24.useState)(look.image ? "upload" : "bot");
-+ const [over, setOver] = (0, import_react24.useState)(false);
-+ const ref = (0, import_react24.useRef)(null);
-+ const fileRef = (0, import_react24.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_react24.useEffect)(() => {
++ 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();
-@@ -7908,7 +7908,7 @@
+- if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return;
+- setMenu(false);
++ 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,863 @@ function MemoryDialog({ bot, actions, leaving = false }) {
+ window.removeEventListener("mousedown", onDown);
window.removeEventListener("keydown", onKey, true);
};
- }, [onClose]);
-- (0, import_react18.useEffect)(() => {
-+ (0, import_react24.useEffect)(() => {
- if (tab !== "upload") return void 0;
- const onPaste = (event) => {
- const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/"));
-@@ -7922,32 +7922,32 @@
- }, [tab, bot]);
- const hasPhoto = Boolean(look.image);
- const glide = useGlide(tab);
-- return (0, import_react18.createElement)(
-+ return (0, import_react24.createElement)(
- "div",
- { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") },
-- (0, import_react18.createElement)(
-+ (0, import_react24.createElement)(
- "div",
- { className: "bt-avedit-top" },
-- (0, import_react18.createElement)(
-+ (0, import_react24.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_react24.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react24.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_react24.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_react24.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_react24.createElement)(
- "div",
- {
- className: "bt-dropzone",
-@@ -7963,15 +7963,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_react24.createElement)("img", { src: look.image, alt: "" }) : null,
-+ (0, import_react24.createElement)("div", null, t("Drag, drop, or paste an image")),
-+ (0, import_react24.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_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")),
-+ hasPhoto ? (0, import_react24.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_react24.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
- const file = event.target.files?.[0];
- event.target.value = "";
- upload(file);
-@@ -7982,26 +7982,27 @@
- function 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_react24.createElement)(
- Card,
- { title: t("Memory") },
-- (0, import_react18.createElement)(
-+ (0, import_react24.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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, label)),
-+ (0, import_react24.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react24.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_react24.useState)(false);
-+ const [confirming, setConfirming] = (0, import_react24.useState)(false);
-+ const closeEditor = (0, import_react24.useCallback)(() => setEditing(false), []);
-+ (0, import_react24.useEffect)(() => {
- if (!confirming) return void 0;
- const timer = setTimeout(() => setConfirming(false), 4e3);
- return () => clearTimeout(timer);
- }, [confirming]);
+- }, [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]);
+ if (bot.remote) return null;
- const isMainBot = isMainOf(roster, bot.id);
- const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1;
- const isTile = roster.mainBotId === bot.id;
-@@ -8011,112 +8011,116 @@
- 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;
++ 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_react18.useEffect)(() => {
-+ (0, import_react24.useEffect)(() => {
- void actions.refreshModels?.();
- }, [bot.id]);
-- return (0, import_react18.createElement)(
-+ return (0, import_react24.createElement)(
+- 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_react18.createElement)(
-+ (0, import_react24.createElement)(
+- { 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)(
++ { className: "bt-bs" },
++ (0, import_react25.createElement)(
"div",
- { className: "bt-bs-hero" },
-- (0, import_react18.createElement)(
-+ (0, import_react24.createElement)(
+- { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) },
+- (0, import_react17.createElement)(
++ { className: "bt-bs-hero" },
++ (0, import_react25.createElement)(
"div",
- { className: "bt-avatar-anchor" },
-- (0, import_react18.createElement)(
-+ (0, import_react24.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_react24.createElement)(BotMark, { bot, size: 64, live: true }),
-+ (0, import_react24.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react24.createElement)(PencilIcon))
- ),
-- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
-+ editing ? (0, import_react24.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
+- { 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
++ { 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_react18.createElement)(
-+ (0, import_react24.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_react24.createElement)("span", { className: "bt-bs-name" }, bot.name),
-+ (0, import_react24.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
- )
- ),
-- (0, import_react18.createElement)(
-+ (0, import_react24.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_react24.createElement)(Row, { label: t("Name") }, (0, import_react24.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })),
-+ (0, import_react24.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react24.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) }))
- ),
-- (0, import_react18.createElement)(
-+ (0, import_react24.createElement)(
- Card,
- { title: t("Model") },
-- (0, import_react18.createElement)(
-+ (0, import_react24.createElement)(
+- (0, import_react17.createElement)(
++ (0, import_react25.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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))),
-+ (0, import_react24.createElement)(ModelMenu, { bot, roster, actions, run })
-+ ),
-+ bot.model ? (0, import_react24.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null
- ),
-- (0, import_react18.createElement)(
-+ (0, import_react24.createElement)(
- Card,
- { title: t("Working folder") },
-- (0, import_react18.createElement)(
-+ (0, import_react24.createElement)(
+- { 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 })
++ { 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_react17.createElement)(
++ 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_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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
-+ (0, import_react24.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) })
- )
- ),
-- (0, import_react18.createElement)(
-+ (0, import_react24.createElement)(
- Card,
- { title: t("Instructions") },
-- (0, import_react18.createElement)(
-+ (0, import_react24.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_react24.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)(
+- { 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)
+- )
+- )
++ { 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 }) })
++ )
++ ),
+ // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406).
-+ (0, import_react24.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react24.createElement)(Card, { title: t("Sharing") }, children) }),
-+ (0, import_react24.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }),
-+ (0, import_react24.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }),
-+ roster.memory === false ? null : (0, import_react24.createElement)(MemoryCard, { bot, actions }),
-+ (0, import_react24.createElement)(BotSchedules, { bot, roster, actions }),
-+ (0, import_react24.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }),
-+ (0, import_react24.createElement)(
- Card,
- { title: t("Main Bot") },
-- onlyMain ? (0, import_react18.createElement)(
-+ onlyMain ? (0, import_react24.createElement)(
- "div",
- { className: "bt-set-row bt-bs-row" },
-- (0, import_react18.createElement)(
-+ (0, import_react24.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_react24.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_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change"))
-+ ) : (0, import_react24.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_react24.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_react24.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_react24.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_react24.createElement)(
- Card,
-- { title: t("Manage") },
-- (0, import_react18.createElement)(
++ (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,
+ { title: t("More") },
-+ (0, import_react24.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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))),
-+ (0, import_react24.createElement)("span", { className: "bt-chevron" }, (0, import_react24.createElement)(ChevronRightIcon))
- ),
-- isMainBot ? null : (0, import_react18.createElement)(
-+ isMainBot ? null : (0, import_react24.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_react24.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_react24.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")),
-+ (0, import_react24.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_react24.createElement)("span", { className: "bt-chevron" }, (0, import_react24.createElement)(ChevronRightIcon))
++ (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))
)
)
);
-@@ -8133,6 +8133,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%}
+ }
++var BOT_SETTINGS_CSS = `
++.bt-bs{display:flex;flex-direction:column;gap:18px}
++.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px}
++.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px}
++.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
++.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)}
++.bt-bs .bt-avedit{left:0;transform:none}
++.bt-bs-row{min-height:48px}
++label.bt-bs-row{cursor:text}
++.bt-bs-row>.bt-set-copy{flex:1 1 auto}
++.bt-bs-label{font-size:13px;line-height:18px;font-weight:500}
++.bt-bs-row .bt-model-trigger{flex:none;max-width:58%}
+${MODEL_TEST_CSS}
- .bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease}
- .bt-bs-input:hover{background:var(--bt-hover)}
- .bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left}
-@@ -8155,6 +8155,610 @@
- @media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}}
- `;
++.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease}
++.bt-bs-input:hover{background:var(--bt-hover)}
++.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left}
++.bt-bs-input::placeholder{color:var(--bt-ink-3)}
++.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px}
++.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)}
++.bt-bs-area:hover{background:var(--bt-active)}
++.bt-bs-danger .bt-bs-label{color:#c21d2e}
++.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)}
++.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)}
++.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease}
++.bt-switch[aria-checked=true]{background:var(--bt-accent)}
++.bt-switch:disabled{opacity:.45;cursor:default}
++.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px}
++.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}}
++.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)}
++.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px}
++.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)}
++.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out}
++@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}}
++`;
+-// src/client/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;
+- }
+- 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();
+- }
+// src/client/group-settings.js
-+var import_react25 = require("react");
++var import_react26 = 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."]
@@ -9757,31 +12513,50 @@ 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, 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_react25.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_react25.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_react25.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name];
-+ return index === 0 ? marked : [(0, import_react25.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked];
-+ });
-+ return (0, import_react25.createElement)(
++ 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];
+ });
+-}
+-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 })
++ return (0, import_react26.createElement)(
+ "div",
+ { className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 },
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)(
+ "span",
+ { className: "bt-gs-who bt-face-person" },
-+ (0, import_react25.createElement)(PersonMark, { size, name: person.name }),
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)(PersonMark, { size, name: person.name }),
++ (0, import_react26.createElement)(
+ "span",
+ { className: "bt-face-person-text" },
-+ (0, import_react25.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
-+ (0, import_react25.createElement)("span", { className: "bt-face-bots" }, bots)
++ (0, import_react26.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
++ (0, import_react26.createElement)("span", { className: "bt-face-bots" }, bots)
+ )
+ )
-+ );
-+}
+ );
+ }
+-function SwitchRow({ label, hint, on, disabled, onChange }) {
+- return (0, import_react18.createElement)(
+function FaceLead({ room: room2, roster, pick: pick2 }) {
+ const lead = room2.face?.lead;
-+ const [open, setOpen] = (0, import_react25.useState)(false);
-+ const [busy, setBusy] = (0, import_react25.useState)(false);
-+ const [error, setError] = (0, import_react25.useState)("");
++ const [open, setOpen] = (0, import_react26.useState)(false);
++ const [busy, setBusy] = (0, import_react26.useState)(false);
++ const [error, setError] = (0, import_react26.useState)("");
+ const seats = faceSeats(room2, roster);
+ const current = seats.find((seat) => isLeadSeat(seat, lead?.handle));
+ if (!current) return null;
@@ -9798,48 +12573,90 @@ diff --git a/client.js b/client.js
+ setBusy(false);
+ }
+ };
-+ return (0, import_react25.createElement)(
-+ "div",
++ 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 })
+ { className: "bt-face-lead", "data-flybot": "face-lead" },
-+ (0, import_react25.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
++ (0, import_react26.createElement)(
+ "div",
+ { className: "bt-set-row" },
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react25.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)),
-+ (0, import_react25.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_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."))
+ ),
-+ canPick ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null
++ canPick ? (0, import_react26.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_react25.createElement)(
++ open && canPick ? seats.map((seat) => (0, import_react26.createElement)(
+ "div",
+ { key: seat.key, className: "bt-set-row" },
-+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)),
-+ seat === current ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react25.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead"))
++ (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"))
+ )) : null,
-+ open && canPick && lead.picked ? (0, import_react25.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_react25.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")),
-+ error ? (0, import_react25.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : 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.");
+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_react25.useState)(false);
-+ const [confirming, setConfirming] = (0, import_react25.useState)(false);
-+ (0, import_react25.useEffect)(() => {
++ const [adding, setAdding] = (0, import_react26.useState)(false);
++ const [confirming, setConfirming] = (0, import_react26.useState)(false);
++ (0, import_react26.useEffect)(() => {
+ if (!confirming) return void 0;
+ const timer = setTimeout(() => setConfirming(false), 4e3);
+ return () => clearTimeout(timer);
@@ -9877,41 +12694,41 @@ diff --git a/client.js b/client.js
+ if (done) onDeleted?.();
+ });
+ };
-+ 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)(RoomAvatar, { room: room2, roster, size: 64 }),
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
++ (0, import_react26.createElement)(
+ "div",
+ { className: "bt-bs-who" },
-+ (0, import_react25.createElement)("span", { className: "bt-bs-name" }, room2.name),
-+ (0, import_react25.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
++ (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_react25.createElement)(
++ (0, import_react26.createElement)(
+ Card,
+ { title: t("Profile") },
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)(
+ Row,
+ { label: t("Name"), hint: t("Leave it empty to name the group after its members.") },
-+ (0, import_react25.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)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) })
+ ),
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)(
+ Row,
+ { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true },
-+ (0, import_react25.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)(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_react25.createElement)(
++ (0, import_react26.createElement)(
+ Card,
+ { title: t("Who answers") },
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row bt-bs-stack" },
-+ (0, import_react25.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react25.createElement)("button", {
++ (0, import_react26.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react26.createElement)("button", {
+ key: id,
+ type: "button",
+ "aria-pressed": mode === id,
@@ -9921,104 +12738,104 @@ diff --git a/client.js b/client.js
+ if (mode !== id) void update({ mode: id });
+ }
+ }, t(label)))),
-+ (0, import_react25.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
++ (0, import_react26.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
+ )
+ ),
-+ (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: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ 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: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) })
+ )
+ ),
-+ face ? (0, import_react25.createElement)(
++ face ? (0, import_react26.createElement)(
+ Card,
+ { title: t("Members · {count}", { count: people2.length }) },
-+ people2.map((person) => (0, import_react25.createElement)(FacePersonRow, { key: person.key, person, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
++ people2.map((person) => (0, import_react26.createElement)(FacePersonRow, { key: person.key, person, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
+ ) : null,
-+ face ? null : (0, import_react25.createElement)(
++ face ? null : (0, import_react26.createElement)(
+ Card,
+ { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) },
-+ humans.map((person) => (0, import_react25.createElement)(
++ humans.map((person) => (0, import_react26.createElement)(
+ "div",
+ { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)(
+ "span",
+ { className: "bt-gs-who" },
-+ (0, import_react25.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id),
-+ (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Human")),
-+ person.role === "admin" ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Admin")) : null
++ (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
+ ),
-+ humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react25.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_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
+ )),
+ members.map((member) => {
+ const leads = room2.admin === member.id;
-+ return (0, import_react25.createElement)(
++ return (0, import_react26.createElement)(
+ "div",
+ { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" },
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)(
+ "button",
+ { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) },
-+ (0, import_react25.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
-+ (0, import_react25.createElement)("span", { className: "bt-row-title" }, member.name),
-+ leads ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Admin")) : null,
-+ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Chief")) : null
++ (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
+ ),
-+ leads ? (0, import_react25.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react25.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")),
-+ members.length > 2 ? (0, import_react25.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_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
+ );
+ }),
-+ remotes.map((person) => (0, import_react25.createElement)(
++ remotes.map((person) => (0, import_react26.createElement)(
+ "div",
+ { key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)(
+ "span",
+ { className: "bt-gs-who" },
-+ (0, import_react25.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
-+ (0, import_react25.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)),
++ (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)),
+ // #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_react25.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_react26.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_react25.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_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" ? null : adding ? [
-+ ...outsidePeople.map((person) => (0, import_react25.createElement)(
++ ...outsidePeople.map((person) => (0, import_react26.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_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)),
-+ (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Add person"))
++ (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"))
+ )),
-+ ...outside.map((bot) => (0, import_react25.createElement)(
++ ...outside.map((bot) => (0, import_react26.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_react25.createElement)(BotMark, { bot, size: 22 }),
-+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, bot.name)),
-+ (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Add"))
++ (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"))
+ )),
-+ ...outsideRemotes.map((bot) => (0, import_react25.createElement)(
++ ...outsideRemotes.map((bot) => (0, import_react26.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_react25.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_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))),
-+ (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot"))
++ (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_react25.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_react25.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react25.createElement)(PlusIcon), t("Add member")) : null,
-+ (0, import_react25.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react25.createElement)(
++ (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)(
+ "span",
+ { className: "bt-set-hint" },
+ // The host checks for two members on every change, even a rename.
@@ -10026,42 +12843,83 @@ 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_react25.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
++ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react26.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
+ ),
-+ (0, import_react25.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
-+ (0, import_react25.createElement)(
++ (0, import_react26.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
++ (0, import_react26.createElement)(
+ Card,
+ { title: t("Sidebar") },
-+ (0, import_react25.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_react25.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)(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_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 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: room2.name }) : t("Delete group")),
-+ (0, import_react25.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-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_react25.createElement)("span", { className: "bt-chevron" }, (0, import_react25.createElement)(ChevronRightIcon))
++ (0, import_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.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_react25.useState)(null);
-+ const [members, setMembers] = (0, import_react25.useState)([]);
-+ const [name, setName] = (0, import_react25.useState)("");
-+ const [busy, setBusy] = (0, import_react25.useState)(false);
++ 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 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);
@@ -10072,51 +12930,129 @@ diff --git a/client.js b/client.js
+ if (done && room2) onCreated(room2);
+ else setBusy(false);
+ };
-+ const chip = (bot, on, onClick, role) => (0, import_react25.createElement)("button", {
++ const chip = (bot, on, onClick, role) => (0, import_react26.createElement)("button", {
+ key: bot.id,
+ type: "button",
+ role,
+ className: "bt-ng-chip",
+ "aria-checked": on,
+ onClick
-+ }, (0, import_react25.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react25.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react25.createElement)(AdminStar) : null);
-+ return (0, import_react25.createElement)(
-+ "div",
++ }, (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)(
+ { className: "bt-set-card bt-ng" },
-+ (0, import_react25.createElement)(
-+ "div",
++ (0, import_react26.createElement)(
+ "div",
+- { className: "bt-avedit-top" },
+- (0, import_react18.createElement)(
+ { className: "bt-set-row bt-bs-row bt-bs-stack" },
-+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react25.createElement)(AdminStar), t("Choose the group admin"))),
-+ (0, import_react25.createElement)(
-+ "div",
++ (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"))
+ { 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_react25.createElement)(
-+ "div",
+ ),
+- 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)(
+ { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" },
-+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))),
-+ (0, import_react25.createElement)(
-+ "div",
++ (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))
+ { 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_react25.createElement)(
++ admin === null || members.length === 0 ? null : (0, import_react26.createElement)(
+ "label",
+ { className: "bt-set-row bt-bs-row bt-ng-step" },
-+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
-+ (0, import_react25.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)("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_react25.createElement)(
++ (0, import_react26.createElement)(
+ "div",
+ { className: "bt-set-row bt-task-foot" },
-+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
-+ (0, import_react25.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_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);
+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}
@@ -10147,13 +13083,13 @@ diff --git a/client.js b/client.js
+`;
+
+// src/client/palette.js
-+var import_react26 = require("react");
++var import_react27 = 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_react26.useState)(start ?? "");
-+ const [cursor, setCursor] = (0, import_react26.useState)(0);
-+ const [leaving, setLeaving] = (0, import_react26.useState)(false);
++ 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 wanted = query.trim().toLowerCase();
+ const close = () => {
+ if (leaving) return;
@@ -10189,7 +13125,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_react26.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }),
++ avatar: (0, import_react27.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,
@@ -10200,7 +13136,7 @@ diff --git a/client.js b/client.js
+ })),
+ ...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({
+ key: room2.id,
-+ avatar: (0, import_react26.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
++ avatar: (0, import_react27.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,
@@ -10213,7 +13149,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_react26.createElement)("span", { className: "bt-palette-agent" }, (0, import_react26.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
++ avatar: (0, import_react27.createElement)("span", { className: "bt-palette-agent" }, (0, import_react27.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
+ title: "DSH Agent",
+ subtitle: t("Plain dsh session"),
+ run: () => actions.openAgent(agent.id)
@@ -10226,16 +13162,16 @@ diff --git a/client.js b/client.js
+ key: `${id}:${turn}`,
+ title: bot?.name ?? room2?.name ?? t("Bot"),
+ subtitle: snippet,
-+ avatar: bot ? (0, import_react26.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react26.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
++ avatar: bot ? (0, import_react27.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react27.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_react26.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") },
-+ { key: "cmd:bot", icon: (0, import_react26.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") },
-+ { key: "cmd:group", icon: (0, import_react26.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") },
-+ { key: "cmd:settings", icon: (0, import_react26.createElement)(GearIcon), title: t("Settings"), run: () => actions.openSettings("general") }
++ { 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);
@@ -10245,27 +13181,43 @@ diff --git a/client.js b/client.js
+${shown}`, { axis: "y", ...QUICK_EDGES });
+ const listRef = glide.box;
+ useFlip(listRef, shown, "[data-flip]");
-+ (0, import_react26.useEffect)(() => {
++ (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_react26.createElement)(
-+ "div",
++ return (0, import_react27.createElement)(
+ "div",
+- { className: "bt-bs" },
+- (0, import_react18.createElement)(
+ { className: "bt-palette-layer", "data-leaving": leaving || void 0 },
-+ (0, import_react26.createElement)("div", { className: "bt-backdrop", onMouseDown: close }),
-+ (0, import_react26.createElement)(
-+ "div",
++ (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_react26.createElement)(
-+ "div",
++ (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_react26.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react26.createElement)(SearchIcon)),
-+ (0, import_react26.createElement)("input", {
++ (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"),
@@ -10291,29 +13243,114 @@ diff --git a/client.js b/client.js
+ }
+ }
+ }),
-+ query ? (0, import_react26.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
++ query ? (0, import_react27.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
+ setQuery("");
+ setCursor(0);
-+ } }, (0, import_react26.createElement)(CloseIcon)) : null
-+ ),
-+ (0, import_react26.createElement)(
-+ "div",
++ } }, (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))
+ { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") },
-+ (0, import_react26.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ rows.length === 0 ? (0, import_react26.createElement)(
++ (0, import_react27.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ rows.length === 0 ? (0, import_react27.createElement)(
+ "div",
+ { className: "bt-palette-empty" },
-+ (0, import_react26.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react26.createElement)(SearchIcon)),
-+ (0, import_react26.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")),
-+ (0, import_react26.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message"))
-+ ) : sections.map(([title, items]) => (0, import_react26.createElement)(
-+ import_react26.Fragment,
++ (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,
+ { key: title },
-+ (0, import_react26.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
++ (0, import_react27.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
+ items.map((row) => {
+ index += 1;
+ const at = index;
-+ return (0, import_react26.createElement)(
++ return (0, import_react27.createElement)(
+ "button",
+ {
+ key: row.key,
@@ -10327,26 +13364,58 @@ diff --git a/client.js b/client.js
+ },
+ onClick: () => choose(row)
+ },
-+ (0, import_react26.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react26.createElement)("span", { className: "bt-palette-cmd" }, row.icon)),
-+ (0, import_react26.createElement)(
++ (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)(
+ "span",
+ { className: "bt-palette-text" },
-+ (0, import_react26.createElement)("span", { className: "bt-palette-title" }, row.title),
-+ row.subtitle ? (0, import_react26.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null
++ (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
+ ),
-+ row.badge ? (0, import_react26.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null
++ row.badge ? (0, import_react27.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null
+ );
+ })
+ ))
-+ )
-+ )
-+ );
-+}
-+
+ )
+ )
+ );
+ }
+-var BOT_SETTINGS_CSS = `
+-.bt-bs{display:flex;flex-direction:column;gap:18px}
+-.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px}
+-.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px}
+-.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+-.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)}
+-.bt-bs .bt-avedit{left:0;transform:none}
+-.bt-bs-row{min-height:48px}
+-label.bt-bs-row{cursor:text}
+-.bt-bs-row>.bt-set-copy{flex:1 1 auto}
+-.bt-bs-label{font-size:13px;line-height:18px;font-weight:500}
+-.bt-bs-row .bt-model-trigger{flex:none;max-width:58%}
+-.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease}
+-.bt-bs-input:hover{background:var(--bt-hover)}
+-.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left}
+-.bt-bs-input::placeholder{color:var(--bt-ink-3)}
+-.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px}
+-.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)}
+-.bt-bs-area:hover{background:var(--bt-active)}
+-.bt-bs-danger .bt-bs-label{color:#c21d2e}
+-.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)}
+-.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)}
+-.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease}
+-.bt-switch[aria-checked=true]{background:var(--bt-accent)}
+-.bt-switch:disabled{opacity:.45;cursor:default}
+-.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px}
+-.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}}
+-.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)}
+-.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px}
+-.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)}
+-.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out}
+-@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}}
+-`;
+
// src/client/bot-draft.js
function freshBotName(roster, base = "New Bot") {
- const taken = new Set(roster.bots.map((bot) => bot.name.toLowerCase()));
-@@ -8165,36 +8165,48 @@
+@@ -8165,36 +8165,48 @@ function freshBotName(roster, base = "New Bot") {
}
function startModel(roster) {
const models = roster.models ?? [];
@@ -10377,7 +13446,7 @@ diff --git a/client.js b/client.js
// src/client/create-bot.js
-var import_react19 = require("react");
-+var import_react27 = require("react");
++var import_react28 = require("react");
var pick = (list) => list[Math.floor(Math.random() * list.length)];
var randomLook = (avoid = {}) => {
const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape);
@@ -10395,22 +13464,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_react27.useState)(() => randomLook());
-+ const [image, setImage] = (0, import_react27.useState)(null);
-+ const [name, setName] = (0, import_react27.useState)(start?.name ?? "");
-+ const [model, setModel] = (0, import_react27.useState)(() => startModel(roster));
-+ const [effort, setEffort] = (0, import_react27.useState)(void 0);
-+ const [busy, setBusy] = (0, import_react27.useState)(false);
-+ const [error, setError] = (0, import_react27.useState)("");
-+ const fileRef = (0, import_react27.useRef)(null);
-+ const frame = (0, import_react27.useRef)(null);
++ 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);
useEscape(onBack);
- (0, import_react19.useEffect)(() => {
-+ (0, import_react27.useEffect)(() => {
++ (0, import_react28.useEffect)(() => {
if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" });
}, []);
const fallbackName = freshBotName(roster, t("New Bot"));
-@@ -8206,8 +8206,8 @@
+@@ -8206,8 +8206,8 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
setError("");
setBusy(true);
try {
@@ -10421,59 +13490,59 @@ diff --git a/client.js b/client.js
} catch (failure) {
setError(failure?.message ?? String(failure));
setBusy(false);
-@@ -8219,25 +8219,25 @@
+@@ -8219,25 +8219,25 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
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_react27.createElement)(
++ const card = (0, import_react28.createElement)(
"div",
{ className: "bt-create-card" },
- (0, import_react19.createElement)(
-+ (0, import_react27.createElement)(
++ (0, import_react28.createElement)(
"div",
{ className: "bt-create-preview" },
// The key remounts the mark, so every new pick plays the pop.
- (0, import_react19.createElement)(
-+ (0, import_react27.createElement)(
++ (0, import_react28.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_react27.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_react19.createElement)("span", { className: "bt-create-name" }, finalName)
-+ (0, import_react27.createElement)("span", { className: "bt-create-name" }, finalName)
++ (0, import_react28.createElement)("span", { className: "bt-create-name" }, finalName)
),
- (0, import_react19.createElement)("div", { className: "bt-create-step" }, t("Look")),
- (0, import_react19.createElement)(
-+ (0, import_react27.createElement)("div", { className: "bt-create-step" }, t("Look")),
-+ (0, import_react27.createElement)(
++ (0, import_react28.createElement)("div", { className: "bt-create-step" }, t("Look")),
++ (0, import_react28.createElement)(
"div",
{ className: "bt-create-looks" },
- (0, import_react19.createElement)(CharacterGrid, {
-+ (0, import_react27.createElement)(CharacterGrid, {
++ (0, import_react28.createElement)(CharacterGrid, {
look,
hasPhoto: Boolean(image),
onShape: (shape) => {
-@@ -8249,27 +8249,27 @@
+@@ -8249,27 +8249,27 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
setLook((current) => ({ ...current, color }));
}
}),
- (0, import_react19.createElement)(
-+ (0, import_react27.createElement)(
++ (0, import_react28.createElement)(
"div",
{ className: "bt-actions bt-create-lookbar" },
- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
-+ (0, import_react27.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: () => {
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_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")),
-+ image ? (0, import_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null,
-+ (0, import_react27.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
++ (0, import_react28.createElement)("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) => {
const file = event.target.files?.[0];
event.target.value = "";
upload(file);
@@ -10481,76 +13550,76 @@ diff --git a/client.js b/client.js
)
),
- (0, import_react19.createElement)(
-+ (0, import_react27.createElement)(
++ (0, import_react28.createElement)(
"label",
{ className: "bt-field" },
- (0, import_react19.createElement)("span", { className: "bt-field-label" }, t("Name")),
- (0, import_react19.createElement)("input", {
-+ (0, import_react27.createElement)("span", { className: "bt-field-label" }, t("Name")),
-+ (0, import_react27.createElement)("input", {
++ (0, import_react28.createElement)("span", { className: "bt-field-label" }, t("Name")),
++ (0, import_react28.createElement)("input", {
className: "bt-input",
autoFocus: !inline,
value: name,
-@@ -8284,75 +8284,113 @@
+@@ -8284,75 +8284,113 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
}
}
}),
- 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_react27.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react27.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
),
- (0, import_react19.createElement)(
-+ (0, import_react27.createElement)(
++ (0, import_react28.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_react27.createElement)("span", { className: "bt-field-label" }, t("Model")),
-+ (0, import_react27.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) => {
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_react27.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."))
),
- error ? (0, import_react19.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
- (0, import_react19.createElement)(
-+ error ? (0, import_react27.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
-+ (0, import_react27.createElement)(
++ error ? (0, import_react28.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
++ (0, import_react28.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_react27.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null,
-+ (0, import_react27.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_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"))
)
);
- 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_react27.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card);
-+ return (0, import_react27.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)(
"div",
{ className: "bt-create", role: "dialog", "aria-label": t("Create Bot") },
- (0, import_react19.createElement)(
-+ (0, import_react27.createElement)(
++ (0, import_react28.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_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react27.createElement)(ChevronLeftIcon)) : (0, import_react27.createElement)("span", { className: "bt-create-gap" }),
-+ (0, import_react27.createElement)("span", { className: "bt-create-title" }, t("Create Bot")),
-+ (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react27.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))
),
- (0, import_react19.createElement)("div", { className: "bt-create-body" }, card)
-+ (0, import_react27.createElement)("div", { className: "bt-create-body" }, card)
++ (0, import_react28.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_react28 = require("react");
++var import_react29 = require("react");
function NewChat({ mode, roster, actions }) {
- const [query, setQuery] = (0, import_react20.useState)("");
- const [picked, setPicked] = (0, import_react20.useState)([]);
@@ -10562,20 +13631,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_react28.useState)("");
-+ const [picked, setPicked] = (0, import_react28.useState)([]);
-+ const [group, setGroup] = (0, import_react28.useState)(mode === "group");
-+ const [compose, setCompose] = (0, import_react28.useState)(mode !== "new");
-+ const [admin, setAdmin] = (0, import_react28.useState)(null);
-+ const [message, setMessage] = (0, import_react28.useState)("");
-+ const [busy, setBusy] = (0, import_react28.useState)(false);
-+ const [error, setError] = (0, import_react28.useState)("");
-+ const [fix, setFix] = (0, import_react28.useState)(null);
-+ const [fixNote, setFixNote] = (0, import_react28.useState)("");
-+ const [inviteLink, setInviteLink] = (0, import_react28.useState)("");
-+ const [cursor, setCursor] = (0, import_react28.useState)(0);
-+ const [draft, setDraft] = (0, import_react28.useState)(mode === "create" ? { name: "", fromList: false } : null);
-+ const inputRef = (0, import_react28.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 close = () => actions.closeOverlay();
useEscape(close);
const paneLeft = usePaneLeft();
@@ -10635,7 +13704,7 @@ diff --git a/client.js b/client.js
const pick2 = (bot) => {
setPicked((current) => [...current, bot.id]);
setQuery("");
-@@ -8386,13 +8386,17 @@
+@@ -8386,13 +8386,17 @@ function NewChat({ mode, roster, actions }) {
};
const created = (bot) => {
close();
@@ -10656,7 +13725,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 @@
+@@ -8411,46 +8411,75 @@ function NewChat({ mode, roster, actions }) {
close();
return;
}
@@ -10677,14 +13746,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_react28.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => {
++ options.push({ key: "chat", icon: (0, import_react29.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_react28.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_react28.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => {
++ 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: () => {
setGroup(true);
+ setCompose(true);
setCursor(0);
@@ -10693,7 +13762,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_react28.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react28.createElement)(PeopleIcon) }) : (0, import_react28.createElement)(PeopleIcon),
++ 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),
+ label: t(action.label),
+ hint: action.hint ? t(action.hint) : void 0,
+ run: () => {
@@ -10706,7 +13775,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_react28.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_react29.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => {
await actions.addAgent();
close();
}) });
@@ -10720,22 +13789,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_react28.createElement)(PlusIcon), label: t("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 });
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_react28.useRef)(null);
++ const drop = (0, import_react29.useRef)(null);
useMorph(drop, listed);
if (draft) {
- return (0, import_react20.createElement)(
-+ return (0, import_react28.createElement)(
++ return (0, import_react29.createElement)(
"div",
{ className: "bt-pane", style: { left: paneLeft } },
- (0, import_react20.createElement)(CreateBot, {
-+ (0, import_react28.createElement)(CreateBot, {
++ (0, import_react29.createElement)(CreateBot, {
roster,
actions,
start: draft,
@@ -10743,12 +13812,12 @@ diff --git a/client.js b/client.js
canGoBack: draft.fromList,
onBack: draft.fromList ? () => {
setDraft(null);
-@@ -8467,33 +8467,41 @@
+@@ -8467,33 +8467,41 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
if (picked.length > 0) setPicked((current) => current.slice(0, -1));
else if (admin !== null) setAdmin(null);
};
- return (0, import_react20.createElement)(
-+ return (0, import_react28.createElement)(
++ return (0, import_react29.createElement)(
"div",
- { className: "bt-pane", style: { left: paneLeft } },
- (0, import_react20.createElement)(
@@ -10760,52 +13829,52 @@ diff --git a/client.js b/client.js
+ if (event.target === event.currentTarget) close();
+ } : void 0
+ },
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)(
++ menuOnly ? null : (0, import_react29.createElement)(
"div",
{ className: "bt-to" },
- (0, import_react20.createElement)("span", { className: "bt-to-label" }, t("To:")),
- adminBot ? (0, import_react20.createElement)(
-+ (0, import_react28.createElement)("span", { className: "bt-to-label" }, t("To:")),
-+ adminBot ? (0, import_react28.createElement)(
++ (0, import_react29.createElement)("span", { className: "bt-to-label" }, t("To:")),
++ adminBot ? (0, import_react29.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_react28.createElement)(BotMark, { bot: adminBot, size: 16 }),
++ (0, import_react29.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_react28.createElement)(AdminStar),
-+ (0, import_react28.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) }, "×")
) : 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_react28.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react28.createElement)(AdminStar)) : null,
-+ pickedBots.map((bot) => (0, import_react28.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)(
"span",
{ key: bot.id, className: "bt-token" },
- (0, import_react20.createElement)(BotMark, { bot, size: 16 }),
-+ (0, import_react28.createElement)(BotMark, { bot, size: 16 }),
++ (0, import_react29.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_react28.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_react20.createElement)("input", {
-+ (0, import_react28.createElement)("input", {
++ (0, import_react29.createElement)("input", {
ref: inputRef,
autoFocus: true,
value: query,
-@@ -8517,20 +8517,20 @@
+@@ -8517,20 +8517,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
} else if (event.key === "Backspace" && query === "") unpick();
}
}),
- group && admin !== null ? (0, import_react20.createElement)("button", {
-+ group && admin !== null ? (0, import_react28.createElement)("button", {
++ group && admin !== null ? (0, import_react29.createElement)("button", {
type: "button",
className: "bt-send bt-to-create",
disabled: busy || picked.length === 0,
@@ -10813,86 +13882,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_react28.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_react20.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react20.createElement)(
-+ (0, import_react28.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react28.createElement)(
++ (0, import_react29.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react29.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_react28.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ options.length === 0 ? (0, import_react28.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_react28.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)(
"button",
{
key: option.key,
-@@ -8542,21 +8542,36 @@
+@@ -8542,21 +8542,36 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
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_react28.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react28.createElement)("span", { className: "bt-option-icon" }, option.icon),
++ option.bot ? (0, import_react29.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react29.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_react28.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react28.createElement)("span", { className: "bt-option-copy" }, (0, import_react28.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react28.createElement)("span", { className: "bt-option-sub" }, option.hint)),
-+ option.bot && option.hint && index === active ? (0, import_react28.createElement)("span", { className: "bt-option-hint" }, option.hint) : null
++ 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
))
)),
- (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_react28.createElement)("div", { className: "bt-nc-spacer" }),
-+ error && fix ? (0, import_react28.createElement)(
- "div",
++ menuOnly ? null : (0, import_react29.createElement)("div", { className: "bt-nc-spacer" }),
++ error && fix ? (0, import_react29.createElement)(
++ "div",
+ { className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" },
-+ (0, import_react28.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_react28.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_react28.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => {
++ (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: () => {
+ void copyInvite(bot);
+ } }, t("Copy invite link for {name}", { name: bot.name })) : null,
-+ (0, import_react28.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_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 }))
+ ])),
-+ fixNote ? (0, import_react28.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null,
-+ inviteLink ? (0, import_react28.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_react28.createElement)("div", { className: "bt-error" }, error) : null,
-+ menuOnly ? null : (0, import_react28.createElement)(
-+ "div",
++ 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)(
+ "div",
{ className: "bt-nc-compose" },
- (0, import_react20.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react28.createElement)(PlusIcon)),
-+ (0, import_react28.createElement)("textarea", {
++ (0, import_react29.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react29.createElement)(PlusIcon)),
++ (0, import_react29.createElement)("textarea", {
rows: 1,
value: message,
placeholder,
-@@ -8569,7 +8569,7 @@
+@@ -8569,7 +8569,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
}
}
}),
- (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_react28.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react28.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))
)
)
)
-@@ -8577,18 +8577,18 @@
+@@ -8577,18 +8577,18 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
}
// src/client/exchange.js
-var import_react21 = require("react");
-+var import_react29 = require("react");
++var import_react30 = 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_react29.useState)(null);
-+ const [leaving, setLeaving] = (0, import_react29.useState)(false);
++ const [entries2, setEntries] = (0, import_react30.useState)(null);
++ const [leaving, setLeaving] = (0, import_react30.useState)(false);
const close = () => {
if (leaving) return;
setLeaving(true);
@@ -10900,83 +13969,83 @@ diff --git a/client.js b/client.js
};
useEscape(close);
- (0, import_react21.useEffect)(() => {
-+ (0, import_react29.useEffect)(() => {
++ (0, import_react30.useEffect)(() => {
let stop = false;
let timer;
let seen = null;
-@@ -8612,48 +8612,48 @@
+@@ -8612,48 +8612,48 @@ function ExchangeDialog({ pair, roster, actions }) {
const right = roster.byId[pair[1]];
let lastDay = 0;
const paneLeft = usePaneLeft();
- return (0, import_react21.createElement)(
-+ return (0, import_react29.createElement)(
++ return (0, import_react30.createElement)(
"div",
{ className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 },
- (0, import_react21.createElement)(
-+ (0, import_react29.createElement)(
++ (0, import_react30.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_react29.createElement)("div", { className: "bt-head" }, (0, import_react29.createElement)(
++ (0, import_react30.createElement)("div", { className: "bt-head" }, (0, import_react30.createElement)(
"span",
{ className: "bt-pill", style: { cursor: "default" } },
- left ? (0, import_react21.createElement)(BotMark, { bot: left, size: 22 }) : null,
-+ left ? (0, import_react29.createElement)(BotMark, { bot: left, size: 22 }) : null,
++ left ? (0, import_react30.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_react29.createElement)("span", { className: "bt-swap" }, "⇄"),
-+ right ? (0, import_react29.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,
right?.name ?? "?"
)),
- (0, import_react21.createElement)(
-+ (0, import_react29.createElement)(
++ (0, import_react30.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_react29.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react29.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) => {
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_react29.createElement)(
-+ import_react29.Fragment,
++ return (0, import_react30.createElement)(
++ import_react30.Fragment,
{ key: entry.id },
- showDay ? (0, import_react21.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
- (0, import_react21.createElement)(
-+ showDay ? (0, import_react29.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
-+ (0, import_react29.createElement)(
++ showDay ? (0, import_react30.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
++ (0, import_react30.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_react29.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name),
-+ (0, import_react29.createElement)(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: (0, import_react29.createElement)("span", { className: "bt-lead" }, (0, import_react29.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_react21.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react21.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat")))
-+ (0, import_react29.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react29.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")))
)
);
}
// src/client/voice.js
-var import_react22 = require("react");
-+var import_react30 = require("react");
++var import_react31 = 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 @@
+@@ -8665,14 +8665,14 @@ 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_react22.useState)(0);
- (0, import_react22.useEffect)(() => {
-+ const [level, setLevel] = (0, import_react30.useState)(0);
-+ (0, import_react30.useEffect)(() => {
++ const [level, setLevel] = (0, import_react31.useState)(0);
++ (0, import_react31.useEffect)(() => {
if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0;
let stream = null;
let audio = null;
@@ -10987,7 +14056,7 @@ diff --git a/client.js b/client.js
if (stopped) {
media.getTracks().forEach((track) => track.stop());
return;
-@@ -8716,18 +8716,18 @@
+@@ -8716,18 +8716,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|";
});
@@ -11001,137 +14070,181 @@ 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_react30.useState)(() => actions.speech());
-+ const [phase, setPhase] = (0, import_react30.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
-+ const [catalog, setCatalog] = (0, import_react30.useState)(null);
-+ const [heard, setHeard] = (0, import_react30.useState)("");
-+ const [said, setSaid] = (0, import_react30.useState)("");
-+ const [note, setNote] = (0, import_react30.useState)("");
-+ const [serviceLevel, setServiceLevel] = (0, import_react30.useState)(0);
-+ const [cycle, setCycle] = (0, import_react30.useState)(0);
-+ const waiting = (0, import_react30.useRef)(null);
-+ const reply = (0, import_react30.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);
reply.current = lastReply;
- const turn = (0, import_react22.useRef)(0);
-+ const turn = (0, import_react30.useRef)(0);
++ const turn = (0, import_react31.useRef)(0);
const paneLeft = usePaneLeft();
const close = () => {
turn.current += 1;
-@@ -8747,12 +8747,12 @@
+@@ -8747,12 +8747,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
dictation.stop();
submit(text);
});
- (0, import_react22.useEffect)(() => {
-+ (0, import_react30.useEffect)(() => {
++ (0, import_react31.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
}, [note]);
- (0, import_react22.useEffect)(() => {
-+ (0, import_react30.useEffect)(() => {
++ (0, import_react31.useEffect)(() => {
if (!speech) return void 0;
let live = true;
speechCatalog(speech).then(({ catalog: next, usable }) => {
-@@ -8767,16 +8767,16 @@
+@@ -8767,16 +8767,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
};
}, [speech]);
const listen = phase === "listening";
- (0, import_react22.useEffect)(() => {
-+ (0, import_react30.useEffect)(() => {
++ (0, import_react31.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_react30.useEffect)(() => {
++ (0, import_react31.useEffect)(() => {
if (phase !== "listening") dictation.stop();
}, [phase]);
- (0, import_react22.useEffect)(() => {
-+ (0, import_react30.useEffect)(() => {
++ (0, import_react31.useEffect)(() => {
if (!speech || !catalog || !listen) return void 0;
const own = turn.current;
let capture = null;
-@@ -8838,7 +8838,7 @@
- setServiceLevel(0);
- };
- }, [speech, catalog, listen, cycle]);
+@@ -8797,369 +8797,125 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
+ 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_react22.useEffect)(() => {
-+ (0, import_react30.useEffect)(() => {
- if (phase !== "thinking" || running || waiting.current === null) return;
- const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)];
- if (count === waiting.current || !text.trim()) return;
-@@ -8858,7 +8858,7 @@
- window.speechSynthesis.cancel();
- window.speechSynthesis.speak(utterance);
- }, [phase, running, lastReply]);
+- 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_react22.useEffect)(() => () => window.speechSynthesis?.cancel(), []);
-+ (0, import_react30.useEffect)(() => () => window.speechSynthesis?.cancel(), []);
- const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening);
- const level = speech ? serviceLevel : micLevel;
- const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle";
-@@ -8875,292 +8875,48 @@
- const openSetup = phase === "setup" ? actions.voiceSetup() : null;
- const caption = phase === "speaking" ? said : heard;
- const shownNote = note || dictation.note;
+- 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;
- return (0, import_react22.createElement)(
-+ return (0, import_react30.createElement)(
- "div",
- { className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
+- "div",
+- { className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
- (0, import_react22.createElement)(
-+ (0, import_react30.createElement)(
- "div",
- { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase },
+- "div",
+- { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase },
- (0, import_react22.createElement)(
-+ (0, import_react30.createElement)(
- "div",
- { className: "bt-voice-orb", style: { "--bt-level": level } },
+- "div",
+- { className: "bt-voice-orb", style: { "--bt-level": level } },
- (0, import_react22.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
- (0, import_react22.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
- bot ? (0, import_react22.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react22.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
-+ (0, import_react30.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
-+ (0, import_react30.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
-+ bot ? (0, import_react30.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
- ),
+- ),
- (0, import_react22.createElement)("div", { className: "bt-voice-name" }, name),
- (0, import_react22.createElement)(
-+ (0, import_react30.createElement)("div", { className: "bt-voice-name" }, name),
-+ (0, import_react30.createElement)(
- "div",
- { className: "bt-voice-status", role: "status" },
+- "div",
+- { className: "bt-voice-status", role: "status" },
- phase === "thinking" ? (0, import_react22.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react22.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
-+ phase === "thinking" ? (0, import_react30.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react30.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
- ),
+- ),
- (0, import_react22.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
- openSetup ? (0, import_react22.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
-+ (0, import_react30.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
-+ openSetup ? (0, import_react30.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
- close();
- openSetup();
- } }, t("Set up voice input")) : null,
+- close();
+- openSetup();
+- } }, t("Set up voice input")) : null,
- shownNote ? (0, import_react22.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
-+ shownNote ? (0, import_react30.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
- ),
+- ),
- (0, import_react22.createElement)(
-+ (0, import_react30.createElement)(
- "div",
- { className: "bt-voice-bar" },
+- "div",
+- { className: "bt-voice-bar" },
- (0, import_react22.createElement)("button", {
-+ (0, import_react30.createElement)("button", {
- type: "button",
- className: "bt-voice-btn",
- "aria-pressed": phase === "paused",
- disabled: blocked,
- "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"),
- onClick: togglePause
+- type: "button",
+- className: "bt-voice-btn",
+- "aria-pressed": phase === "paused",
+- disabled: blocked,
+- "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"),
+- onClick: togglePause
- }, (0, import_react22.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react22.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
- (0, import_react22.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react22.createElement)(CloseIcon))
-+ }, (0, import_react30.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react30.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
-+ (0, import_react30.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react30.createElement)(CloseIcon))
- )
- );
- }
-
+- )
+- );
+-}
+-
-// src/client/group-settings.js
-var import_react23 = require("react");
-var ROOM_MODES = [
@@ -11308,7 +14421,81 @@ diff --git a/client.js b/client.js
- }));
- if (done && room2) onCreated(room2);
- else setBusy(false);
-- };
++ }
++ };
++ 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",
@@ -11318,14 +14505,22 @@ 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)(
-- "div",
++ const openSetup = phase === "setup" ? actions.voiceSetup() : null;
++ const caption = phase === "speaking" ? said : heard;
++ const shownNote = note || dictation.note;
++ return (0, import_react31.createElement)(
+ "div",
- { className: "bt-set-card bt-ng" },
- (0, import_react23.createElement)(
-- "div",
++ { className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
++ (0, import_react31.createElement)(
+ "div",
- { className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))),
- (0, import_react23.createElement)(
-- "div",
++ { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase },
++ (0, import_react31.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"))
- )
@@ -11335,7 +14530,14 @@ 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)(
-- "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_react31.createElement)("div", { className: "bt-voice-name" }, name),
++ (0, import_react31.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"))
- )
@@ -11345,15 +14547,35 @@ diff --git a/client.js b/client.js
- { className: "bt-set-row bt-bs-row bt-ng-step" },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
- (0, import_react23.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) })
-- ),
++ { className: "bt-voice-status", role: "status" },
++ phase === "thinking" ? (0, import_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)(
-- "div",
++ (0, import_react31.createElement)(
+ "div",
- { className: "bt-set-row bt-task-foot" },
- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
- (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat"))
-- )
-- );
--}
++ { className: "bt-voice-bar" },
++ (0, import_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}
@@ -11375,11 +14597,10 @@ diff --git a/client.js b/client.js
-.bt-bs-stack .bt-seg{align-self:stretch}
-@media (prefers-reduced-motion:reduce){.bt-gs-pick{animation:none}}
-`;
--
+
// src/client/usage-data.js
var HOUR = 0;
- var OWNER = 1;
-@@ -9277,13 +9277,13 @@
+@@ -9277,13 +9277,13 @@ function summarize(view, range, keep = () => true, seriesOf = () => "all") {
}
return result;
}
@@ -11395,12 +14616,12 @@ diff --git a/client.js b/client.js
}
return String(Math.round(count));
}
-@@ -9294,11 +9294,11 @@
+@@ -9294,11 +9294,11 @@ function percent(part, whole) {
}
// src/client/usage-page.js
-var import_react24 = require("react");
-+var import_react31 = require("react");
++var import_react32 = require("react");
var HEAT_WEEKS = 53;
var TOP_SERIES = 7;
var RING_R = 42;
@@ -11409,61 +14630,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 @@
+@@ -9341,9 +9341,9 @@ function titleOf(range, bucket) {
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_react31.useState)(0);
-+ const from = (0, import_react31.useRef)(0);
-+ (0, import_react31.useEffect)(() => {
++ const [shown, setShown] = (0, import_react32.useState)(0);
++ const from = (0, import_react32.useRef)(0);
++ (0, import_react32.useEffect)(() => {
if (reducedMotion() || typeof requestAnimationFrame !== "function") {
from.current = value;
setShown(value);
-@@ -9363,9 +9363,9 @@
+@@ -9363,9 +9363,9 @@ function useTween(value) {
}, [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_react31.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react31.createElement)(icon3));
++var glyphBox = (size, icon3) => (0, import_react32.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react32.createElement)(icon3));
function ModelGlyph({ name, paint, size }) {
- return (0, import_react24.createElement)("span", {
-+ return (0, import_react31.createElement)("span", {
++ return (0, import_react32.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 @@
+@@ -9382,7 +9382,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_react24.createElement)(BotAvatar, { bot, size, badge: false, live: false })
-+ glyph: (size) => (0, import_react31.createElement)(BotAvatar, { bot, size, badge: false, live: false })
++ glyph: (size) => (0, import_react32.createElement)(BotAvatar, { bot, size, badge: false, live: false })
};
}
if (owner.kind === "room") {
-@@ -9392,7 +9392,7 @@
+@@ -9392,7 +9392,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_react24.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
-+ glyph: (size) => room2 ? (0, import_react31.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
++ glyph: (size) => room2 ? (0, import_react32.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
};
}
if (owner.kind === "agent") {
-@@ -9414,30 +9414,30 @@
+@@ -9414,30 +9414,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_react24.createElement)(
-+ return (0, import_react31.createElement)(
++ return (0, import_react32.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_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ options.map(([id, text, icon3]) => (0, import_react31.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react31.createElement)(icon3) : 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))
);
}
function Stats({ range, total, previous }) {
@@ -11471,34 +14692,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_react31.createElement)("div", { className: "bt-us-stat" }, (0, import_react31.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react31.createElement)("span", { className: "bt-us-stat-value" }, value));
-+ return (0, import_react31.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)(
"section",
{ className: "bt-us-stats", style: { "--i": 0 } },
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
-+ (0, import_react31.createElement)(
++ (0, import_react32.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
++ (0, import_react32.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_react31.createElement)("small", null, t("tokens")),
-+ change === null ? null : (0, import_react31.createElement)(
++ (0, import_react32.createElement)("small", null, t("tokens")),
++ change === null ? null : (0, import_react32.createElement)(
"span",
{ className: "bt-us-delta", title: compareText(range) },
`${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%`
-@@ -9455,83 +9455,83 @@
+@@ -9455,83 +9455,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_react24.useState)(null);
-+ const [hover, setHover] = (0, import_react31.useState)(null);
++ const [hover, setHover] = (0, import_react32.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]));
@@ -11506,77 +14727,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_react31.createElement)(
++ return (0, import_react32.createElement)(
"section",
{ className: "bt-us-card bt-us-chart", style: { "--i": 1 } },
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)("h3", null, t("Tokens over time"), (0, import_react31.createElement)("span", null, unitLabel(range.unit))),
-+ onBy ? (0, import_react31.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_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.createElement)(
"div",
{ className: "bt-us-chart-body" },
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react31.createElement)("span", null, compact(top * level)))),
-+ (0, import_react31.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react31.createElement)("span", null, "0")),
-+ (0, import_react31.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)(
"div",
{ className: "bt-us-cols" },
- columns.map((column, at) => (0, import_react24.createElement)("div", {
-+ columns.map((column, at) => (0, import_react31.createElement)("div", {
++ columns.map((column, at) => (0, import_react32.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_react31.createElement)(
++ }, column.total > 0 ? (0, import_react32.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_react31.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null)
-+ ) : (0, import_react31.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" })))
),
- tip ? (0, import_react24.createElement)(
-+ tip ? (0, import_react31.createElement)(
++ tip ? (0, import_react32.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_react31.createElement)("div", { className: "bt-us-tip-title" }, tip.title),
-+ (0, import_react31.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_react31.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)(
"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_react31.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }),
-+ (0, import_react31.createElement)("span", null, tipOf.get(key)?.name),
-+ (0, import_react31.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))
))
) : null
),
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)(
++ columns.map((column, at) => (0, import_react32.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 : ""
@@ -11584,103 +14805,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_react31.createElement)(
++ series.length > 1 || series[0]?.pick ? (0, import_react32.createElement)(
"div",
{ className: "bt-us-legend" },
- series.map((entry) => (0, import_react24.createElement)(entry.pick ? "button" : "span", {
-+ series.map((entry) => (0, import_react31.createElement)(entry.pick ? "button" : "span", {
++ series.map((entry) => (0, import_react32.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_react31.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))
) : 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_react31.createElement)(
++ return (0, import_react32.createElement)(
onClick ? "button" : "div",
{
className: "bt-us-row",
-@@ -9540,69 +9540,69 @@
+@@ -9540,69 +9540,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick
title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }),
...onClick ? { type: "button", onClick } : {}
},
- glyph ? (0, import_react24.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
- (0, import_react24.createElement)(
-+ glyph ? (0, import_react31.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
-+ (0, import_react31.createElement)(
++ glyph ? (0, import_react32.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
++ (0, import_react32.createElement)(
"span",
{ className: "bt-us-row-body" },
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)("span", { className: "bt-us-row-name" }, name),
-+ sub ? (0, import_react31.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_react24.createElement)("span", { className: "bt-us-track" }, (0, import_react24.createElement)("span", { style: { width: `${width}%`, background: fill2 } }))
-+ (0, import_react31.createElement)("span", { className: "bt-us-track" }, (0, import_react31.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_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_react31.createElement)("span", { className: "bt-us-row-num" }, (0, import_react31.createElement)("b", null, compact(sum.total)), (0, import_react31.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)}%`))
);
}
function ListCard({ title, count, index, children }) {
- return (0, import_react24.createElement)(
-+ return (0, import_react31.createElement)(
++ return (0, import_react32.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_react31.createElement)("div", { className: "bt-us-card-head" }, (0, import_react31.createElement)("h3", null, title, count === void 0 ? null : (0, import_react31.createElement)("span", null, count))),
-+ (0, import_react31.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)
);
}
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_react31.createElement)(
++ return (0, import_react32.createElement)(
"section",
{ className: "bt-us-profile" },
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.createElement)(
"span",
{ className: "bt-us-ring" },
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)("defs", null, (0, import_react31.createElement)(
++ face.stops ? (0, import_react32.createElement)("defs", null, (0, import_react32.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_react31.createElement)("stop", { key: offset, offset, stopColor: color }))
++ face.stops.map(([offset, color]) => (0, import_react32.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_react31.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }),
-+ arc > 0 ? (0, import_react31.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)("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_react24.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64))
-+ (0, import_react31.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_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)("h3", null, face.name),
-+ face.sub ? (0, import_react31.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null,
-+ (0, import_react31.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) }))
)
);
}
@@ -11688,77 +14909,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_react31.useRef)(null);
++ const box = (0, import_react32.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_react31.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today))));
-+ const [anchor, setAnchor] = (0, import_react31.useState)(null);
-+ const [hover, setHover] = (0, import_react31.useState)(null);
-+ (0, import_react31.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 onDown = (event) => {
if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose();
};
-@@ -9628,17 +9628,17 @@
+@@ -9628,17 +9628,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
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_react31.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 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_react31.createElement)("span", { key: `b${at}` }));
++ const cells = Array.from({ length: lead }, (_, at) => (0, import_react32.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_react31.createElement)("button", {
++ cells.push((0, import_react32.createElement)("button", {
key,
type: "button",
className: "bt-us-day",
-@@ -9654,15 +9654,15 @@
+@@ -9654,15 +9654,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
"data-has": flag(active.has(key)),
onMouseEnter: () => setHover(key),
onClick: () => press(key)
- }, (0, import_react24.createElement)("b", null, day)));
-+ }, (0, import_react31.createElement)("b", null, day)));
++ }, (0, import_react32.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_react31.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_react31.createElement)(ChevronLeftIcon)) : (0, import_react31.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_react31.createElement)(ChevronRightIcon));
-+ return (0, import_react31.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)(
"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_react31.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react31.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))),
-+ (0, import_react31.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react31.createElement)("span", { key: at }, name))),
-+ (0, import_react31.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)
);
};
const shortcuts = [
-@@ -9671,30 +9671,30 @@
+@@ -9671,30 +9671,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_react24.createElement)(
-+ return (0, import_react31.createElement)(
++ return (0, import_react32.createElement)(
"div",
{ ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") },
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)("button", {
++ shortcuts.map(([id, label, from, to]) => (0, import_react32.createElement)("button", {
key: id,
type: "button",
"aria-pressed": span?.from === from && span?.to === to,
@@ -11767,110 +14988,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_react31.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
-+ (0, import_react31.createElement)(
++ (0, import_react32.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
++ (0, import_react32.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_react31.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")),
-+ low === null ? null : (0, import_react31.createElement)("span", null, (0, import_react31.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)}`)
)
);
}
function Heat({ view, keep, paint, index }) {
- const [hover, setHover] = (0, import_react24.useState)(null);
-+ const [hover, setHover] = (0, import_react31.useState)(null);
++ const [hover, setHover] = (0, import_react32.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 @@
+@@ -9710,13 +9710,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_react24.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday)));
-+ months.push((0, import_react31.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)));
}
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_react31.createElement)("span", {
++ cells.push((0, import_react32.createElement)("span", {
key: ms,
className: "bt-us-cell",
style: { "--w": week },
-@@ -9724,7 +9724,7 @@
+@@ -9724,7 +9724,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_react24.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
-+ }, level > 0 ? (0, import_react31.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));
}
}
const tipSide = hover ? {
-@@ -9733,61 +9733,61 @@
+@@ -9733,61 +9733,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_react24.createElement)(
-+ return (0, import_react31.createElement)(
++ return (0, import_react32.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_react31.createElement)("div", { className: "bt-us-card-head" }, (0, import_react31.createElement)("h3", null, t("Activity")), (0, import_react31.createElement)("span", { className: "bt-us-head-note" }, summary)),
-+ (0, import_react31.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months),
-+ (0, import_react31.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)(
"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_react31.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
-+ hover ? (0, import_react31.createElement)(
++ (0, import_react32.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
++ hover ? (0, import_react32.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_react31.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)),
-+ (0, import_react31.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) }))
) : null
),
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react31.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) } }))),
t("Heavy")
)
);
}
function Skeleton2() {
- return (0, import_react24.createElement)(
-+ return (0, import_react31.createElement)(
++ return (0, import_react32.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_react31.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
-+ (0, import_react31.createElement)(
++ (0, import_react32.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
++ (0, import_react32.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_react31.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
-+ (0, import_react31.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
-+ (0, import_react31.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)(
"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_react31.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }),
-+ (0, import_react31.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 } })
)
)
);
@@ -11884,102 +15105,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_react31.useState)(null);
-+ const [error, setError] = (0, import_react31.useState)("");
-+ const [rangeId, setRangeId] = (0, import_react31.useState)("7d");
-+ const [span, setSpan] = (0, import_react31.useState)(null);
-+ const [picking, setPicking] = (0, import_react31.useState)(false);
-+ const [by, setBy] = (0, import_react31.useState)("bot");
-+ const tabs = (0, import_react31.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 focus = focusOf(focusId);
- (0, import_react24.useEffect)(() => {
-+ (0, import_react31.useEffect)(() => {
++ (0, import_react32.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 @@
+@@ -9797,7 +9797,7 @@ function UsagePage({ roster, actions, focusId }) {
const value = await actions.usage();
await resized;
if (stopped) return;
- (0, import_react24.startTransition)(() => setView(value));
-+ (0, import_react31.startTransition)(() => setView(value));
++ (0, import_react32.startTransition)(() => setView(value));
setError("");
timer = setTimeout(load, value.scanning ? 3e3 : 2e4);
} catch (failure) {
-@@ -9814,7 +9814,7 @@
+@@ -9814,7 +9814,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_react24.useMemo)(() => {
-+ const shaped = (0, import_react31.useMemo)(() => {
++ const shaped = (0, import_react32.useMemo)(() => {
if (!view) return null;
const faces2 = view.owners.map((owner) => ownerFace(owner, roster));
const lifetime = /* @__PURE__ */ new Map();
-@@ -9823,7 +9823,7 @@
+@@ -9823,7 +9823,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_react24.createElement)(ModelGlyph, { name, paint, size }) };
-+ return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react31.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 }) };
});
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 @@
+@@ -9871,23 +9871,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_react24.useRef)(null);
- (0, import_react24.useEffect)(() => {
-+ const root = (0, import_react31.useRef)(null);
-+ (0, import_react31.useEffect)(() => {
++ const root = (0, import_react32.useRef)(null);
++ (0, import_react32.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_react31.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react31.createElement)(Skeleton2);
++ return error ? (0, import_react32.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react32.createElement)(Skeleton2);
}
if (view.rows.length === 0) {
- return (0, import_react24.createElement)(
-+ return (0, import_react31.createElement)(
++ return (0, import_react32.createElement)(
"div",
{ className: "bt-us" },
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }),
-+ (0, import_react31.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")),
-+ (0, import_react31.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."))
)
);
}
-@@ -9912,7 +9912,7 @@
+@@ -9912,7 +9912,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_react24.createElement)(Row2, {
-+ const ownerRow = ([at, sum], index, total = whole) => (0, import_react31.createElement)(Row2, {
++ const ownerRow = ([at, sum], index, total = whole) => (0, import_react32.createElement)(Row2, {
key: `o${at}`,
kind: faces[at].kind,
glyph: faces[at].glyph(32),
-@@ -9924,7 +9924,7 @@
+@@ -9924,7 +9924,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_react24.createElement)(Row2, {
-+ const modelRow = ([at, sum], index) => (0, import_react31.createElement)(Row2, {
++ const modelRow = ([at, sum], index) => (0, import_react32.createElement)(Row2, {
key: `m${at}`,
kind: "model",
glyph: models[at].glyph(32),
-@@ -9936,30 +9936,30 @@
+@@ -9936,30 +9936,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_react24.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range."));
-+ const nothing = (0, import_react31.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."));
let body;
if (face) {
const kinds = ranked(part.kinds);
@@ -11988,88 +15209,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_react31.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
-+ (0, import_react31.createElement)(
++ (0, import_react32.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
++ (0, import_react32.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_react31.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
-+ (0, import_react31.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
-+ (0, import_react31.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)(
"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_react31.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react31.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_react31.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)(
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_react31.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
)
),
- (0, import_react24.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 })
-+ (0, import_react31.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 })
)
];
} else {
-@@ -9968,39 +9968,39 @@
+@@ -9968,39 +9968,39 @@ function UsagePage({ roster, actions, focusId }) {
const rest = owners.filter(([at]) => !isBot(at));
const usedModels = ranked(all.models);
body = [
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
-+ (0, import_react31.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
-+ (0, import_react31.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)(
"div",
{ key: "pair", className: "bt-us-pair" },
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.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,
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_react31.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_react24.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 })
-+ (0, import_react31.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 })
)
];
}
- return (0, import_react24.createElement)(
-+ return (0, import_react31.createElement)(
++ return (0, import_react32.createElement)(
"div",
{ ref: root, className: "bt-us", key: focusId ?? "all" },
- (0, import_react24.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }),
-+ view.scanning ? (0, import_react31.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null,
-+ picking ? (0, import_react31.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
),
- error ? (0, import_react24.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
-+ error ? (0, import_react31.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,
body
);
}
-@@ -10150,33 +10150,135 @@
+@@ -10150,33 +10150,135 @@ button.bt-us-row:hover{background:var(--bt-hover)}
@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)}}
@@ -12082,7 +15303,7 @@ diff --git a/client.js b/client.js
// src/client/connectors.js
-var import_react25 = require("react");
-+var import_react32 = require("react");
++var import_react33 = 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/";
@@ -12116,11 +15337,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_react32.createElement)(
++var GitHubMark = () => (0, import_react33.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_react32.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" })
);
-var MARKS = { github: GitHubMark };
-var ABOUT = { github: () => t("Repositories, issues, and pull requests") };
@@ -12144,20 +15365,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_react32.createElement)(
++ return (0, import_react33.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_react32.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
++ label ? (0, import_react33.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
+ );
+}
+function AppPicker({ apps, onChange, search, statuses = {} }) {
-+ const [query, setQuery] = (0, import_react32.useState)("");
-+ const [found, setFound] = (0, import_react32.useState)(null);
-+ const [failed, setFailed] = (0, import_react32.useState)(null);
-+ const names = (0, import_react32.useRef)(new Map(APPS));
-+ const asked = (0, import_react32.useRef)(0);
-+ (0, import_react32.useEffect)(() => {
++ 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 text = query.trim();
+ if (text === "" || search === void 0) {
+ setFound(null);
@@ -12184,13 +15405,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_react32.createElement)(
++ return (0, import_react33.createElement)(
+ "div",
+ { className: "bt-connector-apps" },
-+ apps.length > 0 ? (0, import_react32.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react32.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null,
-+ (0, import_react32.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_react32.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null,
-+ (0, import_react32.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react32.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react32.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => {
++ 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: () => {
+ names.current.set(app.slug, app.name);
+ onChange([...apps, app.slug]);
+ } }, app.name)))
@@ -12198,32 +15419,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_react32.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react32.createElement)(
++ return (0, import_react33.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react33.createElement)(
+ "li",
+ { key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" },
-+ (0, import_react32.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)),
-+ (0, import_react32.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "")
++ (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]]?.() ?? "")
+ )));
+}
+function ConnectorRow({ connector, roster, actions }) {
-+ const [form, setForm] = (0, import_react32.useState)(false);
-+ const [bots, setBots] = (0, import_react32.useState)(false);
-+ const [token, setToken] = (0, import_react32.useState)("");
-+ const [apps, setApps] = (0, import_react32.useState)(() => connector.apps ?? []);
-+ const [picking, setPicking] = (0, import_react32.useState)(false);
-+ const [busy, setBusy] = (0, import_react32.useState)(false);
-+ const [error, setError] = (0, import_react32.useState)(null);
++ 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 run = (task, after) => {
setBusy(true);
setError(null);
-@@ -10184,72 +10184,128 @@
+@@ -10184,72 +10184,128 @@ function ConnectorRow({ connector, actions }) {
after?.();
}, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false));
};
+ const gateway = connector.id === "composio";
-+ const [statuses, setStatuses] = (0, import_react32.useState)({});
++ const [statuses, setStatuses] = (0, import_react33.useState)({});
+ const appsKey = (connector.apps ?? []).join(",");
-+ (0, import_react32.useEffect)(() => {
++ (0, import_react33.useEffect)(() => {
+ if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) {
+ setStatuses({});
+ return void 0;
@@ -12238,7 +15459,7 @@ diff --git a/client.js b/client.js
+ live = false;
+ };
+ }, [gateway, connector.state, appsKey, picking]);
-+ const search = (0, import_react32.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]);
++ const search = (0, import_react33.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), () => {
@@ -12264,113 +15485,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_react32.useRef)(null);
++ const row = (0, import_react33.useRef)(null);
+ useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}`);
-+ return (0, import_react32.createElement)(
++ return (0, import_react33.createElement)(
"div",
{ ref: row, className: "bt-connector", "data-state": connector.state },
- (0, import_react25.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react32.createElement)(Mark) : null),
-+ (0, import_react32.createElement)(
++ (0, import_react33.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react33.createElement)(Mark) : null),
++ (0, import_react33.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_react32.createElement)("span", { className: "bt-connector-name" }, connector.name),
-+ (0, import_react32.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_react25.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.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_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
++ 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: () => {
+ setApps(connector.apps ?? []);
+ setPicking(true);
+ } }, t("Choose apps")) : null,
-+ on ? (0, import_react32.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_react32.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_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect"))
++ 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 && form ? (0, import_react25.createElement)(
-+ on && gateway && !picking ? (0, import_react32.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null,
-+ on ? (0, import_react32.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null,
-+ on && bots ? (0, import_react32.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null,
-+ !on && form ? (0, import_react32.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)(
"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_react32.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react32.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })),
-+ gateway ? (0, import_react32.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null,
-+ (0, import_react32.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)(
"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_react32.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_react32.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: () => {
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_react32.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_react32.createElement)(
++ (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)(
+ "div",
+ { className: "bt-connector-form" },
-+ (0, import_react32.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }),
-+ (0, import_react32.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_react32.createElement)(
++ (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)(
+ "div",
+ { className: "bt-connector-form-row" },
-+ (0, import_react32.createElement)("span", { className: "bt-connector-link" }),
-+ (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
++ (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: () => {
+ setPicking(false);
+ setError(null);
+ } }, t("Cancel")),
-+ (0, import_react32.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_react32.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null
++ (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
);
}
function ConnectorsPage({ roster, actions }) {
const list = roster?.connectors ?? [];
- return (0, import_react25.createElement)(
-+ return (0, import_react32.createElement)(
++ return (0, import_react33.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_react32.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react32.createElement)("div", { className: "bt-set-row" }, (0, import_react32.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react32.createElement)(ConnectorRow, { key: connector.id, connector, roster, 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 })))
);
}
// src/client/feedback.js
-var import_react26 = require("react");
-var FeedbackIcon = () => (0, import_react26.createElement)(
-+var import_react33 = require("react");
-+var FeedbackIcon = () => (0, import_react33.createElement)(
++var import_react34 = require("react");
++var FeedbackIcon = () => (0, import_react34.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_react33.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_react33.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" })
);
function download(name, text) {
const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" }));
-@@ -10260,7 +10260,7 @@
+@@ -10260,7 +10260,7 @@ function download(name, text) {
setTimeout(() => URL.revokeObjectURL(url), 1e3);
}
var copy = (text) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text).then(() => true, () => false);
@@ -12379,7 +15600,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 @@
+@@ -10270,18 +10270,18 @@ function entrySummary(entry) {
if (entry.source === "turn") return failureLabel(entry.code, entry.text);
return entry.text;
}
@@ -12390,154 +15611,154 @@ 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_react33.useState)("");
-+ const [report, setReport] = (0, import_react33.useState)(null);
-+ const [preview, setPreview] = (0, import_react33.useState)(false);
-+ const [note, setNote] = (0, import_react33.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 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_react33.useEffect)(() => {
++ (0, import_react34.useEffect)(() => {
void run(load());
}, []);
const copyReport = () => run(fetchReport().then(async (next) => {
-@@ -10289,7 +10289,7 @@
+@@ -10289,7 +10289,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_react26.useState)(null);
-+ const [issueLink, setIssueLink] = (0, import_react33.useState)(null);
++ const [issueLink, setIssueLink] = (0, import_react34.useState)(null);
const openIssue = () => {
const win = window.open("", "_blank");
if (win !== null) win.opener = null;
-@@ -10317,59 +10317,59 @@
+@@ -10317,59 +10317,59 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) {
const total = (report?.total ?? 0) + browserErrors.length;
const shown = entries2.slice(0, 8);
return [
- (0, import_react26.createElement)(
-+ (0, import_react33.createElement)(
++ (0, import_react34.createElement)(
Section,
{ key: "report", title: t("Report a problem") },
- (0, import_react26.createElement)(
-+ (0, import_react33.createElement)(
++ (0, import_react34.createElement)(
"div",
{ className: "bt-set-row bt-set-paste bt-fb-what" },
- (0, import_react26.createElement)(
-+ (0, import_react33.createElement)(
++ (0, import_react34.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_react33.createElement)("span", null, t("What happened?")),
-+ (0, import_react33.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_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_react33.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_react26.createElement)(
-+ (0, import_react33.createElement)(
++ (0, import_react34.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_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")),
-+ (0, import_react33.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"))
),
- 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_react33.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react33.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null,
-+ (0, import_react33.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)(
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_react33.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
++ (0, import_react34.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
)
),
- note ? (0, import_react26.createElement)(
-+ note ? (0, import_react33.createElement)(
++ note ? (0, import_react34.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_react33.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
) : null,
- (0, import_react26.createElement)(
-+ (0, import_react33.createElement)(
++ (0, import_react34.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_react33.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_react33.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)(
"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_react33.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
-+ (0, import_react33.createElement)(
++ (0, import_react34.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
++ (0, import_react34.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_react33.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)),
-+ (0, import_react33.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].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(" · "))
),
- 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_react33.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
)),
- (0, import_react26.createElement)(
-+ (0, import_react33.createElement)(
++ (0, import_react34.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_react33.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")),
-+ (0, import_react33.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"))
)
)
];
-@@ -10377,8 +10377,8 @@
+@@ -10377,8 +10377,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_react26.useState)(false);
- (0, import_react26.useEffect)(() => {
-+ const [leaving, setLeaving] = (0, import_react33.useState)(false);
-+ (0, import_react33.useEffect)(() => {
++ const [leaving, setLeaving] = (0, import_react34.useState)(false);
++ (0, import_react34.useEffect)(() => {
if (!toast) return void 0;
setLeaving(false);
const fade = setTimeout(() => setLeaving(true), TOAST_MS);
-@@ -10389,15 +10389,16 @@
+@@ -10389,15 +10389,16 @@ function ErrorToast({ useUi, actions }) {
};
}, [toast?.token]);
if (!toast) return null;
- return (0, import_react26.createElement)(
+ const info = toast.tone === "info";
-+ return (0, import_react33.createElement)(
++ return (0, import_react34.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_react33.createElement)("span", { className: "bt-toast-text" }, toast.text),
-+ info ? null : (0, import_react33.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => {
++ (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: () => {
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_react33.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react33.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))
);
}
var FEEDBACK_CSS = `
-@@ -10428,35 +10428,44 @@
+@@ -10428,35 +10428,44 @@ var FEEDBACK_CSS = `
`;
// src/client/about.js
-var import_react27 = require("react");
-var CHEER_MS = 1800;
-+var import_react34 = require("react");
++var import_react35 = 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…");
@@ -12580,13 +15801,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_react34.useState)(false);
-+ const [stars, setStars] = (0, import_react34.useState)(null);
-+ (0, import_react34.useEffect)(() => {
++ const [checking, setChecking] = (0, import_react35.useState)(false);
++ const [stars, setStars] = (0, import_react35.useState)(null);
++ (0, import_react35.useEffect)(() => {
let live = true;
actions.repoStars().then((count) => {
if (live && Number.isInteger(count)) setStars(count);
-@@ -10466,87 +10466,72 @@
+@@ -10466,87 +10466,72 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) {
live = false;
};
}, []);
@@ -12614,14 +15835,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_react34.createElement)(
++ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react35.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_react34.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_react34.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_react34.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)(
"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")
@@ -12629,22 +15850,22 @@ diff --git a/client.js b/client.js
const repo = update.repoUrl;
return [
- (0, import_react27.createElement)(
-+ (0, import_react34.createElement)(
++ (0, import_react35.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_react34.createElement)("span", { className: "bt-about-mark" }, (0, import_react34.createElement)(FlyBotMark, { size: 52 })),
-+ (0, import_react34.createElement)(
++ (0, import_react35.createElement)("span", { className: "bt-about-mark" }, (0, import_react35.createElement)(FlyBotMark, { size: 52 })),
++ (0, import_react35.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_react34.createElement)("span", { className: "bt-about-name" }, t("FlyBot")),
-+ update.current ? (0, import_react34.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : 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
),
- repo ? (0, import_react27.createElement)(
-+ repo ? (0, import_react34.createElement)(
++ repo ? (0, import_react35.createElement)(
"a",
{
className: "bt-star",
@@ -12658,31 +15879,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_react34.createElement)("span", { className: "bt-star-main" }, (0, import_react34.createElement)("span", { className: "bt-star-icon" }, (0, import_react34.createElement)(StarLineIcon)), "Star"),
-+ stars === null ? null : (0, import_react34.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))
) : 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_react34.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_react34.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)(
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_react34.createElement)(Row3, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react34.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null),
-+ dsh?.current ? (0, import_react34.createElement)(Row3, { label: t("DeepSeek Harness (underlying)"), 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_react27.createElement)(
-+ (0, import_react34.createElement)(
++ (0, import_react35.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_react34.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react34.createElement)("a", linkProps(repo), (0, import_react34.createElement)(GitHubMark), t("Open"))) : null,
-+ update.notesUrl ? (0, import_react34.createElement)(Row3, { label: t("Release notes") }, (0, import_react34.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
-+ (0, import_react34.createElement)(Row3, { label: t("Report a problem") }, (0, import_react34.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")))
)
];
}
@@ -12697,29 +15918,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 @@
+@@ -10569,54 +10569,59 @@ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-s
`;
// src/client/settings.js
-var import_react28 = require("react");
-+var import_react35 = require("react");
++var import_react36 = 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_react35.createElement)("section", { className: "bt-set-section" }, (0, import_react35.createElement)("h3", null, title), (0, import_react35.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));
}
function SetRow({ label, hint, children }) {
- return (0, import_react28.createElement)(
-+ return (0, import_react35.createElement)(
++ return (0, import_react36.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_react35.createElement)("div", { className: "bt-set-copy" }, (0, import_react35.createElement)("span", null, label), hint ? (0, import_react35.createElement)("span", { className: "bt-set-hint" }, hint) : null),
-+ children ? (0, import_react35.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
);
}
+var NOTIFY_KINDS = [
@@ -12734,9 +15955,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_react35.useState)(prefs.accent ?? "#4d6bfe");
-+ const timer = (0, import_react35.useRef)(0);
-+ (0, import_react35.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 pickCustom = (value) => {
setCustom(value);
clearTimeout(timer.current);
@@ -12744,53 +15965,53 @@ diff --git a/client.js b/client.js
};
return [
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
SetSection,
{ key: "look", title: t("Appearance") },
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
SetRow,
{ label: t("Theme"), hint: t("Colors for the whole team, on every device.") },
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.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_react35.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)))
)
),
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
SetRow,
{ label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") },
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.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_react35.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_react35.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", {
key: ink,
type: "button",
className: "bt-swatch",
-@@ -10626,75 +10626,87 @@
+@@ -10626,75 +10626,87 @@ function GeneralPage({ roster, actions, run }) {
style: { background: INKS[ink][0] },
onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] }))
})),
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.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_react35.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_react28.createElement)(
+ // One switch for every system notification (#353), then one per kind (#469).
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
SetSection,
- { key: "mode", title: t("Mode") },
- (0, import_react28.createElement)(
@@ -12799,14 +16020,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_react35.createElement)(SwitchRow, {
++ (0, import_react36.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_react35.createElement)(SwitchRow, {
++ NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react36.createElement)(SwitchRow, {
+ key: kind,
+ label: t(label),
+ hint: hint ? t(hint) : void 0,
@@ -12824,40 +16045,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_react35.createElement)(
++ return (0, import_react36.createElement)(
SetSection,
{ title: t("Main Bot") },
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.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_react35.createElement)("select", {
++ (0, import_react36.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_react35.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react35.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)))
),
- others.map((bot) => (0, import_react28.createElement)(
-+ others.map((bot) => (0, import_react35.createElement)(
++ others.map((bot) => (0, import_react36.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_react35.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_react35.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"))
)),
- candidates.length > 0 ? (0, import_react28.createElement)(
-+ candidates.length > 0 ? (0, import_react35.createElement)(
++ candidates.length > 0 ? (0, import_react36.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_react35.createElement)("select", {
++ (0, import_react36.createElement)("select", {
className: "bt-select",
"aria-label": t("Add a Main Bot"),
value: "",
@@ -12865,7 +16086,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_react35.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react35.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)))
) : null
);
}
@@ -12873,47 +16094,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_react35.createElement)(
++ return (0, import_react36.createElement)(
SetSection,
{ title: t("Animation") },
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.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_react35.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react35.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", {
key: id,
type: "button",
"aria-pressed": motion === id,
-@@ -10706,69 +10706,69 @@
+@@ -10706,69 +10706,69 @@ function MotionSection({ roster, actions, run }) {
);
}
function BotsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react28.useState)(false);
-+ const [creating, setCreating] = (0, import_react35.useState)(false);
++ const [creating, setCreating] = (0, import_react36.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_react35.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
-+ (0, import_react35.createElement)(MotionSection, { key: "motion", roster, actions, run }),
-+ creating ? (0, import_react35.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react35.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)(
"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_react35.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react35.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_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.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_react35.createElement)(
++ roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react36.createElement)(
"button",
{ key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) },
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react28.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
@@ -12921,42 +16142,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_react35.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
-+ (0, import_react35.createElement)("span", { className: "bt-row-title" }, bot.name),
-+ bot.role ? (0, import_react35.createElement)("span", { className: "bt-tag" }, bot.role) : null,
-+ isMainOf(roster, bot.id) ? (0, import_react35.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
-+ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react35.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_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon))
-+ (0, import_react35.createElement)("span", { className: "bt-chevron" }, (0, import_react35.createElement)(ChevronRightIcon))
++ (0, import_react36.createElement)("span", { className: "bt-chevron" }, (0, import_react36.createElement)(ChevronRightIcon))
))
)
];
}
function GroupsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react28.useState)(false);
-+ const [creating, setCreating] = (0, import_react35.useState)(false);
++ const [creating, setCreating] = (0, import_react36.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_react35.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react35.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)(
"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_react35.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_react35.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_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.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_react35.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) => {
const admin = roster.byId[room2.admin];
- return (0, import_react28.createElement)(
-+ return (0, import_react35.createElement)(
++ return (0, import_react36.createElement)(
"button",
{ key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) },
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react28.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
@@ -12964,14 +16185,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_react35.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
-+ (0, import_react35.createElement)("span", { className: "bt-row-title" }, room2.name),
-+ (0, import_react35.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })),
-+ admin ? (0, import_react35.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react35.createElement)(AdminStar), admin.name) : null,
-+ room2.hidden ? (0, import_react35.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_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon))
-+ (0, import_react35.createElement)("span", { className: "bt-chevron" }, (0, import_react35.createElement)(ChevronRightIcon))
++ (0, import_react36.createElement)("span", { className: "bt-chevron" }, (0, import_react36.createElement)(ChevronRightIcon))
);
})
)
@@ -12980,12 +16201,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_react35.useState)("");
-+ const close = (0, import_react35.useCallback)(() => actions.closeSettings(), []);
++ const [error, setError] = (0, import_react36.useState)("");
++ const close = (0, import_react36.useCallback)(() => actions.closeSettings(), []);
useEscape(close);
const run = (promise) => Promise.resolve(promise).then(() => {
setError("");
-@@ -10779,104 +10779,112 @@
+@@ -10779,104 +10779,112 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false
});
const bot = page === "bots" && id ? roster.byId[id] : void 0;
const room2 = page === "groups" && id ? roster.roomsById[id] : void 0;
@@ -13002,34 +16223,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_react35.useRef)({ key: pageKey, depth, move: "up" });
++ const last = (0, import_react36.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_react35.useState)(shown === "usage");
++ const [usageOpened, setUsageOpened] = (0, import_react36.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_react35.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
-+ return (0, import_react35.createElement)(
++ const errorLine = error ? (0, import_react36.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
++ return (0, import_react36.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_react35.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
++ (0, import_react36.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_react35.createElement)(
++ (0, import_react36.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_react35.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ pages.map(([key, text]) => (0, import_react35.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", {
key,
type: "button",
className: "bt-settings-item",
@@ -13037,10 +16258,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_react35.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text))),
++ }, (0, import_react36.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_react35.createElement)("button", {
++ (0, import_react36.createElement)("button", {
type: "button",
className: "bt-settings-item bt-settings-shell",
- title: t("Models, providers, and the rest of DeepSeek Harness"),
@@ -13051,23 +16272,23 @@ diff --git a/client.js b/client.js
openHarnessSettings();
}
- }, (0, import_react28.createElement)(SlidersIcon), t("Harness settings"))
-+ }, (0, import_react35.createElement)(SlidersIcon), t("Harness settings"), (0, import_react35.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
++ }, (0, import_react36.createElement)(SlidersIcon), t("Harness settings"), (0, import_react36.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
),
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
"div",
{ className: "bt-settings-main" },
- (0, import_react28.createElement)(
-+ (0, import_react35.createElement)(
++ (0, import_react36.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_react35.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react35.createElement)(ChevronLeftIcon)) : null,
-+ (0, import_react35.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)
),
- shown === "usage" ? null : (0, import_react28.createElement)(
-+ shown === "usage" ? null : (0, import_react35.createElement)(
++ shown === "usage" ? null : (0, import_react36.createElement)(
"div",
{ key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move },
errorLine,
@@ -13082,41 +16303,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_react35.createElement)(GeneralPage, { roster, actions, run }) : null,
-+ bot ? (0, import_react35.createElement)("div", { className: "bt-settings-form" }, (0, import_react35.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
-+ shown === "bots" && !bot ? (0, import_react35.createElement)(BotsPage, { roster, actions, run }) : null,
-+ room2 ? (0, import_react35.createElement)("div", { className: "bt-settings-form" }, (0, import_react35.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
-+ shown === "groups" && !room2 ? (0, import_react35.createElement)(GroupsPage, { roster, actions, run }) : null,
-+ shown === "connectors" ? (0, import_react35.createElement)(ConnectorsPage, { roster, actions }) : null,
-+ pluginPage ? (0, import_react35.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null,
-+ shown === "capabilities" ? (0, import_react35.createElement)(CapabilitiesPage, { roster, actions, run }) : null,
-+ shown === "schedules" ? (0, import_react35.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
-+ shown === "secrets" ? (0, import_react35.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
-+ shown === "feedback" ? (0, import_react35.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
-+ shown === "about" ? (0, import_react35.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
),
- usageOpened ? (0, import_react28.createElement)(
-+ usageOpened ? (0, import_react35.createElement)(
++ usageOpened ? (0, import_react36.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_react35.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
++ (0, import_react36.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_react35.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react35.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))
)
);
}
// src/client/context-menu.js
-var import_react29 = require("react");
-+var import_react36 = require("react");
++var import_react37 = require("react");
function ContextMenu({ menu, roster, actions, leaving = false }) {
useEscape(() => actions.closeMenu());
- (0, import_react29.useEffect)(() => {
-+ (0, import_react36.useEffect)(() => {
++ (0, import_react37.useEffect)(() => {
const onDown = (event) => {
if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu();
};
@@ -13127,25 +16348,25 @@ 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_react36.useRef)(null);
-+ const [place, setPlace] = (0, import_react36.useState)({ left: menu.x, top: menu.y });
-+ (0, import_react36.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 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_react36.createElement)(AgentMenu, { menu, roster, actions, leaving });
++ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react37.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 @@
+@@ -10884,13 +10884,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
const isMainBot = isMainOf(roster, menu.id);
const isTile = roster.mainBotId === menu.id;
const mainCount = (roster.mainBotIds ?? []).length;
- const groups = [
+ const groups = bot?.remote ? [
- [
++ [
+ [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)]
+ ],
+ [
@@ -13154,7 +16375,7 @@ diff --git a/client.js b/client.js
+ pluginItems(structure, bot, actions).map(([label, run]) => [label, LinkIcon, run]),
+ [[t("Copy conversation ID"), CopyIcon, () => navigator.clipboard?.writeText(currentPart(bot))]]
+ ] : [
-+ [
+ [
isTile ? null : [entry.pinned ? t("Unpin") : t("Pin"), PinIcon, () => actions.setFlags(menu.id, { pinned: !entry.pinned })],
[t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)]
],
@@ -13164,7 +16385,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 @@
+@@ -10901,20 +10901,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
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
],
@@ -13178,27 +16399,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_react36.createElement)(
++ return (0, import_react37.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_react36.createElement)(
-+ import_react36.Fragment,
++ shown.map((group, index) => (0, import_react37.createElement)(
++ import_react37.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_react36.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
-+ group.map(([label, Icon, run, danger]) => (0, import_react36.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", {
key: label,
type: "button",
role: "menuitem",
-@@ -10923,14 +10923,14 @@
+@@ -10923,14 +10923,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
actions.closeMenu();
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react29.createElement)(Icon), label))
-+ }, (0, import_react36.createElement)(Icon), label))
++ }, (0, import_react37.createElement)(Icon), label))
))
);
}
@@ -13206,38 +16427,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_react36.useRef)(null);
-+ const [place, setPlace] = (0, import_react36.useState)({ left: menu.x, top: menu.y });
-+ (0, import_react36.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 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 @@
+@@ -10952,14 +10952,14 @@ function AgentMenu({ menu, roster, actions, leaving }) {
[t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true]
]
];
- return (0, import_react29.createElement)(
-+ return (0, import_react36.createElement)(
++ return (0, import_react37.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_react36.createElement)(
-+ import_react36.Fragment,
++ groups.map((group, index) => (0, import_react37.createElement)(
++ import_react37.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_react36.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
-+ group.map(([label, Icon, run, danger]) => (0, import_react36.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", {
key: label,
type: "button",
role: "menuitem",
-@@ -10968,16 +10968,31 @@
+@@ -10968,303 +10968,93 @@ function AgentMenu({ menu, roster, actions, leaving }) {
actions.closeMenu();
void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error));
}
- }, (0, import_react29.createElement)(Icon), label))
-+ }, (0, import_react36.createElement)(Icon), label))
++ }, (0, import_react37.createElement)(Icon), label))
))
);
}
@@ -13260,15 +16481,67 @@ diff --git a/client.js b/client.js
var STORAGE = {
unread: "ds-bot:unread",
reactions: "ds-bot:reactions",
- seen: "ds-bot:seen",
-+ friendLines: "ds-bot:friend-lines",
- prefs: "ds-bot:prefs",
- surface: "ds-bot:surface",
- agentSessions: "ds-bot:agent-sessions",
-@@ -11038,233 +11038,8 @@
- }
- }
-
+- seen: "ds-bot:seen",
+- prefs: "ds-bot:prefs",
+- surface: "ds-bot:surface",
+- agentSessions: "ds-bot:agent-sessions",
+- browserWidth: "ds-bot:browser-width",
+- browsers: "ds-bot.browser.v2"
+-};
+-var LEGACY_PREFIXES = ["dsh-bot:", "dsh-bot-team:"];
+-var LEGACY_BROWSER_KEYS = ["ds-bot.browser.v1", "dsh-bot.browser.v1"];
+-var LEGACY_THEMES = { grok: "mono" };
+-function legacyKeys(key) {
+- if (key === STORAGE.browsers) return LEGACY_BROWSER_KEYS;
+- const name = key.slice(key.indexOf(":") + 1);
+- return LEGACY_PREFIXES.map((prefix) => `${prefix}${name}`);
+-}
+-var migrateBrowsers = (storage) => {
+- if (storage.getItem(STORAGE.browsers) !== null) {
+- for (const key of LEGACY_BROWSER_KEYS) storage.removeItem(key);
+- return;
+- }
+- for (const key of LEGACY_BROWSER_KEYS) {
+- try {
+- const value = storage.getItem(key);
+- if (value === null) continue;
+- const previous = JSON.parse(value) ?? {};
+- const tabs = Object.fromEntries(Object.entries(previous).filter(([, page]) => page && typeof page === "object").map(([botId, page]) => {
+- const tab = { id: `v1-${botId}`, url: page.url ?? "", title: page.title ?? "" };
+- return [botId, { tabs: [tab], active: tab.id }];
+- }));
+- storage.setItem(STORAGE.browsers, JSON.stringify(tabs));
+- for (const earlier of LEGACY_BROWSER_KEYS) storage.removeItem(earlier);
+- return;
+- } catch {
+- }
+- }
+-};
+-function migrateStorage(storage = globalThis.localStorage) {
+- for (const key of Object.values(STORAGE)) {
+- if (key === STORAGE.browsers) continue;
+- for (const legacy of legacyKeys(key)) {
+- try {
+- const value = storage.getItem(legacy);
+- if (value !== null && storage.getItem(key) === null) storage.setItem(key, value);
+- storage.removeItem(legacy);
+- } catch {
+- }
+- }
+- }
+- try {
+- migrateBrowsers(storage);
+- } catch {
+- }
+- try {
+- const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null");
+- if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) {
+- storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] }));
+- }
+- } catch {
+- }
+-}
+-
-// src/client/member-status.js
-var import_react30 = require("react");
-var NO_WORK = { bots: {}, rooms: {}, approvals: [] };
@@ -13451,13 +16724,72 @@ diff --git a/client.js b/client.js
- entry.here ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null
- )
- );
--}
++ seen: "ds-bot:seen",
++ friendLines: "ds-bot:friend-lines",
++ prefs: "ds-bot:prefs",
++ surface: "ds-bot:surface",
++ agentSessions: "ds-bot:agent-sessions",
++ browserWidth: "ds-bot:browser-width",
++ browsers: "ds-bot.browser.v2"
++};
++var LEGACY_PREFIXES = ["dsh-bot:", "dsh-bot-team:"];
++var LEGACY_BROWSER_KEYS = ["ds-bot.browser.v1", "dsh-bot.browser.v1"];
++var LEGACY_THEMES = { grok: "mono" };
++function legacyKeys(key) {
++ if (key === STORAGE.browsers) return LEGACY_BROWSER_KEYS;
++ const name = key.slice(key.indexOf(":") + 1);
++ return LEGACY_PREFIXES.map((prefix) => `${prefix}${name}`);
+ }
-function useMemberWork(room2, useWork) {
- const work = useWork?.((value) => value) ?? NO_WORK;
- const entries2 = room2 ? memberWork(room2, work) : [];
- const now = useNow2(entries2.some((entry) => entry.state !== "idle"));
- return { entries: entries2, now };
--}
++var migrateBrowsers = (storage) => {
++ if (storage.getItem(STORAGE.browsers) !== null) {
++ for (const key of LEGACY_BROWSER_KEYS) storage.removeItem(key);
++ return;
++ }
++ for (const key of LEGACY_BROWSER_KEYS) {
++ try {
++ const value = storage.getItem(key);
++ if (value === null) continue;
++ const previous = JSON.parse(value) ?? {};
++ const tabs = Object.fromEntries(Object.entries(previous).filter(([, page]) => page && typeof page === "object").map(([botId, page]) => {
++ const tab = { id: `v1-${botId}`, url: page.url ?? "", title: page.title ?? "" };
++ return [botId, { tabs: [tab], active: tab.id }];
++ }));
++ storage.setItem(STORAGE.browsers, JSON.stringify(tabs));
++ for (const earlier of LEGACY_BROWSER_KEYS) storage.removeItem(earlier);
++ return;
++ } catch {
++ }
++ }
++};
++function migrateStorage(storage = globalThis.localStorage) {
++ for (const key of Object.values(STORAGE)) {
++ if (key === STORAGE.browsers) continue;
++ for (const legacy of legacyKeys(key)) {
++ try {
++ const value = storage.getItem(legacy);
++ if (value !== null && storage.getItem(key) === null) storage.setItem(key, value);
++ storage.removeItem(legacy);
++ } catch {
++ }
++ }
++ }
++ try {
++ migrateBrowsers(storage);
++ } catch {
++ }
++ try {
++ const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null");
++ if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) {
++ storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] }));
++ }
++ } catch {
++ }
+ }
-var MEMBER_CSS = `
-.bt-mw-dock-stack{box-sizing:border-box;display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto 6px;pointer-events:none}
-.bt-mw-dock-stack>.bt-event.bt-mw-dock{align-self:stretch;margin:0}
@@ -13493,148 +16825,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_react37 = require("react");
++var import_react38 = 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 @@
+@@ -11289,10 +11289,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_react31.useState)(null);
-+ const [pending, setPending] = (0, import_react37.useState)(null);
++ const [pending, setPending] = (0, import_react38.useState)(null);
const catalog = roster.permissions;
const saved = bot.permission ?? catalog?.defaultPreset;
- (0, import_react31.useEffect)(() => {
-+ (0, import_react37.useEffect)(() => {
++ (0, import_react38.useEffect)(() => {
if (pending === saved) setPending(null);
}, [pending, saved]);
if (!catalog?.options?.length) return null;
-@@ -11302,14 +11302,14 @@
+@@ -11302,14 +11302,14 @@ function PermissionRow({ bot, roster, actions, run }) {
setPending(value);
void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null));
};
- return (0, import_react31.createElement)(
-+ return (0, import_react37.createElement)(
++ return (0, import_react38.createElement)(
"div",
{ className: "bt-drawer-permission" },
- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
- (0, import_react31.createElement)(
-+ (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
++ (0, import_react38.createElement)(
Segmented,
{ label: t("Permissions"), value: current },
- catalog.options.map((option) => (0, import_react31.createElement)("button", {
-+ catalog.options.map((option) => (0, import_react37.createElement)("button", {
++ catalog.options.map((option) => (0, import_react38.createElement)("button", {
key: option.value,
type: "button",
"aria-pressed": option.value === current,
-@@ -11323,42 +11323,42 @@
+@@ -11323,42 +11323,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_react31.useEffect)(() => {
-+ (0, import_react37.useEffect)(() => {
++ (0, import_react38.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_react37.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."));
}
if (state.ids.length === 0) {
- return (0, import_react31.createElement)(
-+ return (0, import_react37.createElement)(
++ return (0, import_react38.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_react37.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
-+ (0, import_react37.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
-+ (0, import_react37.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_react37.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"))
);
}
- return (0, import_react31.createElement)(
-+ return (0, import_react37.createElement)(
++ return (0, import_react38.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_react37.createElement)(
++ return (0, import_react38.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_react37.createElement)(
++ (0, import_react38.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_react37.createElement)(
++ page.excerpt ? (0, import_react38.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_react37.createElement)("span", { className: "bt-bcard-ptitle" }, label),
-+ (0, import_react37.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
-+ ) : (0, import_react37.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react37.createElement)(GlobeIcon), (0, import_react37.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_react31.createElement)("button", {
-+ (0, import_react37.createElement)("button", {
++ (0, import_react38.createElement)("button", {
type: "button",
className: "bt-bcard-x",
"aria-label": t("Close tab"),
-@@ -11367,87 +11367,194 @@
+@@ -11367,87 +11367,194 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow
void actions.browserCloseTab(bot.id, tabId);
}
}, "×"),
- (0, import_react31.createElement)(
-+ (0, import_react37.createElement)(
++ (0, import_react38.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_react37.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
-+ (0, import_react37.createElement)("span", { className: "bt-bcard-title" }, label),
-+ page.url ? (0, import_react37.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_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_react37.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react37.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"))
);
}
function About({ text, title = "About" }) {
- const [open, setOpen] = (0, import_react31.useState)(false);
-+ const [open, setOpen] = (0, import_react37.useState)(false);
++ const [open, setOpen] = (0, import_react38.useState)(false);
const long = text.split("\n").length > 5 || text.length > 240;
- const body = (0, import_react31.useRef)(null);
-+ const body = (0, import_react37.useRef)(null);
++ const body = (0, import_react38.useRef)(null);
useMorph(body, open);
- return (0, import_react31.createElement)(
-+ return (0, import_react37.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_react37.createElement)("div", { className: "bt-section-title" }, t(title)),
-+ (0, import_react37.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text),
-+ long ? (0, import_react37.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null
- );
- }
++ return (0, import_react38.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
++ );
++}
+function SharedRoomApprovals({ room: room2 }) {
-+ const [items, setItems] = (0, import_react37.useState)(room2.pendingApprovals ?? []);
-+ const [busy, setBusy] = (0, import_react37.useState)("");
++ const [items, setItems] = (0, import_react38.useState)(room2.pendingApprovals ?? []);
++ const [busy, setBusy] = (0, import_react38.useState)("");
+ const face = room2.origin === "face-to-face";
-+ (0, import_react37.useEffect)(() => {
++ (0, import_react38.useEffect)(() => {
+ const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length;
+ if (haveItems) setItems(room2.pendingApprovals);
+ if (room2.viewerRole !== "invited-owner" && !face) return void 0;
@@ -13660,36 +16989,39 @@ diff --git a/client.js b/client.js
+ setBusy("");
+ }
+ };
-+ return (0, import_react37.createElement)(
++ return (0, import_react38.createElement)(
+ "div",
+ { key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" },
-+ (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react37.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))),
-+ items.map((item) => (0, import_react37.createElement)(
++ (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)(
+ "div",
+ { key: item.id, className: "bt-set-row" },
-+ (0, import_react37.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")),
-+ (0, import_react37.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")),
-+ (0, import_react37.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject"))
++ (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"))
+ ))
+ );
+}
+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_react37.createElement)(
-+ "div",
-+ null,
-+ (0, import_react37.createElement)(FaceLead, { room: room2, roster, pick: pick2 }),
-+ (0, import_react37.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })),
-+ (0, import_react37.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react37.createElement)(FacePersonRow, {
++ return (0, import_react38.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, {
+ key: person.key,
+ person,
+ className: "bt-member",
+ onOpenBot: (id) => actions.openSession(id),
+ lead: room2.face?.lead?.handle
+ })))
-+ );
-+}
+ );
+ }
function GroupOverview({ room: room2, roster, actions, usage, useWork, openSettings }) {
+ const live = room2.presentedParticipants ?? room2.participants ?? [];
+ const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0);
@@ -13706,31 +17038,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_react37.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
-+ invited && room2.sharedFrom && !face ? (0, import_react37.createElement)(
- "div",
-+ { key: "from", className: "bt-shared-from" },
-+ (0, import_react37.createElement)("div", { className: "bt-section-title" }, title),
-+ (0, import_react37.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_react37.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null,
-+ (0, import_react37.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot"))
-+ ) : face ? null : room2.notice ? (0, import_react37.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react37.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
-+ face ? null : (0, import_react37.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
-+ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react37.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react37.createElement)(
++ (0, import_react38.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
++ invited && room2.sharedFrom && !face ? (0, import_react38.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)(
+ "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_react37.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })),
-+ (0, import_react37.createElement)("div", { className: "bt-members", role: "list" }, [
-+ ...humans.map((person) => (0, import_react37.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)(
"div",
- { key: member.id, role: "listitem" },
- (0, import_react31.createElement)(
+ { key: person.id, role: "listitem" },
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react31.createElement)(
@@ -13738,70 +17070,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_react37.createElement)("span", { className: "bt-member-name" }, (0, import_react37.createElement)("span", null, person.displayName || person.id)),
-+ (0, import_react37.createElement)("span", { className: "bt-chip" }, t("Human")),
-+ person.role === "admin" ? (0, import_react37.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
++ (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
+ )
+ )),
-+ ...remotes.map((person) => (0, import_react37.createElement)(
++ ...remotes.map((person) => (0, import_react38.createElement)(
+ "div",
+ { key: person.handle || person.id, role: "listitem" },
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)(
+ "div",
+ { className: "bt-member" },
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)(
+ "span",
+ { className: "bt-member-name" },
-+ (0, import_react37.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
-+ (0, import_react37.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person))
++ (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))
),
- 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_react37.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
++ face ? null : (0, import_react38.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
+ )
+ )),
+ ...members.map((member) => {
+ const entry = entries2.find((item) => item.id === member.id);
-+ return (0, import_react37.createElement)(
++ return (0, import_react38.createElement)(
+ "div",
+ { key: member.id, role: "listitem" },
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)(
+ "div",
+ { className: "bt-member" },
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)(
+ "button",
+ { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) },
-+ (0, import_react37.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
-+ (0, import_react37.createElement)("span", null, member.name)
++ (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)
+ ),
-+ room2.admin === member.id ? (0, import_react37.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
++ room2.admin === member.id ? (0, import_react38.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
+ ),
-+ entry ? (0, import_react37.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
++ entry ? (0, import_react38.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
+ );
+ })
+ ])
),
- (0, import_react31.createElement)(
-+ face ? null : (0, import_react37.createElement)(
++ face ? null : (0, import_react38.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_react37.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
-+ (0, import_react37.createElement)("div", { className: "bt-kv" }, (0, import_react37.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")))
),
- usage ? (0, import_react31.createElement)(
-+ face || !usage ? null : (0, import_react37.createElement)(
++ face || !usage ? null : (0, import_react38.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_react37.createElement)("div", { className: "bt-section-title" }, t("Usage")),
-+ (0, import_react37.createElement)("div", { className: "bt-kv" }, (0, import_react37.createElement)("span", null, room2.name), (0, import_react37.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, room2.name), (0, import_react38.createElement)("span", null, usage))
+ )
];
}
@@ -13810,181 +17142,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_react37.useState)("details");
++ const [tab, setTab] = (0, import_react38.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_react37.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
-+ const [editing, setEditing] = (0, import_react37.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
-+ const [draftName, setDraftName] = (0, import_react37.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
-+ const [copied, setCopied] = (0, import_react37.useState)(false);
-+ const [error, setError] = (0, import_react37.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 glide = useGlide(tab);
- const last = (0, import_react31.useRef)({ view, tab, move: void 0 });
-+ const last = (0, import_react37.useRef)({ view, tab, move: void 0 });
++ const last = (0, import_react38.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 @@
+@@ -11472,36 +11472,43 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
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_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
++ const errorLine = error ? (0, import_react38.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_react37.createElement)(
++ (0, import_react38.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_react37.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react37.createElement)(ChevronLeftIcon)),
-+ (0, import_react37.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
-+ hasStrip ? null : (0, import_react37.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react37.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_react31.createElement)(
-+ (0, import_react37.createElement)(
++ (0, import_react38.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_react37.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react37.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 })
)
];
}
- const top = (0, import_react31.createElement)(
-+ const top = (0, import_react37.createElement)(
++ const top = (0, import_react38.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_react37.createElement)("button", {
++ bot || room2 ? (0, import_react38.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_react37.createElement)(GearIcon)) : null,
-+ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react37.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_react37.createElement)(GlobeIcon)) : null,
-+ bot || room2 ? (0, import_react37.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_react37.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react37.createElement)(ShareIcon)) : null,
-+ hasStrip ? null : (0, import_react37.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react37.createElement)(CloseIcon))
++ }, (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))
);
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_react37.createElement)(import_react37.Fragment, { key: "top" }, top),
-+ (0, import_react37.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react37.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.")))
];
}
const current = bot?.name ?? room2.name;
-@@ -11515,12 +11515,12 @@
+@@ -11515,12 +11515,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_react31.createElement)(import_react31.Fragment, { key: "top" }, top),
- (0, import_react31.createElement)(
-+ (0, import_react37.createElement)(import_react37.Fragment, { key: "top" }, top),
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)(import_react38.Fragment, { key: "top" }, top),
++ (0, import_react38.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_react37.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react37.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
-+ bot?.remote ? (0, import_react37.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react37.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react37.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", {
className: "bt-drawer-name-input",
autoFocus: true,
value: draftName,
-@@ -11535,87 +11535,86 @@
+@@ -11535,87 +11535,86 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
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_react37.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: () => {
setDraftName(room2 && !room2.named ? "" : current);
setEditing(true);
} }, current),
- subtitle ? (0, import_react31.createElement)("span", { className: "bt-drawer-role" }, subtitle) : null
-+ subtitle ? (0, import_react37.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react37.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : 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
),
- (0, import_react31.createElement)(
-+ bot?.remote ? null : (0, import_react37.createElement)(
++ bot?.remote ? null : (0, import_react38.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_react37.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ tabs.map((key) => (0, import_react37.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_react31.createElement)(
-+ (0, import_react37.createElement)(
++ (0, import_react38.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_react37.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
++ room2 && tab === "details" ? (0, import_react38.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
+ bot && bot.remote && tab === "details" ? [
-+ bot.remote.handle ? (0, import_react37.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null,
-+ (0, import_react37.createElement)(FlyBotDetails, { key: "social", bot, roster, actions })
++ 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 })
+ ] : null,
+ bot && !bot.remote && tab === "details" ? [
-+ (0, import_react37.createElement)(
++ (0, import_react38.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_react37.createElement)("div", { className: "bt-section-title" }, t("Model")),
-+ (0, import_react37.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
-+ modelOf(bot, roster) ? null : (0, import_react37.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_react31.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
- (0, import_react31.createElement)(
-+ (0, import_react37.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
++ (0, import_react38.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_react37.createElement)("div", { className: "bt-section-title" }, t("Working folder")),
-+ (0, import_react37.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 })) })
),
- 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_react37.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react37.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
-+ roster.memory === false ? null : (0, import_react37.createElement)(MemoryRow, { key: "memory", bot, actions }),
-+ usage ? (0, import_react37.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)(
"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_react37.createElement)("div", { className: "bt-section-title" }, t("Usage")),
-+ (0, import_react37.createElement)("div", { className: "bt-kv" }, (0, import_react37.createElement)("span", null, bot.name), (0, import_react37.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))
) : null,
- (0, import_react31.createElement)(ScheduleSummary, { key: "routines", bot, actions })
-+ (0, import_react37.createElement)(ScheduleSummary, { key: "routines", bot, actions })
++ (0, import_react38.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_react37.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
)
];
}
// src/client/browser-pane.js
-var import_react32 = require("react");
-+var import_react38 = require("react");
++var import_react39 = require("react");
var import_react_dom8 = require("react-dom");
var PANEL_LINGER_MS = 300;
var WIDTH_KEY = STORAGE.browserWidth;
@@ -14001,11 +17333,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_react38.createElement)(
++ return (0, import_react39.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_react38.createElement)("path", { key: index, d }))
++ paths.map((d, index) => (0, import_react39.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"]);
@@ -14028,13 +17360,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_react38.useState)(null);
-+ if (!src || failed === src) return (0, import_react38.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react38.createElement)(GlobeIcon));
-+ return (0, import_react38.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) });
}
var savedWidth = () => {
try {
-@@ -11625,8 +11625,8 @@
+@@ -11625,8 +11625,8 @@ var savedWidth = () => {
return null;
}
};
@@ -14044,7 +17376,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 @@
+@@ -11674,11 +11674,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);
@@ -14053,82 +17385,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_react38.useRef)(null);
-+ const ind = (0, import_react38.useRef)(null);
-+ const spot = (0, import_react38.useRef)(null);
-+ const under = (0, import_react38.useRef)(null);
-+ (0, import_react38.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 box = wrap.current;
const capsule = ind.current;
if (!box || !capsule) return void 0;
-@@ -11707,7 +11707,7 @@
+@@ -11707,7 +11707,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
trail.cancel();
};
});
- (0, import_react32.useEffect)(() => {
-+ (0, import_react38.useEffect)(() => {
++ (0, import_react39.useEffect)(() => {
const box = wrap.current;
if (!box) return void 0;
const observer = new ResizeObserver(() => {
-@@ -11721,13 +11721,13 @@
+@@ -11721,13 +11721,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
observer.observe(box);
return () => observer.disconnect();
}, []);
- return (0, import_react32.createElement)(
-+ return (0, import_react38.createElement)(
++ return (0, import_react39.createElement)(
"div",
{ className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") },
- (0, import_react32.createElement)(
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
"div",
{ className: "bt-ptabs-in", ref: wrap },
- (0, import_react32.createElement)(
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
"button",
{
type: "button",
-@@ -11737,13 +11737,13 @@
+@@ -11737,13 +11737,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
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_react38.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
-+ view === "details" ? (0, import_react38.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
),
ids.map((tabId) => {
const page = pages[tabId] ?? EMPTY_PAGE;
const on = view === "browser" && tabId === active;
- return (0, import_react32.createElement)(
-+ return (0, import_react38.createElement)(
++ return (0, import_react39.createElement)(
"button",
{
key: tabId,
-@@ -11764,9 +11764,9 @@
+@@ -11764,9 +11764,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
}
}
},
- (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_react38.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
-+ (0, import_react38.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
-+ (0, import_react38.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", {
className: "bt-ptab-x",
role: "button",
"aria-label": t("Close tab"),
-@@ -11778,20 +11778,20 @@
+@@ -11778,20 +11778,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
}, "×")
);
}),
- (0, import_react32.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true })
-+ (0, import_react38.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 })
),
// 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_react38.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react38.createElement)(PlusTabIcon)),
-+ (0, import_react38.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_react38.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))
);
}
function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) {
@@ -14136,42 +17468,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_react38.useRef)(null);
-+ const input = (0, import_react38.useRef)(null);
-+ const [draft, setDraft] = (0, import_react38.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 showGuest = landed && !leaving && Boolean(page.url) && !page.error;
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react38.useLayoutEffect)(() => {
++ (0, import_react39.useLayoutEffect)(() => {
const element = frame.current;
if (!element || leaving || !tabId) {
void actions.browserShow(null);
-@@ -11817,7 +11817,7 @@
+@@ -11817,7 +11817,7 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
window.removeEventListener("resize", schedule);
};
}, [tabId, showGuest, leaving]);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react38.useEffect)(() => {
++ (0, import_react39.useEffect)(() => {
setDraft(null);
if (landed && !page.url) input.current?.focus();
}, [tabId, landed]);
-@@ -11833,27 +11833,27 @@
+@@ -11833,27 +11833,27 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
};
const editing = draft !== null;
const secure = /^https:/i.test(page.url);
- return (0, import_react32.createElement)(
-+ return (0, import_react38.createElement)(
++ return (0, import_react39.createElement)(
"div",
{ className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) },
- (0, import_react32.createElement)(
-+ (0, import_react38.createElement)(
++ (0, import_react39.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_react38.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_react38.createElement)(BackIcon)),
-+ (0, import_react38.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_react38.createElement)(ForwardIcon)),
-+ (0, import_react38.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", {
type: "button",
className: "bt-icon-btn",
disabled: !page.url,
@@ -14180,54 +17512,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_react38.createElement)(page.loading ? StopIcon : ReloadIcon)),
-+ (0, import_react38.createElement)(
++ }, (0, import_react39.createElement)(page.loading ? StopIcon : ReloadIcon)),
++ (0, import_react39.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_react38.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react38.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
-+ (0, import_react38.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", {
ref: input,
type: "text",
spellCheck: false,
-@@ -11877,25 +11877,25 @@
+@@ -11877,25 +11877,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
}
})
),
- page.loading ? (0, import_react32.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
-+ page.loading ? (0, import_react38.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
),
- (0, import_react32.createElement)(
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
"div",
{ ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 },
- page.error ? (0, import_react32.createElement)(
-+ page.error ? (0, import_react38.createElement)(
++ page.error ? (0, import_react39.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_react38.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
-+ (0, import_react38.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
-+ page.url ? (0, import_react38.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: () => {
void actions.browserNavigate(tabId, page.url);
} }, t("Try again")) : null
- ) : !page.url ? (0, import_react32.createElement)(
-+ ) : !page.url ? (0, import_react38.createElement)(
++ ) : !page.url ? (0, import_react39.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_react38.createElement)(BotMark, { bot, size: 48, 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-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 }))
) : null
)
);
-@@ -11907,41 +11907,41 @@
+@@ -11907,41 +11907,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);
@@ -14239,36 +17571,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_react38.useRef)(null);
-+ const fly = (0, import_react38.useRef)(null);
-+ const [landed, setLanded] = (0, import_react38.useState)(false);
-+ const [settling, setSettling] = (0, import_react38.useState)(false);
-+ const [width, setWidth] = (0, import_react38.useState)(savedWidth);
-+ const [viewport, setViewport] = (0, import_react38.useState)(() => window.innerWidth);
-+ const [dragging, setDragging] = (0, import_react38.useState)(false);
-+ const [resizing, setResizing] = (0, import_react38.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 instant = dragging || resizing;
- const [reading, setReading] = (0, import_react32.useState)(false);
-+ const [reading, setReading] = (0, import_react38.useState)(false);
++ const [reading, setReading] = (0, import_react39.useState)(false);
const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport));
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react38.useLayoutEffect)(() => {
++ (0, import_react39.useLayoutEffect)(() => {
if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH);
}, [view, width]);
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react38.useLayoutEffect)(() => {
++ (0, import_react39.useLayoutEffect)(() => {
chatRoom.hold(instant);
}, [instant]);
- const [last, setLast] = (0, import_react32.useState)({ view, width: panelWidth });
-+ const [last, setLast] = (0, import_react38.useState)({ view, width: panelWidth });
++ const [last, setLast] = (0, import_react39.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_react38.useRef)(view);
-+ (0, import_react38.useLayoutEffect)(() => {
++ const shownView = (0, import_react39.useRef)(view);
++ (0, import_react39.useLayoutEffect)(() => {
const switched = settling && shownView.current !== view;
shownView.current = view;
if (switched) chatRoom.hold(true);
@@ -14276,96 +17608,96 @@ diff --git a/client.js b/client.js
if (switched) chatRoom.hold(false);
}, [view, panelWidth, leaving]);
- (0, import_react32.useEffect)(() => () => {
-+ (0, import_react38.useEffect)(() => () => {
++ (0, import_react39.useEffect)(() => () => {
chatRoom.set(null);
chatRoom.hold(false);
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react38.useEffect)(() => {
++ (0, import_react39.useEffect)(() => {
let timer;
const onResize = () => {
setResizing(true);
-@@ -11955,28 +11955,28 @@
+@@ -11955,28 +11955,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
window.removeEventListener("resize", onResize);
};
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react38.useEffect)(() => {
++ (0, import_react39.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_react38.useEffect)(() => {
++ (0, import_react39.useEffect)(() => {
const timer = setTimeout(() => setLanded(true), 360);
return () => clearTimeout(timer);
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react38.useEffect)(() => {
++ (0, import_react39.useEffect)(() => {
if (canBrowse) actions.browserEnsure(bot.id);
}, [bot?.id, canBrowse]);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react38.useEffect)(() => {
++ (0, import_react39.useEffect)(() => {
if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details");
}, [view, active, tabState.ids.length]);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react38.useEffect)(() => {
++ (0, import_react39.useEffect)(() => {
if (view !== "browser" || leaving) void actions.browserShow?.(null);
}, [view, leaving]);
- (0, import_react32.useEffect)(() => () => {
-+ (0, import_react38.useEffect)(() => () => {
++ (0, import_react39.useEffect)(() => () => {
void actions.browserShow?.(null);
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react38.useEffect)(() => {
++ (0, import_react39.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 @@
+@@ -12013,7 +12013,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
return actions[name](...args);
};
}
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react38.useLayoutEffect)(() => {
++ (0, import_react39.useLayoutEffect)(() => {
const move = fly.current;
if (!move) return;
fly.current = null;
-@@ -12096,7 +12096,7 @@
+@@ -12096,7 +12096,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_react32.createElement)(
-+ return (0, import_react38.createElement)(
++ return (0, import_react39.createElement)(
"aside",
{
ref: root,
-@@ -12113,15 +12113,15 @@
+@@ -12113,15 +12113,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
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_react38.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 }),
// 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_react38.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
-+ hasStrip ? (0, import_react38.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
-+ view === "browser" && bot ? (0, import_react38.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react38.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)(
"div",
{ key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" },
- (0, import_react32.createElement)(DetailsDrawer, {
-+ (0, import_react38.createElement)(DetailsDrawer, {
++ (0, import_react39.createElement)(DetailsDrawer, {
sessionId: pane.id,
startRenaming: pane.renaming,
startTab: pane.tab,
-@@ -12217,8 +12217,78 @@
+@@ -12217,8 +12217,78 @@ var BROWSER_CSS = `
@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_react39 = require("react");
++var import_react40 = 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}
@@ -14389,8 +17721,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_react39.useRef)(null);
-+ (0, import_react39.useEffect)(() => {
++ const dialog = (0, import_react40.useRef)(null);
++ (0, import_react40.useEffect)(() => {
+ const before = document.activeElement;
+ dialog.current?.focus?.({ preventScroll: true });
+ return () => {
@@ -14405,30 +17737,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_react39.createElement)(
++ return (0, import_react40.createElement)(
+ "div",
+ { className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" },
-+ (0, import_react39.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
-+ (0, import_react39.createElement)(
++ (0, import_react40.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
++ (0, import_react40.createElement)(
+ "div",
+ { className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown },
-+ (0, import_react39.createElement)(
++ (0, import_react40.createElement)(
+ "div",
+ { className: "bt-remote-sheet-head" },
-+ (0, import_react39.createElement)(BotMark, { bot, size: 40, live: true }),
-+ (0, import_react39.createElement)(
++ (0, import_react40.createElement)(BotMark, { bot, size: 40, live: true }),
++ (0, import_react40.createElement)(
+ "div",
+ { className: "bt-remote-sheet-who" },
-+ (0, import_react39.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name),
-+ (0, import_react39.createElement)("span", { className: "bt-remote-sheet-sub" }, 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_react39.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react39.createElement)(CloseIcon))
++ (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_react39.createElement)(
++ (0, import_react40.createElement)(
+ "div",
+ { className: "bt-remote-sheet-body" },
-+ bot.remote.handle ? (0, import_react39.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null,
-+ (0, import_react39.createElement)(FlyBotDetails, { bot, roster, actions })
++ 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 })
+ )
+ )
+ );
@@ -14436,18 +17768,18 @@ diff --git a/client.js b/client.js
+
// src/client/overlays.js
-var import_react33 = require("react");
-+var import_react40 = require("react");
++var import_react41 = require("react");
var import_react_dom9 = require("react-dom");
var DIALOG_EXIT_MS = 160;
var MENU_EXIT_MS3 = 120;
-@@ -12227,15 +12227,15 @@
+@@ -12227,15 +12227,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_react33.useRef)(current);
- (0, import_react33.useEffect)(() => {
-+ const previous = (0, import_react40.useRef)(current);
-+ (0, import_react40.useEffect)(() => {
++ const previous = (0, import_react41.useRef)(current);
++ (0, import_react41.useEffect)(() => {
if (previous.current === current) return;
previous.current = current;
if (current) actions.followPanel?.(current);
@@ -14455,12 +17787,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_react40.useState)(open);
-+ (0, import_react40.useEffect)(() => {
++ const [kept, setKept] = (0, import_react41.useState)(open);
++ (0, import_react41.useEffect)(() => {
if (open) {
setKept(open);
return void 0;
-@@ -12245,19 +12245,24 @@
+@@ -12245,19 +12245,26 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity
}, [ui2.details, ui2.view, ui2.renaming, ui2.tab]);
const pane = open ?? kept;
const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS);
@@ -14471,8 +17803,9 @@ diff --git a/client.js b/client.js
- return (0, import_react33.createElement)(
- import_react33.Fragment,
+ const [inbox, inboxLeaving] = useLinger(ui2.inbox ?? null, DIALOG_EXIT_MS);
-+ return (0, import_react40.createElement)(
-+ import_react40.Fragment,
++ const [board, boardLeaving] = useLinger(ui2.board ?? null, DIALOG_EXIT_MS);
++ return (0, import_react41.createElement)(
++ import_react41.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,
@@ -14482,20 +17815,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_react40.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_react40.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null,
-+ ui2.palette ? (0, import_react_dom9.createPortal)((0, import_react40.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_react40.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null,
-+ ui2.voice ? (0, import_react_dom9.createPortal)((0, import_react40.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
-+ menu ? (0, import_react_dom9.createPortal)((0, import_react40.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_react40.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
-+ remoteBotSettings ? (0, import_react_dom9.createPortal)((0, import_react40.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_react40.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
-+ inbox ? (0, import_react_dom9.createPortal)((0, import_react40.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), 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
);
}
-@@ -12304,7 +12304,7 @@
+@@ -12304,7 +12304,7 @@ function readPage(limits) {
`;
};
const expand = (text2) => text2.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)]));
@@ -14504,7 +17838,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 @@
+@@ -12372,7 +12372,7 @@ ${fence}`;
const marker = ordered ? `${number++}. ` : "- ";
if (body2 === "") continue;
const pad2 = " ".repeat(marker.length);
@@ -14513,7 +17847,7 @@ diff --git a/client.js b/client.js
}
return keep(lines.join("\n"));
};
-@@ -12398,11 +12398,11 @@
+@@ -12398,11 +12398,11 @@ ${children(cell, ctx)}
let width = 0;
for (const row of rows.slice(0, limits.tableRows + 1)) {
if (over()) break;
@@ -14527,7 +17861,7 @@ diff --git a/client.js b/client.js
carry[column].left -= 1;
column += 1;
}
-@@ -12414,14 +12414,14 @@
+@@ -12414,14 +12414,14 @@ ${children(cell, ctx)}
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) {
@@ -14545,7 +17879,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 @@
+@@ -12552,7 +12552,7 @@ ${"#".repeat(Math.min(6, Number(level) || 2))} ${title}${mark}
}
if (tag === "blockquote") {
const quoted = block(body2);
@@ -14554,7 +17888,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,63 @@
+@@ -13052,11 +13052,63 @@ function serveBrowserReads({ browsers, post, clientId = globalThis.crypto.random
};
}
@@ -14619,7 +17953,7 @@ diff --git a/client.js b/client.js
function disposeAll(disposers, label) {
for (const dispose of [...disposers].reverse()) {
try {
-@@ -13075,8 +13075,8 @@
+@@ -13075,8 +13075,8 @@ function surfaceGroups(install) {
};
return {
reconcile(wanted) {
@@ -14630,7 +17964,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 @@
+@@ -13086,7 +13086,7 @@ function surfaceGroups(install) {
}
},
dispose() {
@@ -14639,16 +17973,16 @@ diff --git a/client.js b/client.js
}
};
}
-@@ -13105,7 +13105,7 @@
+@@ -13105,7 +13105,7 @@ function writeMode(mode, storage = globalThis.localStorage) {
}
// src/client/user-message.js
-var import_react34 = require("react");
-+var import_react41 = require("react");
++var import_react42 = require("react");
var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
var canDrawUserMessages = () => typeof ui.projectUserText === "function";
function contentParts(content) {
-@@ -13119,11 +13119,59 @@
+@@ -13119,10 +13119,58 @@ function contentParts(content) {
}
return { text: texts2.join("\n"), attachments, rest };
}
@@ -14679,7 +18013,7 @@ diff --git a/client.js b/client.js
+ const humans = (room2?.participants ?? []).filter((p) => p.kind === "human" && p.leftSeq === void 0).map((p) => ({ id: p.id, name: p.displayName || p.id, kind: "human", role: p.role }));
+ const remotes = (room2?.participants ?? []).filter((p) => p.remote && p.leftSeq === void 0).map((p) => ({ id: p.handle || p.id, name: remoteBotTitle(p), kind: "bot", remote: p, mentionLabel: remoteBotTitle(p) }));
+ return bots.concat(humans, remotes, remoteMentionableBots(roster, roster.bots));
- }
++}
+function faceMentionables(room2, roster, fromFriend) {
+ const humans = (room2.participants ?? []).filter((p) => p.kind === "human" && p.leftSeq === void 0).map((p) => ({ id: p.id, name: p.displayName || p.id, kind: "human", role: p.role }));
+ const all = faceSeats(room2, roster, { left: true });
@@ -14707,11 +18041,10 @@ diff --git a/client.js b/client.js
+ ownerName: remote?.ownerName,
+ mine: roster?.self?.memberId === authorId
+ };
-+}
+ }
function userTextParts(text, bots, referenceLabels = []) {
const parts = [];
- let cursor = 0;
-@@ -13132,43 +13132,44 @@
+@@ -13132,43 +13132,44 @@ function userTextParts(text, bots, referenceLabels = []) {
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;
@@ -14727,37 +18060,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_react41.createElement)(BotMark, { bot: part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`];
++ const chip = (part) => [(0, import_react42.createElement)(BotMark, { bot: part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`];
+ return userTextParts(text, bots, referenceLabels).map((part, index) => {
-+ if (!part.bot) return (0, import_react41.createElement)(import_react41.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
++ if (!part.bot) return (0, import_react42.createElement)(import_react42.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
+ const title = `@${part.bot.name}`;
-+ return typeof part.bot.id === "string" ? (0, import_react41.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react41.createElement)("span", { key: index, className: "bt-umention", title }, chip(part));
++ 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));
+ });
}
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_react41.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => {
++ return (0, import_react42.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_react41.createElement)(import_react41.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;
}
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_react41.createElement)(
++ return (0, import_react42.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_react41.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null,
-+ (0, import_react41.createElement)("span", { className: "bt-ufile-text" }, (0, import_react41.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react41.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)
);
}));
}
function CopyButton({ text }) {
- const [copied, setCopied] = (0, import_react34.useState)(false);
-+ const [copied, setCopied] = (0, import_react41.useState)(false);
++ const [copied, setCopied] = (0, import_react42.useState)(false);
const copy2 = () => {
void navigator.clipboard?.writeText(text).then(() => {
setCopied(true);
@@ -14765,11 +18098,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_react41.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_react41.createElement)(CheckIcon) : (0, import_react41.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));
}
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 @@
+@@ -13178,28 +13178,52 @@ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageIma
const referenceLabels = data.referenceLabels ?? [];
const references = openFile && openSkill ? { openFile, openSkill } : void 0;
const steering = node.kind === "steering";
@@ -14781,7 +18114,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_react41.createElement)(
++ return (0, import_react42.createElement)(
"div",
- { className: "bt-umsg", "data-steering": steering || void 0 },
- (0, import_react34.createElement)(
@@ -14793,28 +18126,28 @@ diff --git a/client.js b/client.js
+ "data-author-kind": author?.authorKind,
+ "data-author-name": author?.name
+ },
-+ person ? (0, import_react41.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react41.createElement)(PersonMark, { name: author.name })) : null,
-+ faceMine ? (0, import_react41.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react41.createElement)(PersonMark, { name: myName || void 0 })) : null,
-+ (0, import_react41.createElement)(
++ 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)(
"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_react41.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react41.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react41.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null,
-+ attachments.length > 0 ? (0, import_react41.createElement)(Attachments, { attachments, renderMessageImages }) : null,
-+ text !== "" || rest.length > 0 ? (0, import_react41.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)(
"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_react41.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_react41.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)
) : 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_react41.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
),
- text === "" ? null : (0, import_react34.createElement)("div", { className: "bt-utools" }, (0, import_react34.createElement)(CopyButton, { text }))
-+ text === "" ? null : (0, import_react41.createElement)("div", { className: "bt-utools" }, (0, import_react41.createElement)(CopyButton, { text }))
++ text === "" ? null : (0, import_react42.createElement)("div", { className: "bt-utools" }, (0, import_react42.createElement)(CopyButton, { text }))
);
}
var USER_MESSAGE_CSS = `
@@ -14831,7 +18164,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 @@
+@@ -13216,42 +13216,114 @@ var USER_MESSAGE_CSS = `
`;
// src/client/approval-notices.js
@@ -14913,10 +18246,10 @@ diff --git a/client.js b/client.js
+ off();
+ timers.clearTimeout?.(timer);
+ };
- }
-
++}
++
+// src/client/stats-dock.js
-+var import_react42 = require("react");
++var import_react43 = require("react");
+var STATS_DOCK = "conversation.composer.dock";
+var STATS_ID = "stats";
+var STATS_IDS = [STATS_ID, "activity"];
@@ -14932,15 +18265,15 @@ 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_react42.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]);
++ const t2 = (0, import_react43.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_react42.createElement)(entry.component, { ...props, t: t2 });
++ return (0, import_react43.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 inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout"];
function apply(ctx) {
@@ -14959,7 +18292,7 @@ diff --git a/client.js b/client.js
migrateStorage();
const UNREAD_KEY = STORAGE.unread;
let savedUnread = {};
-@@ -13274,7 +13274,13 @@
+@@ -13274,7 +13274,13 @@ function apply(ctx) {
} catch {
seenAt = {};
}
@@ -14974,7 +18307,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 @@
+@@ -13344,13 +13344,25 @@ function apply(ctx) {
}
return { ...state, unread };
});
@@ -15001,7 +18334,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 @@
+@@ -13368,10 +13368,15 @@ function apply(ctx) {
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;
@@ -15017,7 +18350,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 @@
+@@ -13488,20 +13488,12 @@ function apply(ctx) {
const style = installStyles("", "mentions");
let last = "";
const render = () => {
@@ -15040,7 +18373,7 @@ diff --git a/client.js b/client.js
return () => {
offRoster();
offRegistry();
-@@ -13520,7 +13520,7 @@
+@@ -13520,7 +13520,7 @@ function apply(ctx) {
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;
@@ -15049,7 +18382,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 @@
+@@ -13536,7 +13536,7 @@ function apply(ctx) {
};
render();
const offRoster = roster.subscribe(render);
@@ -15058,7 +18391,7 @@ diff --git a/client.js b/client.js
return () => {
offRoster();
offRegistry();
-@@ -13588,6 +13588,7 @@
+@@ -13588,6 +13588,7 @@ function apply(ctx) {
// or the click appears to do nothing.
isAgentSession: (id) => isAgentSessionId(id),
openSession: (id) => {
@@ -15066,7 +18399,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 @@
+@@ -13653,12 +13653,12 @@ function apply(ctx) {
},
// `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.
@@ -15082,7 +18415,7 @@ diff --git a/client.js b/client.js
expected = target;
ctx.uiWorkspace.openSession(target);
},
-@@ -13708,10 +13708,15 @@
+@@ -13708,10 +13708,15 @@ function apply(ctx) {
},
// `id` opens one Bot (page 'bots') or one group chat (page 'groups').
openSettings: (page = "general", id = null) => {
@@ -15100,30 +18433,36 @@ diff --git a/client.js b/client.js
},
openMemory: (botId) => {
ui2.set((state) => ({ ...state, memory: botId, menu: null, palette: null }));
-@@ -13719,6 +13719,13 @@
+@@ -13719,6 +13719,19 @@ function apply(ctx) {
closeMemory: () => {
ui2.set((state) => state.memory ? { ...state, memory: null } : state);
},
+ // The Agent inbox (#349) opens over the conversation; Open on a card closes it first.
+ openInbox: () => {
-+ ui2.set((state) => ({ ...state, inbox: true, menu: null, palette: null, newChat: null }));
++ ui2.set((state) => ({ ...state, inbox: true, board: null, menu: null, palette: null, newChat: null }));
+ },
+ closeInbox: () => {
+ ui2.set((state) => state.inbox ? { ...state, inbox: null } : state);
++ },
++ openBoard: () => {
++ ui2.set((state) => ({ ...state, board: true, inbox: null, menu: null, palette: null, newChat: null }));
++ },
++ closeBoard: () => {
++ ui2.set((state) => state.board ? { ...state, board: null } : state);
+ },
openConnectors: () => {
ui2.set((state) => ({ ...state, settings: { page: "connectors", id: null }, menu: null, palette: null, newChat: null }));
},
-@@ -13728,7 +13728,7 @@
+@@ -13728,7 +13728,7 @@ function apply(ctx) {
if (mode !== "bot" && mode !== "agent") return;
if (surface.getSnapshot().mode === mode) return;
if (mode === "agent") {
- ui2.set((state) => ({ ...state, details: null, view: "details", settings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, toast: null }));
-+ ui2.set((state) => ({ ...state, details: null, view: "details", settings: null, remoteBotSettings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, inbox: null, toast: null }));
++ ui2.set((state) => ({ ...state, details: null, view: "details", settings: null, remoteBotSettings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, inbox: null, board: null, toast: null }));
}
writeMode(mode);
surface.set((state) => ({ ...state, mode }));
-@@ -13757,6 +13757,12 @@
+@@ -13757,6 +13757,12 @@ function apply(ctx) {
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() } } }));
},
@@ -15136,7 +18475,7 @@ diff --git a/client.js b/client.js
endVisit: (id) => {
leaveVisits([id]);
ui2.set((state) => {
-@@ -13812,6 +13812,7 @@
+@@ -13812,6 +13812,7 @@ function apply(ctx) {
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 })),
@@ -15144,7 +18483,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,24 @@
+@@ -13819,15 +13819,26 @@ function apply(ctx) {
secretCancel: (requestId) => after(call("secret-cancel", { requestId })),
secretUpdate: (name, scope) => after(call("secret-update", { name, scope })),
secretDelete: (name) => after(call("secret-delete", { name })),
@@ -15160,7 +18499,9 @@ diff --git a/client.js b/client.js
approveCapability: (name) => after(call("capability-approve", { name })),
+ setBotConnector: (botId, connector, enabled) => after(call("bot-connectors", { botId, connector, enabled })),
setBotCapability: (botId, name, attached) => after(call("bot-capability", { botId, name, attached })),
-+ tasks: () => call("tasks"),
++ tasks: (payload) => call("tasks", payload ?? {}),
++ taskUpdate: (payload) => call("task-update", payload),
++ taskCreate: (payload) => call("task-create", payload),
schedules: () => call("schedules"),
scheduleCreate: (task) => call("schedule-create", task),
+ scheduleUpdate: (task) => call("schedule-update", task),
@@ -15170,7 +18511,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 @@
+@@ -13846,6 +13846,8 @@ function apply(ctx) {
noteClientError(where, error);
ui2.set((state) => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } }));
},
@@ -15179,7 +18520,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 @@
+@@ -13890,6 +13890,7 @@ function apply(ctx) {
actions.closePanel = () => {
ui2.set((state) => state.details ? { ...state, details: null, view: "details", renaming: false, tab: null } : state);
};
@@ -15187,7 +18528,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 @@
+@@ -13964,7 +13964,13 @@ function apply(ctx) {
});
const browserPages = browsers?.pages ?? createSource({});
const browserTabs = browsers?.tabs ?? createSource({});
@@ -15202,7 +18543,7 @@ diff --git a/client.js b/client.js
ctx.effect(() => noticeApprovals({
work,
roster,
-@@ -13989,7 +13989,17 @@
+@@ -13989,7 +13989,17 @@ function apply(ctx) {
} };
};
const shadow = (name, options, component) => ctx.slots.inject(name, () => ctx.slots.register({ name, ...options }, component));
@@ -15220,7 +18561,7 @@ diff --git a/client.js b/client.js
const installAtMenu = () => {
const fiber = ctx.inject(["inputTriggers"], (scope) => {
scope.effect(() => {
-@@ -14001,7 +14001,12 @@
+@@ -14001,7 +14001,12 @@ function apply(ctx) {
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 };
@@ -15233,7 +18574,7 @@ diff --git a/client.js b/client.js
atMenu = null;
for (const off of offs) off();
};
-@@ -14010,6 +14010,14 @@
+@@ -14010,6 +14010,14 @@ function apply(ctx) {
return () => fiber.dispose();
};
actions.canReferenceChat = () => atMenu !== null;
@@ -15248,7 +18589,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,44 @@
+@@ -14019,33 +14019,45 @@ function apply(ctx) {
installCompiledStyles(SHELL_CSS, "shell"),
installCompiledStyles(SIDEBAR_CSS, "sidebar"),
installCompiledStyles(SECRET_CSS, "secrets"),
@@ -15257,6 +18598,7 @@ diff --git a/client.js b/client.js
installCompiledStyles(USAGE_CSS, "usage"),
installCompiledStyles(SCHEDULE_CSS, "schedules"),
+ installCompiledStyles(INBOX_CSS, "inbox"),
++ installCompiledStyles(KANBAN_CSS, "kanban"),
installCompiledStyles(FEEDBACK_CSS, "feedback"),
installCompiledStyles(ABOUT_CSS, "about"),
+ installCompiledStyles(FLYBOT_CSS, "flybot"),
@@ -15297,7 +18639,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 @@
+@@ -14057,15 +14057,24 @@ function apply(ctx) {
// 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),
@@ -15325,7 +18667,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 @@
+@@ -14074,7 +14074,8 @@ function apply(ctx) {
shadow("conversation.input.dock", { id: "bot-members", order: -120, inject: face }, MemberDock),
shadow("conversation.input.dock", { id: "bot-model", order: -110, inject: face }, ModelChoiceDock),
shadow("conversation.input.dock", { id: "bot-question", order: -100, inject: face }, QuestionDock),
diff --git a/patches/ds-bot/files/src/client/kanban.js b/patches/ds-bot/files/src/client/kanban.js
new file mode 100644
index 000000000..6087e004b
--- /dev/null
+++ b/patches/ds-bot/files/src/client/kanban.js
@@ -0,0 +1,383 @@
+// Per-Bot task board (#1058). The records are the task list in src/host/tasks.js.
+// This is not the Agent inbox's board (#471): Inbox stays a list of what needs the
+// owner. Tasks opens one Bot's board, a child of that Bot, or a group an admin can see.
+import { createElement as h, useCallback, useEffect, useState } from 'react'
+import { columnOf, priorityOf, progressOf } from '../shared/task-board.js'
+import { CloseIcon } from './icons.js'
+import { useEscape } from './overlay-hooks.js'
+import { t } from './i18n.js'
+
+const POLL_MS = 15_000
+
+function BoardIcon() {
+ return h('svg', { width: 18, height: 18, viewBox: '0 0 20 20', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinejoin: 'round', 'aria-hidden': true },
+ h('rect', { x: 2.5, y: 3, width: 4, height: 14, rx: 1 }),
+ h('rect', { x: 8, y: 3, width: 4, height: 9, rx: 1 }),
+ h('rect', { x: 13.5, y: 3, width: 4, height: 12, rx: 1 }))
+}
+
+export function shortId(id) {
+ const hex = String(id ?? '').replace(/^task-/, '')
+ return `#${hex.slice(-6)}`
+}
+
+export function boardColumns(tasks) {
+ const columns = { 'not-started': [], 'in-progress': [], done: [] }
+ for (const task of tasks ?? []) {
+ const column = columnOf(task)
+ ;(columns[column] ?? columns['not-started']).push(task)
+ }
+ for (const list of Object.values(columns)) list.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))
+ return columns
+}
+
+function adminOf(roster, room) {
+ const me = roster?.self?.memberId
+ if (!me) return room?.origin !== 'face-to-face'
+ const human = (room?.participants ?? []).find(person => person.kind === 'human' && person.id === me && person.leftSeq === undefined)
+ if (human?.role === 'admin') return true
+ const admin = roster?.byId?.[room?.admin]
+ return Boolean(admin && !admin.remote && admin.ownerId === me)
+}
+
+// Own Bots, nested by who created them, then the groups this person administers.
+export function boardRail(roster) {
+ const me = roster?.self?.memberId
+ const bots = (roster?.bots ?? []).filter(bot => bot && !bot.remote && (!me || !bot.ownerId || bot.ownerId === me))
+ const ids = new Set(bots.map(bot => bot.id))
+ const children = new Map()
+ for (const bot of bots) {
+ const parent = bot.createdBy && ids.has(bot.createdBy) ? bot.createdBy : ''
+ const list = children.get(parent) ?? []
+ list.push(bot)
+ children.set(parent, list)
+ }
+ for (const list of children.values()) list.sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0) || String(a.name).localeCompare(String(b.name)))
+ const rows = []
+ const walk = (parent, depth) => {
+ for (const bot of children.get(parent) ?? []) {
+ rows.push({ kind: 'bot', id: bot.id, name: bot.name, depth, bot })
+ walk(bot.id, depth + 1)
+ }
+ }
+ walk('', 0)
+ const groups = (roster?.rooms ?? [])
+ .filter(room => room && room.hidden !== true && room.origin !== 'face-to-face' && adminOf(roster, room))
+ .map(room => ({ kind: 'group', id: room.id, name: room.displayName || room.name || t('Group'), room }))
+ return { bots: rows, groups }
+}
+
+export function canMoveTask(roster, task) {
+ const me = roster?.self?.memberId
+ if (!me) return true
+ const bot = roster?.byId?.[task?.ownerBotId]
+ return Boolean(bot && !bot.remote && (!bot.ownerId || bot.ownerId === me))
+}
+
+function priorityLabel(priority) {
+ if (priority === 'urgent') return t('Highest')
+ if (priority === 'high') return t('High')
+ if (priority === 'low') return t('Low')
+ return t('Normal')
+}
+
+function statusLabel(status) {
+ switch (status) {
+ case 'claimed': return t('Claimed')
+ case 'working': return t('In progress')
+ case 'blocked': return t('Blocked')
+ case 'result-ready': return t('Result ready')
+ case 'accepted': return t('Done')
+ case 'cancelled': return t('Cancelled')
+ case 'failed': return t('Failed')
+ default: return t('Not started')
+ }
+}
+
+function dropLabel(column) {
+ if (column === 'in-progress') return t('Start task')
+ if (column === 'done') return t('Complete task')
+ return t('Not started')
+}
+
+function columnTitle(column) {
+ if (column === 'in-progress') return t('In progress')
+ if (column === 'done') return t('Done')
+ return t('Not started')
+}
+
+function botName(roster, task) {
+ return roster?.byId?.[task.ownerBotId]?.name || task.ownerBotName || ''
+}
+
+function lanesOf(tasks, roster) {
+ const order = []
+ const map = new Map()
+ for (const task of tasks) {
+ const id = task.ownerBotId ?? ''
+ if (!map.has(id)) { map.set(id, []); order.push(id) }
+ map.get(id).push(task)
+ }
+ return order.map(id => ({ id, name: botName(roster, map.get(id)[0]) || id, tasks: map.get(id) }))
+}
+
+export function useBoardTasks(actions, scope) {
+ const [own, setOwn] = useState([])
+ const [groupTasks, setGroupTasks] = useState([])
+ const key = scope?.kind === 'group' ? `group:${scope.roomId}` : 'own'
+ useEffect(() => {
+ let stopped = false
+ const load = () => {
+ const job = scope?.kind === 'group'
+ ? Promise.resolve(actions?.tasks?.({ roomId: scope.roomId }))
+ : Promise.resolve(actions?.tasks?.())
+ // A reading host or an older core has no board; keep the list already shown.
+ void job.then((list) => {
+ if (stopped || !Array.isArray(list)) return
+ if (scope?.kind === 'group') setGroupTasks(list)
+ else setOwn(list)
+ }).catch(() => undefined)
+ }
+ load()
+ const timer = setInterval(load, POLL_MS)
+ return () => { stopped = true; clearInterval(timer) }
+ }, [key])
+ const tasks = scope?.kind === 'group' ? groupTasks : own.filter(task => !scope?.botId || task.ownerBotId === scope.botId)
+ const replace = (task) => {
+ const write = (list) => (list.some(item => item.id === task.id) ? list.map(item => item.id === task.id ? task : item) : [...list, task])
+ if (scope?.kind === 'group') setGroupTasks(write)
+ else setOwn(write)
+ }
+ return { own, tasks, replace }
+}
+
+function RailButton({ row, selected, meta, onSelect }) {
+ return h('button', {
+ type: 'button',
+ className: 'bt-board-rail-item',
+ 'data-kind': row.kind,
+ 'data-id': row.id,
+ 'data-selected': selected || undefined,
+ 'aria-current': selected ? 'true' : undefined,
+ style: row.depth ? { paddingLeft: `${10 + row.depth * 14}px` } : undefined,
+ onClick: () => onSelect(row.kind === 'group' ? { kind: 'group', roomId: row.id } : { kind: 'bot', botId: row.id }),
+ },
+ h('span', { className: 'bt-board-rail-name' }, row.name),
+ meta ? h('span', { className: 'bt-board-rail-meta' }, meta) : null)
+}
+
+export function BoardCard({ task, roster, movable, onMove, onDrag }) {
+ const column = columnOf(task)
+ const priority = priorityOf(task)
+ const progress = progressOf(task)
+ const who = botName(roster, task)
+ return h('article', {
+ className: 'bt-board-card',
+ 'data-task': task.id,
+ 'data-column': column,
+ 'data-priority': priority,
+ draggable: movable || undefined,
+ onDragStart: (event) => {
+ event.dataTransfer?.setData('text/plain', task.id)
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
+ onDrag?.(task.id)
+ },
+ onDragEnd: () => onDrag?.(null),
+ },
+ h('span', { className: 'bt-board-card-top' },
+ h('span', { className: 'bt-board-id' }, shortId(task.id)),
+ h('span', { className: 'bt-board-priority', 'data-priority': priority }, priorityLabel(priority)),
+ h('span', { className: 'bt-board-status' }, statusLabel(task.status))),
+ h('span', { className: 'bt-board-title' }, task.title || t('Untitled task')),
+ task.note ? h('span', { className: 'bt-board-note' }, task.note) : null,
+ h('span', { className: 'bt-board-foot' },
+ who ? h('span', { className: 'bt-board-who' }, who) : null,
+ h('span', { className: 'bt-board-progress' },
+ h('span', { className: 'bt-board-progress-bar', role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': progress, 'aria-label': t('Progress') },
+ h('span', { style: { width: `${progress}%` } })),
+ h('span', { className: 'bt-board-percent' }, t('{percent}%', { percent: progress })))),
+ movable && column === 'not-started' ? h('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' ? h('button', { type: 'button', className: 'bt-soft bt-soft-sm', onClick: () => onMove?.(task.id, 'done') }, t('Complete')) : null)
+}
+
+function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag }) {
+ return h('section', {
+ className: 'bt-board-column',
+ 'data-column': column,
+ 'aria-label': columnTitle(column),
+ onDragOver: (event) => { if (!dragging) return; event.preventDefault() },
+ onDrop: (event) => {
+ event.preventDefault()
+ const id = event.dataTransfer?.getData('text/plain')
+ if (id) onMove?.(id, column)
+ onDrag?.(null)
+ },
+ },
+ h('h3', { className: 'bt-board-column-head' },
+ h('span', null, columnTitle(column)),
+ h('span', { className: 'bt-board-count' }, String(tasks.length))),
+ dragging ? h('div', { className: 'bt-board-drop' }, dropLabel(column)) : null,
+ tasks.length === 0 && !dragging
+ ? h('p', { className: 'bt-board-empty' }, t('Nothing in this column.'))
+ : lanes
+ ? lanesOf(tasks, roster).map(lane => h('div', { key: lane.id, className: 'bt-board-lane' },
+ h('span', { className: 'bt-board-lane-name' }, lane.name),
+ lane.tasks.map(task => h(BoardCard, { key: task.id, task, roster, movable: canMoveTask(roster, task), onMove, onDrag }))))
+ : tasks.map(task => h(BoardCard, { key: task.id, task, roster, movable: canMoveTask(roster, task), onMove, onDrag })))
+}
+
+export function BoardView({ roster, tasks, rail, scope, counts, 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 h('div', { className: 'bt-board-body' },
+ h('aside', { className: 'bt-board-rail', 'aria-label': t('Task board') },
+ h('h3', { className: 'bt-board-rail-head' }, t('Your Bots')),
+ rail.bots.length === 0 ? h('p', { className: 'bt-board-empty' }, t('No tasks yet.')) : rail.bots.map(row => {
+ const count = counts?.[row.id] ?? { done: 0, total: 0 }
+ return h(RailButton, {
+ key: row.id, row, selected: row.id === selectedBot, onSelect,
+ meta: count.total > 0 ? t('{done}/{total}', { done: count.done, total: count.total }) : '',
+ })
+ }),
+ rail.groups.length > 0 ? h('h3', { className: 'bt-board-rail-head' }, t('Groups you admin')) : null,
+ rail.groups.map(row => h(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect }))),
+ h('div', { className: 'bt-board-main' },
+ error ? h('p', { className: 'bt-board-error', role: 'alert' }, error) : null,
+ scope?.kind === 'bot' && onCreate ? h('form', { className: 'bt-board-add', onSubmit: (event) => { event.preventDefault(); onCreate(draft) } },
+ h('input', { 'aria-label': t('Task title'), placeholder: t('New task'), value: draft ?? '', onChange: (event) => onDraft?.(event.target.value) }),
+ h('button', { type: 'submit', className: 'bt-soft bt-soft-sm' }, t('Add task'))) : null,
+ h('div', { className: 'bt-board-columns' },
+ ['not-started', 'in-progress', 'done'].map(column => h(BoardColumn, {
+ key: column, column, tasks: columns[column], roster, lanes, dragging, onMove, onDrag,
+ })))))
+}
+
+export function KanbanRow({ wide, useUi, actions }) {
+ const open = useUi ? useUi(value => Boolean(value.board)) : false
+ const label = t('Tasks')
+ return h('button', {
+ type: 'button', className: 'bt-row bt-board-row', 'aria-label': label, title: wide ? undefined : label,
+ 'aria-haspopup': 'dialog', 'aria-expanded': open, onClick: () => actions.openBoard(),
+ },
+ h('span', { className: 'bt-board-mark' }, h(BoardIcon)),
+ wide ? h('span', { className: 'bt-board-label' }, label) : null)
+}
+
+function scopeName(roster, scope) {
+ if (scope?.kind === 'group') return roster.roomsById?.[scope.roomId]?.name || t('Group')
+ if (scope?.kind === 'bot') return roster.byId?.[scope.botId]?.name || ''
+ return ''
+}
+
+export function KanbanDialog({ roster, actions, leaving = false }) {
+ const rail = boardRail(roster)
+ const [scope, setScope] = 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] = useState(null)
+ const [error, setError] = useState('')
+ const [draft, setDraft] = useState('')
+ const { own, tasks, replace } = useBoardTasks(actions, scope)
+ const close = useCallback(() => actions.closeBoard(), [])
+ useEscape(close)
+ const counts = {}
+ for (const task of own) {
+ const id = task.ownerBotId
+ const bucket = counts[id] ?? (counts[id] = { done: 0, total: 0 })
+ if (task.status === 'cancelled' || task.status === 'failed') continue
+ bucket.total += 1
+ if (columnOf(task) === 'done') bucket.done += 1
+ }
+ const move = async (id, column) => {
+ const task = tasks.find(item => item.id === id) ?? own.find(item => item.id === id)
+ if (!task || !canMoveTask(roster, task) || columnOf(task) === column) return
+ setError('')
+ try {
+ replace(await actions.taskUpdate({ id, column }))
+ } catch (failure) {
+ setError(failure?.message ?? String(failure))
+ }
+ }
+ const create = async (title) => {
+ const text = String(title ?? '').trim()
+ if (!text || scope?.kind !== 'bot') return
+ setError('')
+ try {
+ replace(await actions.taskCreate({ botId: scope.botId, title: text, column: 'not-started' }))
+ setDraft('')
+ } catch (failure) {
+ setError(failure?.message ?? String(failure))
+ }
+ }
+ const name = scopeName(roster, scope)
+ return h('div', { className: 'bt-mem-layer bt-board-layer', 'data-leaving': leaving || undefined },
+ h('div', { className: 'bt-scrim', onClick: close }),
+ h('div', { className: 'bt-mem bt-board', role: 'dialog', 'aria-modal': true, 'aria-labelledby': 'bt-board-heading' },
+ h('div', { className: 'bt-mem-head' },
+ h('h2', { id: 'bt-board-heading' }, t('Tasks')),
+ h('span', { className: 'bt-mem-status' }, name),
+ h('span', { className: 'bt-mem-head-gap' }),
+ h('button', { type: 'button', className: 'bt-icon-btn', 'aria-label': t('Close tasks'), autoFocus: true, onClick: close }, h(CloseIcon))),
+ h(BoardView, {
+ roster, tasks, rail, scope, counts, dragging, error, draft,
+ onDraft: setDraft, onSelect: setScope, onMove: move, onDrag: setDragging, onCreate: create,
+ })))
+}
+
+export const 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)}
+.bt-board-mark svg{width:18px;height:18px}
+.bt-board-label{flex:1 1 auto;min-width:0;font-size:14px;line-height:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.bt-mem.bt-board{width:min(1080px,calc(100vw - 32px));height:min(760px,calc(100vh - 48px))}
+.bt-board-body{flex:1 1 auto;min-height:0;display:flex;gap:0}
+.bt-board-rail{flex:none;width:232px;overflow:auto;border-right:.5px solid var(--bt-line);padding:8px 8px 16px;display:flex;flex-direction:column;gap:2px}
+.bt-board-rail-head{margin:10px 8px 4px;font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3)}
+.bt-board-rail-item{display:flex;align-items:center;gap:8px;width:100%;min-height:36px;padding:6px 10px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer}
+.bt-board-rail-item:hover{background:var(--bt-hover)}
+.bt-board-rail-item[data-selected]{background:var(--bt-active,var(--bt-hover))}
+.bt-board-rail-name{flex:1 1 auto;min-width:0;font-size:13.5px;line-height:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.bt-board-rail-meta{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:10px;padding:12px 14px 16px}
+.bt-board-add{display:flex;gap:8px}
+.bt-board-add input{flex:1 1 auto;min-width:0;height:32px;padding:0 10px;border:.5px solid var(--bt-line-2);border-radius:10px;background:var(--bt-card,var(--bt-main));color:var(--bt-ink);font:inherit}
+.bt-board-columns{flex:1 1 auto;min-height:0;display:flex;gap:10px}
+.bt-board-column{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:8px;padding:8px;border-radius:14px;background:var(--bt-hover);overflow:auto}
+.bt-board-column-head{display:flex;align-items:center;gap:6px;margin:0;padding:2px 4px;font-size:13px;line-height:18px;font-weight:600;color:var(--bt-ink-2)}
+.bt-board-count{font-weight:500;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-drop{margin:0;padding:18px 8px;border:1px dashed var(--bt-line-2);border-radius:12px;text-align:center;font-size:13px;line-height:18px;color:var(--bt-ink-3)}
+.bt-board-empty{margin:0;padding:8px 4px;font-size:13px;line-height:18px;color:var(--bt-ink-3)}
+.bt-board-error{margin:0;font-size:13px;line-height:18px;color:#c21d2e}
+.bt-board-lane{display:flex;flex-direction:column;gap:6px}
+.bt-board-lane-name{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-3);padding:0 2px}
+.bt-board-card{display:flex;flex-direction:column;gap:6px;padding:10px;border:.5px solid var(--bt-line-2);border-radius:12px;background:var(--bt-card,var(--bt-main));cursor:grab}
+.bt-board-card-top{display:flex;align-items:center;gap:6px;min-width:0}
+.bt-board-id{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-priority{flex:none;height:18px;padding:0 6px;border-radius:999px;font-size:11px;line-height:18px;font-weight:600}
+.bt-board-priority[data-priority=urgent]{background:rgba(224,33,53,.12);color:#c21d2e}
+.bt-board-priority[data-priority=high]{background:rgba(217,119,6,.14);color:#b45309}
+.bt-board-priority[data-priority=normal]{background:color-mix(in srgb,var(--bt-accent) 14%,transparent);color:color-mix(in srgb,var(--bt-accent) 80%,var(--bt-ink))}
+.bt-board-priority[data-priority=low]{background:color-mix(in srgb,var(--bt-green) 16%,transparent);color:color-mix(in srgb,var(--bt-green) 70%,var(--bt-ink))}
+.bt-board-status{margin-left:auto;font-size:11.5px;line-height:16px;color:var(--bt-ink-3);white-space:nowrap}
+.bt-board-title{font-size:13.5px;line-height:19px;color:var(--bt-ink)}
+.bt-board-note{font-size:12px;line-height:16px;color:var(--bt-ink-2)}
+.bt-board-foot{display:flex;align-items:center;gap:8px}
+.bt-board-who{flex:none;max-width:40%;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.bt-board-progress{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:6px}
+.bt-board-progress-bar{flex:1 1 auto;height:4px;border-radius:999px;background:var(--bt-line);overflow:hidden}
+.bt-board-progress-bar>span{display:block;height:100%;background:var(--bt-accent)}
+.bt-board-percent{flex:none;font-size:11.5px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums}
+.bt-board-card .bt-soft{align-self:flex-start}
+@media (max-width:800px){
+ .bt-mem.bt-board{width:min(100vw,calc(100vw - 16px));height:100%}
+ .bt-board-body{flex-direction:column}
+ .bt-board-rail{width:auto;max-height:180px;border-right:0;border-bottom:.5px solid var(--bt-line)}
+ .bt-board-columns{overflow:auto}
+ .bt-board-column{min-width:220px}
+}
+@media (max-width:640px){
+ .bt-mem.bt-board{width:100vw;border-radius:0;border:0}
+ .bt-board-columns{flex-direction:column}
+ .bt-board-column{min-width:0}
+}
+`
diff --git a/patches/ds-bot/files/src/host/authz.js b/patches/ds-bot/files/src/host/authz.js
index f09a47a5b..855730add 100644
--- a/patches/ds-bot/files/src/host/authz.js
+++ b/patches/ds-bot/files/src/host/authz.js
@@ -58,7 +58,7 @@ const SESSION_TARGETED = new Set(['earlier', 'send', 'answer-question', 'dismiss
// Task channel names. Requester checks live on the record in tasks.js; listing them
// here keeps a future `task-*` from auto-allowing for any signed-in actor (#111).
const TASK_ENDPOINTS = new Set([
- 'tasks', 'task-create', 'task-accept', 'task-start', 'task-return', 'task-accept-result', 'task-cancel',
+ 'tasks', 'task-create', 'task-update', 'task-accept', 'task-start', 'task-return', 'task-accept-result', 'task-cancel',
'task-wait-input', 'task-answer-input',
])
diff --git a/patches/ds-bot/files/src/host/tasks.js b/patches/ds-bot/files/src/host/tasks.js
index 9af08f121..4ab53b474 100644
--- a/patches/ds-bot/files/src/host/tasks.js
+++ b/patches/ds-bot/files/src/host/tasks.js
@@ -7,13 +7,16 @@
//
// Create is grant-aware: the requester need not own the Bot. A granted child (or a
// Bot in the named room the requester currently sits in) may take a task within
-// its authority. Outgoing holds stay the owner's (#52 card).
+// its authority. Outgoing holds stay the owner's (#52 card). The board (#1058)
+// also lets that Bot's owner set priority, progress, note and column; the other
+// transitions stay the requester's.
//
// States: open → claimed → working → blocked ⇄ working → result-ready → accepted;
// A returned result may also be cancelled before acceptance. Cancelled and failed
// are terminal, and accepted is reachable only from result-ready. A cancelled or
// failed task never becomes accepted. Records keep pointers and a summary, not transcripts.
import { randomBytes } from 'node:crypto'
+import { columnOf } from '../shared/task-board.js'
import { currentParticipants } from './room-membership.js'
const ACTIVE = new Set(['open', 'claimed', 'working', 'blocked'])
@@ -36,6 +39,34 @@ const forbidden = message => Object.assign(new Error(message), { code: 'bot/forb
const invalid = message => Object.assign(new Error(message), { code: 'bot/invalid' })
const waitingForInput = task => task.status === 'blocked' && task.waitingFor === 'input'
const canLinkSend = task => task.status === 'working' || waitingForInput(task)
+const PRIORITY = new Set(['urgent', 'high', 'normal', 'low'])
+const WALK = ['open', 'claimed', 'working', 'result-ready', 'accepted']
+const TERMINAL_MATCH = new Set(['accepted', 'cancelled', 'failed'])
+
+const readPriority = (value) => {
+ if (value === undefined || value === null || value === '') return undefined
+ const priority = String(value)
+ if (!PRIORITY.has(priority)) throw invalid('Priority must be urgent, high, normal or low')
+ return priority
+}
+const readProgress = (value) => {
+ if (value === undefined || value === null || value === '') return undefined
+ const progress = typeof value === 'number' ? value : Number(value)
+ if (!Number.isInteger(progress) || progress < 0 || progress > 100) throw invalid('Progress must be a whole number from 0 to 100')
+ return progress
+}
+const oneLine = (value, max) => String(value ?? '').replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max)
+const readTitle = value => oneLine(value, 300)
+const readNote = (value) => {
+ if (value === undefined || value === null) return undefined
+ return oneLine(value, 300)
+}
+const readColumn = (value) => {
+ if (value === undefined || value === null || value === '') return undefined
+ const column = String(value)
+ if (column !== 'not-started' && column !== 'in-progress' && column !== 'done') throw invalid('Column must be not-started, in-progress or done')
+ return column
+}
const selectedInput = (value, name) => {
if (typeof value !== 'string' || !value.trim() || value.length > 2000) throw invalid(`${name} must contain 1–2000 characters`)
return value
@@ -99,7 +130,6 @@ export function install(rt) {
record(task, actor, extra)
}
- const view = task => ({ ...task, actors: [...task.actors ?? []] })
const prune = () => {
const all = Object.values(store())
const droppable = all.filter(task => !PROTECTED.has(task.status)).sort((a, b) => a.updatedAt - b.updatedAt)
@@ -107,6 +137,154 @@ export function install(rt) {
}
const save = () => rt.save()
+ const botRecord = id => state.bots?.[id]
+ // The human who owns this Bot. With no signed-in member the single-human product
+ // may see every local Bot, the same way the rest of the task API does.
+ const owns = (actor, botId) => {
+ const bot = botRecord(botId)
+ if (!bot || bot.remote !== undefined) return false
+ if (!rt.authzEnforced?.()) return true
+ return Boolean(actor?.memberId && bot.ownerId === actor.memberId)
+ }
+ // Walk createdBy. Owning the Bot, or any ancestor, is enough to read its tasks.
+ const ancestorOwned = (actor, botId) => {
+ const seen = new Set()
+ let id = botId
+ while (id && !seen.has(id)) {
+ seen.add(id)
+ if (owns(actor, id)) return true
+ id = botRecord(id)?.createdBy
+ }
+ return false
+ }
+ // A group admin is a current human participant with that role, or the owner of
+ // the Bot that administers the group. Membership alone is not enough.
+ const roomAdmin = (actor, room) => {
+ if (!room || !actor?.memberId) return false
+ const me = currentParticipants(room).find(p => p.kind === 'human' && p.id === actor.memberId)
+ if (me?.role === 'admin') return true
+ return owns(actor, room.admin)
+ }
+ const mayUpdate = (task, actor) => {
+ if (!rt.authzEnforced?.()) return true
+ return mayAct(task, actor) || owns(actor, task.ownerBotId)
+ }
+ const view = task => ({
+ ...task,
+ actors: [...task.actors ?? []],
+ ...(botRecord(task.ownerBotId)?.name ? { ownerBotName: botRecord(task.ownerBotId).name } : {}),
+ })
+
+ // Move a card along the existing states. Not started is open/claimed, In progress
+ // is working (or blocked / result-ready), Done is accepted. Reverse moves and a
+ // blocked wait are refused: the wait has its own resume.
+ function applyColumn(task, column, actor, by) {
+ if (!column || columnOf(task) === column) return false
+ if (task.status === 'blocked') throw stale('A blocked task stays put until its wait is cleared')
+ if (task.status === 'cancelled' || task.status === 'failed') throw stale(`a task that is ${task.status} cannot become ${column}`)
+ if (column === 'not-started') throw stale('a task that is in progress or done cannot move back to not started')
+ const target = column === 'in-progress' ? 'working' : 'accepted'
+ const from = WALK.indexOf(task.status)
+ const goal = WALK.indexOf(target)
+ if (from < 0 || goal < 0 || from > goal) throw stale(`a task that is ${task.status} cannot become ${column}`)
+ const extra = by ? { by } : undefined
+ for (let i = from + 1; i <= goal; i++) {
+ const next = WALK[i]
+ transition(task, next, actor, extra)
+ if (next === 'result-ready') task.result = { kind: 'summary', summary: (task.note || 'Completed').slice(0, 2000) }
+ }
+ return true
+ }
+
+ function applyFields(task, body) {
+ const title = body.title !== undefined ? readTitle(body.title) : undefined
+ if (title !== undefined) {
+ if (title === '') throw invalid('A task needs a title')
+ task.title = title
+ }
+ const priority = readPriority(body.priority)
+ if (priority !== undefined) task.priority = priority
+ const progress = readProgress(body.progress)
+ if (progress !== undefined) task.progress = progress
+ if (body.note !== undefined) {
+ const note = readNote(body.note)
+ if (note) task.note = note
+ else delete task.note
+ }
+ return { priority, progress }
+ }
+
+ const roomByName = (botId, name) => {
+ const rooms = Object.values(state.rooms ?? {}).filter(room => Array.isArray(room.members) && room.members.includes(botId) && room.name === name)
+ if (rooms.length !== 1) throw invalid(`No single group named "${name.slice(0, 80)}" that you are in`)
+ return rooms[0].id
+ }
+ const resolveRoom = (botId, group, fallback) => {
+ if (group === undefined || group === null || String(group).trim() === '') return fallback || undefined
+ return roomByName(botId, String(group).trim())
+ }
+ const matchTitle = (botId, title, roomId) => {
+ const found = Object.values(store()).filter(task => task.ownerBotId === botId && task.title === title && !TERMINAL_MATCH.has(task.status) && (task.origin?.roomId ?? undefined) === roomId)
+ if (found.length > 1) throw invalid(`Two open tasks are named "${title}". Pass an id.`)
+ return found[0]
+ }
+ const listed = (botId) => Object.values(store()).filter(task => task.ownerBotId === botId).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 40).map(view)
+
+ // The Bot's own list, written from the conversation. The caller is the Bot;
+ // the requester stays the Bot's owner, so the owner can still move the card.
+ rt.trackBotTasks = async function trackBotTasks(botId, body, origin = {}) {
+ const bot = botRecord(botId)
+ if (!bot || bot.remote !== undefined) throw forbidden('No such Bot')
+ const items = body?.tasks
+ if (items === undefined) return listed(botId)
+ if (!Array.isArray(items)) throw invalid('tasks must be a list')
+ if (items.length > 20) throw invalid('Track at most 20 tasks at once')
+ const actor = bot.ownerId ? { memberId: bot.ownerId } : null
+ const saved = []
+ for (const item of items) {
+ if (item === null || typeof item !== 'object' || Array.isArray(item)) throw invalid('Each task must be an object')
+ const column = readColumn(item.column)
+ const title = item.title !== undefined ? readTitle(item.title) : undefined
+ const roomId = resolveRoom(botId, item.group, origin.roomId)
+ let task
+ let isNew = false
+ if (item.id !== undefined && item.id !== null && String(item.id) !== '') {
+ task = find(item.id)
+ if (!task || task.ownerBotId !== botId) throw forbidden('That task is not yours')
+ } else {
+ if (!title) throw invalid('A new task needs a title')
+ task = matchTitle(botId, title, roomId)
+ }
+ if (!task) {
+ isNew = true
+ const at = stamp()
+ task = {
+ id: `task-${randomBytes(8).toString('hex')}`,
+ status: 'open',
+ createdAt: at,
+ updatedAt: at,
+ requesterId: bot.ownerId ?? null,
+ ownerBotId: botId,
+ title,
+ source: 'conversation',
+ origin: { ...(roomId ? { roomId } : {}), ...(origin.sessionId ? { sessionId: String(origin.sessionId) } : {}) },
+ actors: [{ status: 'open', at, by: 'conversation', ...(actor?.memberId ? { memberId: actor.memberId } : {}) }],
+ }
+ store()[task.id] = task
+ } else if (title) task.title = title
+ const { progress } = applyFields(task, item)
+ if (!task.source) task.source = 'conversation'
+ const before = task.updatedAt
+ const moved = applyColumn(task, column, null, 'conversation')
+ if (column === 'done' && progress === undefined) task.progress = 100
+ if (!isNew && !moved && task.updatedAt === before) record(task, null, { by: 'conversation' })
+ saved.push(task)
+ }
+ prune()
+ await save()
+ return saved.map(view)
+ }
+
// ---------------------------------------------------------------- host hooks
// Name the working task a send belongs to so a hold can block that one (#111).
// Prefer an already-stamped `meta.taskId`; else the Bot's only working task;
@@ -189,11 +367,30 @@ export function install(rt) {
// ---------------------------------------------------------------- channel API
rt.taskEndpoint = async function taskEndpoint(endpoint, body, actor) {
switch (endpoint) {
- case 'tasks':
- return Object.values(store())
- .filter(task => mayAct(task, actor))
- .sort((a, b) => a.createdAt - b.createdAt)
- .map(view)
+ case 'tasks': {
+ const botId = body.botId ? String(body.botId) : ''
+ const roomId = body.roomId ? String(body.roomId) : ''
+ const all = Object.values(store())
+ let rows
+ if (roomId) {
+ const room = state.rooms?.[roomId]
+ if (room === undefined) throw forbidden('No such group')
+ if (rt.authzEnforced?.() && !roomAdmin(actor, room)) throw forbidden('Only a group admin can see that group\'s tasks')
+ const members = new Set(currentParticipants(room).filter(p => p.kind === 'bot' && !p.remote).map(p => p.id))
+ rows = all.filter(task => task.origin?.roomId === roomId && members.has(task.ownerBotId))
+ } else if (botId) {
+ const bot = botRecord(botId)
+ if (bot === undefined || bot.remote !== undefined) throw forbidden('No such Bot')
+ const seesBot = ancestorOwned(actor, botId)
+ rows = all.filter(task => task.ownerBotId === botId && (seesBot || mayAct(task, actor)))
+ if (rt.authzEnforced?.() && !seesBot && rows.length === 0) throw forbidden('You cannot see that Bot\'s tasks')
+ } else {
+ // Ownership and the tree. Group tasks of someone else's Bot stay behind
+ // the room query, so a plain list never becomes a group-admin view.
+ rows = all.filter(task => mayAct(task, actor) || ancestorOwned(actor, task.ownerBotId))
+ }
+ return rows.sort((a, b) => a.createdAt - b.createdAt).map(view)
+ }
case 'task-create': {
const botId = String(body.botId ?? '')
const bot = state.bots?.[botId]
@@ -209,16 +406,33 @@ export function install(rt) {
requesterId,
ownerBotId: botId,
// workBotId is the specialist on the exchange; assigned with #55 wiring (#111).
- title: String(body.title ?? '').slice(0, 300),
+ title: readTitle(body.title),
...(typeof body.criteria === 'string' && body.criteria.trim() !== '' ? { criteria: body.criteria.slice(0, 2000) } : {}),
origin: { ...(roomId ? { roomId } : {}), ...(body.sessionId ? { sessionId: String(body.sessionId) } : {}) },
actors: [{ status: 'open', at: stamp(), ...(actor?.memberId ? { memberId: actor.memberId } : {}) }],
}
+ const { progress } = applyFields(task, body)
+ const column = readColumn(body.column)
+ applyColumn(task, column, actor)
+ if (column === 'done' && progress === undefined) task.progress = 100
store()[task.id] = task
prune()
await save()
return view(task)
}
+ case 'task-update': {
+ const task = find(body.id)
+ if (task === undefined) throw stale(`no task ${String(body.id ?? '')}`)
+ if (!mayUpdate(task, actor)) throw forbidden('Only the requester or the Bot\'s owner may update this task')
+ const { progress } = applyFields(task, body)
+ const column = readColumn(body.column)
+ const before = task.updatedAt
+ const moved = applyColumn(task, column, actor)
+ if (column === 'done' && progress === undefined) task.progress = 100
+ if (!moved && task.updatedAt === before) record(task, actor, { by: 'updated' })
+ await save()
+ return view(task)
+ }
case 'task-accept': {
const task = find(body.id)
if (task === undefined) throw stale(`no task ${String(body.id ?? '')}`)
diff --git a/patches/ds-bot/files/src/shared/task-board.js b/patches/ds-bot/files/src/shared/task-board.js
new file mode 100644
index 000000000..b9f1d4a1c
--- /dev/null
+++ b/patches/ds-bot/files/src/shared/task-board.js
@@ -0,0 +1,31 @@
+// Board columns for the task records in src/host/tasks.js (#1058).
+// The state machine stays the record. A column is how the board reads it.
+// result-ready still waits for acceptance, so it stays In progress.
+
+export const COLUMNS = ['not-started', 'in-progress', 'done']
+export const PRIORITIES = ['urgent', 'high', 'normal', 'low']
+
+const COLUMN_OF = {
+ open: 'not-started',
+ claimed: 'not-started',
+ working: 'in-progress',
+ blocked: 'in-progress',
+ 'result-ready': 'in-progress',
+ accepted: 'done',
+ cancelled: 'done',
+ failed: 'done',
+}
+
+export function columnOf(task) {
+ return COLUMN_OF[task?.status] ?? 'not-started'
+}
+
+export function priorityOf(task) {
+ return PRIORITIES.includes(task?.priority) ? task.priority : 'normal'
+}
+
+export function progressOf(task) {
+ const value = task?.progress
+ if (typeof value !== 'number' || !Number.isFinite(value)) return columnOf(task) === 'done' && task?.status !== 'cancelled' && task?.status !== 'failed' ? 100 : 0
+ return Math.max(0, Math.min(100, Math.round(value)))
+}
diff --git a/patches/ds-bot/files/test/authz.test.js b/patches/ds-bot/files/test/authz.test.js
index db81a421b..4b7c1e525 100644
--- a/patches/ds-bot/files/test/authz.test.js
+++ b/patches/ds-bot/files/test/authz.test.js
@@ -273,7 +273,7 @@ test('task endpoints are an explicit allow-list, not a task-* prefix', async (t)
await rt.load()
const sessionA = await redeemOwner(rt)
const actorA = await rt.authenticate(sessionA.sessionToken)
- for (const ep of ['tasks', 'task-create', 'task-accept', 'task-start', 'task-return', 'task-accept-result', 'task-cancel']) {
+ for (const ep of ['tasks', 'task-create', 'task-update', 'task-accept', 'task-start', 'task-return', 'task-accept-result', 'task-cancel']) {
assert.equal(rt.authorize(actorA, ep, {}).ok, true, ep)
}
const unknown = rt.authorize(actorA, 'task-explode', {})
diff --git a/patches/ds-bot/files/test/bot-tasks.test.js b/patches/ds-bot/files/test/bot-tasks.test.js
new file mode 100644
index 000000000..6d845f15e
--- /dev/null
+++ b/patches/ds-bot/files/test/bot-tasks.test.js
@@ -0,0 +1,245 @@
+// Per-Bot task board (#1058): the existing task records gain a board column,
+// progress and priority. A Bot records work from its own conversation with
+// track_tasks. The owner sees that Bot's list; a tree parent can read a
+// descendant's list; a group admin can read tasks tagged with the group.
+// A non-admin cannot read another Bot's group tasks, and a read is not a write.
+import assert from 'node:assert/strict'
+import { existsSync, mkdtempSync, readFileSync, 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 { columnOf } from '../src/shared/task-board.js'
+import { fakeContext } from './fake-ctx.js'
+
+const team = () => ({
+ version: 2,
+ revision: 1,
+ mainBotIds: ['a'],
+ members: {
+ 'owner-1': { id: 'owner-1', createdAt: 1, designatedChiefId: 'a' },
+ },
+ bots: {
+ a: { id: 'a', name: 'ChiefA', role: 'Boss', sessionId: 'a', createdAt: 1, ownerId: 'owner-1' },
+ s: { id: 's', name: 'Writer', role: 'Write', sessionId: 's', createdAt: 3, ownerId: 'owner-1', createdBy: 'a' },
+ },
+ rooms: {
+ war: {
+ id: 'war', name: 'War', members: ['a', 's'], admin: 'a', mode: 'everyone', named: true, sessions: { s: 's-war' }, log: [], seq: 0, seen: {},
+ participants: [
+ { kind: 'human', id: 'owner-1', role: 'member', displayName: 'Alice', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 'a', role: 'admin', displayName: 'ChiefA', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 's', role: 'member', displayName: 'Writer', joinedSeq: 0, joinedAt: 1 },
+ ],
+ },
+ },
+ grants: {},
+ agents: {}, exchanges: [], exchangeTurns: {}, questions: {}, prefs: {},
+})
+
+async function start(t, config = {}) {
+ const home = config.home ?? mkdtempSync(join(tmpdir(), 'ds-bot-board-'))
+ if (config.state !== undefined || !existsSync(join(home, 'state.json'))) writeFileSync(join(home, 'state.json'), JSON.stringify(config.state ?? team()))
+ const ctx = fakeContext()
+ const agents = new Map()
+ const agentOf = (sessionId) => {
+ if (!agents.has(sessionId)) {
+ agents.set(sessionId, {
+ session: { id: sessionId }, id: sessionId, calls: [], status: 'idle',
+ followup() {}, steer() {}, inject() {}, cancel() {}, async whenIdle() {},
+ inbox: { nextStep: [], nextTurn: [], remove() {} },
+ })
+ }
+ 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: `s${++created}` }), resolveAgent: async id => ({ agent: agentOf(id) }), selectModel: async () => {}, page: async () => ({ records: [], hasMore: false }) },
+ sessionTitle: { rename: async () => {} },
+ sessions: { flush: async () => {} },
+ })
+ let route
+ let created = 0
+ const register = ctx.connection.fetch.register
+ ctx.connection.fetch.register = (options) => {
+ if (options.path.endsWith('bot')) route = options
+ return register(options)
+ }
+ apply(ctx, { home })
+ const call = async (endpoint, payload, token) => {
+ const headers = token ? { 'x-flybot-member-session': token } : undefined
+ return (await route.fetch(new Request('http://localhost/api/bot', { method: 'POST', headers, body: JSON.stringify({ endpoint, payload }) }))).json()
+ }
+ const stop = async ({ keepHome = false } = {}) => {
+ await ctx.dispose()
+ if (!keepHome) rmSync(home, { recursive: true, force: true, maxRetries: 5 })
+ }
+ const signIn = async (displayName, inviterToken) => {
+ const invite = await call('auth-invite', {}, inviterToken)
+ assert.equal(invite.ok, true, JSON.stringify(invite))
+ const redeemed = await call('auth-redeem', { token: invite.value.token, displayName })
+ assert.equal(redeemed.ok, true, JSON.stringify(redeemed))
+ return redeemed.value
+ }
+ const track = (sessionId, args) => ctx.registeredTools.get('track_tasks').execute(args ?? {}, { agent: { id: sessionId } })
+ return { ctx, call, stop, signIn, track, home, statePath: join(home, 'state.json') }
+}
+
+const list = async (call, token, payload) => {
+ const result = await call('tasks', payload ?? {}, token)
+ assert.equal(result.ok, true, JSON.stringify(result))
+ return result.value
+}
+
+test('a Bot records conversation tasks and the owner moves them across the board', async (t) => {
+ const first = await start(t)
+ let app = first
+ t.after(() => app.stop())
+ const alice = await first.signIn('Alice')
+ const created = await first.track('s', { tasks: [{ title: 'Draft the note', column: 'not-started', priority: 'high', progress: 0 }] })
+ assert.match(created, /task-/)
+ const open = (await list(first.call, alice.sessionToken, { botId: 's' })).find(task => task.title === 'Draft the note')
+ assert.ok(open, 'the owner sees the Bot\'s task')
+ assert.equal(open.source, 'conversation')
+ assert.equal(open.status, 'open')
+ assert.equal(columnOf(open), 'not-started')
+ assert.equal(open.priority, 'high')
+ assert.equal(open.progress, 0)
+ assert.equal(open.ownerBotId, 's')
+ assert.equal(open.requesterId, alice.memberId)
+
+ const again = await first.track('s', { tasks: [{ title: 'Draft the note', column: 'in-progress', progress: 40, note: 'Outline done' }] })
+ assert.match(again, new RegExp(open.id))
+ const working = (await list(first.call, alice.sessionToken, { botId: 's' })).filter(task => task.title === 'Draft the note')
+ assert.equal(working.length, 1, 'the same title updates the open task')
+ assert.equal(working[0].id, open.id)
+ assert.equal(working[0].status, 'working')
+ assert.equal(columnOf(working[0]), 'in-progress')
+ assert.equal(working[0].progress, 40)
+ assert.equal(working[0].note, 'Outline done')
+
+ const started = await first.call('task-update', { id: open.id, column: 'not-started' }, alice.sessionToken)
+ assert.equal(started.ok, false)
+ assert.equal(started.error.code, 'bot/stale')
+ assert.equal((await list(first.call, alice.sessionToken, { botId: 's' })).find(task => task.id === open.id).status, 'working')
+
+ const done = await first.call('task-update', { id: open.id, column: 'done' }, alice.sessionToken)
+ assert.equal(done.ok, true, JSON.stringify(done))
+ assert.equal(done.value.status, 'accepted')
+ assert.equal(columnOf(done.value), 'done')
+ assert.equal(done.value.progress, 100)
+ assert.equal(done.value.result.summary, 'Outline done')
+
+ const late = await first.call('task-update', { id: open.id, column: 'in-progress' }, alice.sessionToken)
+ assert.equal(late.ok, false)
+ assert.equal(late.error.code, 'bot/stale')
+
+ await first.stop({ keepHome: true })
+ const restored = await start(t, { home: first.home })
+ app = restored
+ const saved = (await list(restored.call, alice.sessionToken, { botId: 's' })).find(task => task.id === open.id)
+ assert.equal(saved.status, 'accepted')
+ assert.equal(saved.progress, 100)
+ assert.equal(saved.priority, 'high')
+})
+
+test('board moves walk the task states and a blocked task stays put', async (t) => {
+ const app = await start(t)
+ t.after(() => app.stop())
+ const alice = await app.signIn('Alice')
+ const created = await app.call('task-create', { botId: 's', title: 'Check the figures', priority: 'low', progress: 0 }, alice.sessionToken)
+ assert.equal(created.ok, true, JSON.stringify(created))
+ const id = created.value.id
+ assert.equal(created.value.priority, 'low')
+ assert.equal(created.value.progress, 0)
+ const started = await app.call('task-update', { id, column: 'in-progress', progress: 25 }, alice.sessionToken)
+ assert.equal(started.ok, true, JSON.stringify(started))
+ assert.equal(started.value.status, 'working')
+ assert.equal(started.value.progress, 25)
+ assert.ok(started.value.actors.some(entry => entry.status === 'claimed'))
+ const waiting = await app.call('task-wait-input', { id, question: 'Which quarter?' }, alice.sessionToken)
+ assert.equal(waiting.ok, true, JSON.stringify(waiting))
+ const refused = await app.call('task-update', { id, column: 'done' }, alice.sessionToken)
+ assert.equal(refused.ok, false)
+ assert.equal(refused.error.code, 'bot/stale')
+ assert.equal((await list(app.call, alice.sessionToken)).find(task => task.id === id).status, 'blocked')
+ const bad = await app.call('task-update', { id, priority: 'top' }, alice.sessionToken)
+ assert.equal(bad.ok, false)
+ assert.equal(bad.error.code, 'bot/invalid')
+})
+
+test('a tree parent can read a child, and only a group admin can read other Bots\' group tasks', async (t) => {
+ const first = await start(t)
+ let app = first
+ t.after(() => app.stop())
+ const alice = await first.signIn('Alice')
+ assert.equal(alice.memberId, 'owner-1')
+ const bob = await first.signIn('Bob', alice.sessionToken)
+ const carol = await first.signIn('Carol', alice.sessionToken)
+ await first.stop({ keepHome: true })
+ const saved = JSON.parse(readFileSync(first.statePath, 'utf8'))
+ saved.bots.b = { id: 'b', name: 'BobBot', role: 'Help', sessionId: 'b', createdAt: 4, ownerId: bob.memberId, createdBy: null }
+ saved.bots.c = { id: 'c', name: 'Child', role: 'Help', sessionId: 'c', createdAt: 5, ownerId: bob.memberId, createdBy: 'a' }
+ const room = saved.rooms.war
+ room.members = ['a', 's', 'b', 'c']
+ room.admin = 'a'
+ room.sessions = { s: 's-war', b: 'b-war', c: 'c-own' }
+ room.participants = [
+ { kind: 'human', id: alice.memberId, role: 'member', displayName: 'Alice', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'human', id: bob.memberId, role: 'member', displayName: 'Bob', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'human', id: carol.memberId, role: 'admin', displayName: 'Carol', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 'a', role: 'admin', displayName: 'ChiefA', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 's', role: 'member', displayName: 'Writer', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 'b', role: 'member', displayName: 'BobBot', joinedSeq: 0, joinedAt: 1 },
+ { kind: 'bot', id: 'c', role: 'member', displayName: 'Child', joinedSeq: 0, joinedAt: 1 },
+ ]
+ writeFileSync(first.statePath, JSON.stringify(saved))
+ const next = await start(t, { home: first.home })
+ app = next
+
+ const groupLine = await next.track('s-war', { tasks: [{ title: 'Answer in the group', column: 'in-progress', progress: 10, priority: 'urgent' }] })
+ assert.match(groupLine, /in-progress/)
+ const writer = (await list(next.call, alice.sessionToken, { botId: 's' })).find(task => task.title === 'Answer in the group')
+ assert.equal(writer.origin.roomId, 'war')
+ assert.equal(writer.origin.sessionId, 's-war')
+
+ await next.track('b', { tasks: [{ title: 'Private draft', column: 'not-started' }] })
+ await next.track('b-war', { tasks: [{ title: 'Bob\'s group item', column: 'not-started', priority: 'normal' }] })
+ await next.track('c', { tasks: [{ title: 'Child follow-up', column: 'in-progress', progress: 15 }] })
+
+ const child = await list(next.call, alice.sessionToken, { botId: 'c' })
+ assert.equal(child.length, 1)
+ assert.equal(child[0].title, 'Child follow-up')
+ assert.equal(child[0].ownerBotName, 'Child')
+ const carolChild = await next.call('tasks', { botId: 'c' }, carol.sessionToken)
+ assert.equal(carolChild.ok, false)
+ assert.equal(carolChild.error.code, 'bot/forbidden')
+ const aliceAll = await list(next.call, alice.sessionToken)
+ assert.ok(aliceAll.some(task => task.id === child[0].id), 'a parent\'s owner sees the descendant without a group query')
+ assert.equal(aliceAll.some(task => task.title === 'Private draft' || task.title === 'Bob\'s group item'), false)
+
+ const roomTasks = await list(next.call, alice.sessionToken, { roomId: 'war' })
+ assert.deepEqual(roomTasks.map(task => task.title).sort(), ['Answer in the group', 'Bob\'s group item'])
+ const carolRoom = await list(next.call, carol.sessionToken, { roomId: 'war' })
+ assert.deepEqual(carolRoom.map(task => task.title).sort(), ['Answer in the group', 'Bob\'s group item'])
+ const bobRoom = await next.call('tasks', { roomId: 'war' }, bob.sessionToken)
+ assert.equal(bobRoom.ok, false)
+ assert.equal(bobRoom.error.code, 'bot/forbidden')
+ const bobOwn = await list(next.call, bob.sessionToken, { botId: 'b' })
+ assert.deepEqual(bobOwn.map(task => task.title).sort(), ['Bob\'s group item', 'Private draft'])
+ const bobAll = await list(next.call, bob.sessionToken)
+ assert.equal(bobAll.some(task => task.ownerBotId === 's'), false, 'a non-admin does not see another Bot\'s group task')
+
+ const edited = await next.call('task-update', { id: roomTasks.find(task => task.title === 'Bob\'s group item').id, column: 'done' }, alice.sessionToken)
+ assert.equal(edited.ok, false)
+ assert.equal(edited.error.code, 'bot/forbidden')
+ const carolEdit = await next.call('task-update', { id: writer.id, progress: 90 }, carol.sessionToken)
+ assert.equal(carolEdit.ok, false)
+ assert.equal(carolEdit.error.code, 'bot/forbidden')
+ assert.equal((await list(next.call, alice.sessionToken, { botId: 's' })).find(task => task.id === writer.id).progress, 10)
+
+ const stolen = await next.track('s', { tasks: [{ id: bobOwn.find(task => task.title === 'Private draft').id, column: 'done' }] })
+ assert.match(stolen, /not yours/)
+ assert.equal((await list(next.call, bob.sessionToken, { botId: 'b' })).find(task => task.title === 'Private draft').status, 'open')
+})
diff --git a/patches/ds-bot/files/test/kanban.test.js b/patches/ds-bot/files/test/kanban.test.js
new file mode 100644
index 000000000..ca2f8fff1
--- /dev/null
+++ b/patches/ds-bot/files/test/kanban.test.js
@@ -0,0 +1,134 @@
+// The task board (#1058): three columns, a Bot rail that nests children, and a
+// group rail limited to groups the viewer administers. Moving a card is the
+// owner's action; another Bot's card in a group view is visible and not movable.
+import assert from 'node:assert/strict'
+import { test } from 'node:test'
+import { fileURLToPath } from 'node:url'
+import { runInNewContext } from 'node:vm'
+import { build } from 'esbuild'
+
+const bundle = await build({
+ entryPoints: [fileURLToPath(new URL('../src/client/kanban.js', import.meta.url))],
+ bundle: true, format: 'cjs', platform: 'node', write: false,
+ external: ['react', 'react-dom'],
+})
+const module = { exports: {} }
+runInNewContext(bundle.outputFiles[0].text, {
+ module, exports: module.exports, setTimeout, clearTimeout, setInterval, clearInterval, Date, Promise,
+ require: (name) => {
+ const platform = {
+ react: {
+ createElement: (type, props, ...children) => ({ type, props: props ?? {}, children: children.flat(Infinity) }),
+ useState: initial => [typeof initial === 'function' ? initial() : initial, () => {}],
+ useEffect: () => {}, useCallback: fn => fn,
+ },
+ }
+ if (!(name in platform)) throw new Error(`no platform module ${name}`)
+ return platform[name]
+ },
+})
+const { BoardView, boardColumns, boardRail, canMoveTask, shortId } = module.exports
+
+const expand = (node) => {
+ if (Array.isArray(node)) return node.flatMap(expand)
+ if (node === null || typeof node !== 'object') return node
+ if (typeof node.type === 'function') return expand(node.type({ ...node.props, children: node.children }))
+ return { ...node, children: node.children.map(expand) }
+}
+const all = (node, out = []) => {
+ if (Array.isArray(node)) { for (const item of node) all(item, out); return out }
+ if (node && typeof node === 'object') { out.push(node); all(node.children, out) }
+ return out
+}
+const textOf = node => (typeof node === 'string' ? node : node && typeof node === 'object' ? node.children.map(textOf).join('') : '')
+const byClass = (tree, name) => all(tree).filter(node => typeof node.props?.className === 'string' && node.props.className.split(' ').includes(name))
+
+const roster = {
+ self: { memberId: 'm1' },
+ bots: [
+ { id: 'a', name: 'Chief', ownerId: 'm1', createdAt: 1 },
+ { id: 's', name: 'Writer', ownerId: 'm1', createdBy: 'a', createdAt: 2 },
+ { id: 'b', name: 'Other', ownerId: 'm2', createdAt: 3 },
+ ],
+ rooms: [
+ { id: 'war', name: 'War', admin: 'a', participants: [{ kind: 'human', id: 'm1', role: 'member' }] },
+ { id: 'ops', name: 'Ops', admin: 'b', participants: [{ kind: 'human', id: 'm1', role: 'member' }] },
+ { id: 'face', name: 'Face', origin: 'face-to-face', admin: 'a', participants: [{ kind: 'human', id: 'm1', role: 'admin' }] },
+ ],
+}
+roster.byId = Object.fromEntries(roster.bots.map(bot => [bot.id, bot]))
+roster.roomsById = Object.fromEntries(roster.rooms.map(room => [room.id, room]))
+
+const task = (id, status, extra) => ({ id, status, title: `Task ${id}`, ownerBotId: 's', priority: 'normal', progress: 0, updatedAt: 1, ...extra })
+const tasks = [
+ task('task-abcdef123456', 'open', { title: 'Draft the note', priority: 'high', progress: 0, updatedAt: 2 }),
+ task('task-ffffff999999', 'working', { title: 'Ship the note', priority: 'urgent', progress: 40, note: 'Halfway', updatedAt: 3 }),
+ task('task-000000aaaaaa', 'accepted', { title: 'Old note', ownerBotId: 'a', priority: 'low', progress: 100, updatedAt: 1 }),
+ task('task-bbbbbbcccccc', 'open', { title: 'Theirs', ownerBotId: 'b', ownerBotName: 'Other', updatedAt: 4 }),
+]
+
+test('columns follow the task state and a card carries id, priority and progress', () => {
+ const columns = boardColumns(tasks.filter(item => item.ownerBotId === 's' || item.ownerBotId === 'a'))
+ assert.equal(columns['not-started'].map(item => item.title).join('|'), 'Draft the note')
+ assert.equal(columns['in-progress'].map(item => item.title).join('|'), 'Ship the note')
+ assert.equal(columns.done.map(item => item.title).join('|'), 'Old note')
+ assert.equal(shortId('task-abcdef123456'), '#123456')
+ const rail = boardRail(roster)
+ // Names come back from the bundled module's realm; join avoids deepEqual's cross-realm miss.
+ assert.equal(rail.bots.map(row => `${row.name}:${row.depth}`).join('|'), 'Chief:0|Writer:1')
+ assert.equal(rail.groups.map(row => row.name).join('|'), 'War')
+ assert.equal(canMoveTask(roster, tasks[0]), true)
+ assert.equal(canMoveTask(roster, tasks[3]), false)
+})
+
+test('the board draws the three columns, nests the child, and moves only the owner\'s card', () => {
+ const moved = []
+ const tree = expand({
+ type: BoardView,
+ props: {
+ roster,
+ tasks: tasks.filter(item => item.ownerBotId === 's'),
+ rail: boardRail(roster),
+ scope: { kind: 'bot', botId: 's' },
+ counts: { s: { done: 0, total: 1 }, a: { done: 1, total: 1 } },
+ dragging: 'task-abcdef123456',
+ onMove: (id, column) => moved.push([id, column]),
+ onCreate: () => {},
+ onSelect: () => {},
+ },
+ children: [],
+ })
+ const columns = byClass(tree, 'bt-board-column').map(node => node.props['data-column'])
+ assert.deepEqual(columns, ['not-started', 'in-progress', 'done'])
+ assert.match(textOf(tree), /Draft the note/)
+ assert.match(textOf(tree), /#123456/)
+ assert.match(textOf(tree), /High/)
+ assert.match(textOf(tree), /40%/)
+ assert.match(textOf(tree), /Halfway/)
+ assert.match(textOf(tree), /Start task/)
+ assert.match(textOf(tree), /Complete task/)
+ const writer = all(tree).find(node => node.props?.['data-id'] === 's')
+ assert.equal(writer.props.style.paddingLeft, '24px')
+ assert.equal(all(tree).some(node => node.props?.['data-id'] === 'ops'), false)
+ const start = all(tree).find(node => node.type === 'button' && textOf(node) === 'Start')
+ start.props.onClick()
+ assert.deepEqual(moved, [['task-abcdef123456', 'in-progress']])
+ const progress = byClass(tree, 'bt-board-column').find(node => node.props['data-column'] === 'in-progress')
+ progress.props.onDrop({ preventDefault() {}, dataTransfer: { getData: () => 'task-abcdef123456' } })
+ assert.deepEqual(moved[1], ['task-abcdef123456', 'in-progress'])
+ const group = expand({
+ type: BoardView,
+ props: {
+ roster,
+ tasks: [tasks[3]],
+ rail: boardRail(roster),
+ scope: { kind: 'group', roomId: 'war' },
+ onMove: () => moved.push('group'),
+ },
+ children: [],
+ })
+ assert.match(textOf(group), /Theirs/)
+ assert.match(textOf(group), /Other/)
+ assert.equal(all(group).some(node => node.type === 'button' && (textOf(node) === 'Start' || textOf(node) === 'Complete')), false)
+ assert.equal(byClass(group, 'bt-board-card')[0].props.draggable, undefined)
+})