diff --git a/changelog.d/1828-project-roles.added.md b/changelog.d/1828-project-roles.added.md
new file mode 100644
index 000000000..45c8d3b8e
--- /dev/null
+++ b/changelog.d/1828-project-roles.added.md
@@ -0,0 +1 @@
+Projects gain the three roles the governance matrix names, and a way in: the Owner is derived from `ownerUserIds` (an older record without `roles` reads owner-only), `project-role` sets a Maintainer or Member only after the host re-checks who may re-role whom, refusing `not-owner` / `cannot-self-promote` / `last-owner` without touching the record. The sidebar's **Projects** row now expands to the Projects you can see with your role there and opens that board straight, and its right-click **My role** menu offers only the ranks the host says you may switch to.
diff --git a/core/client.js b/core/client.js
index 75ffa7f16..35b4e7f39 100644
--- a/core/client.js
+++ b/core/client.js
@@ -1376,6 +1376,16 @@ var ZH = {
"e.g. Website": "例如:官网",
"Optional. Where this Project's work happens.": "可选。这个项目的工作目录。",
"Projects · {count}": "项目 · {count}",
+ // #1828: a Project's roles, and the sidebar row that lists them.
+ Maintainer: "维护者",
+ Member: "成员",
+ "No Projects yet": "还没有项目",
+ "Reading Projects…": "正在读取项目…",
+ "My role": "我的角色",
+ "No role to switch here.": "这里没有可切换的角色。",
+ "Role changed.": "角色已更改。",
+ "Role unchanged: {reason}": "角色未更改:{reason}",
+ refused: "被拒绝",
"No Project yet — create one.": "还没有项目——新建一个。",
"Bind a Bot or group": "绑定 Bot 或群聊",
"Bind to {name}": "绑定到 {name}",
@@ -2224,8 +2234,8 @@ function bodyOf(def, paint, flat = false) {
return "";
}
}
-var eyeMarkup = ([cx, cy, w, h49, tilt]) => ``;
-var happyEyeMarkup = ([cx, cy, w, h49, tilt]) => ``;
+var eyeMarkup = ([cx, cy, w, h50, tilt]) => ``;
+var happyEyeMarkup = ([cx, cy, w, h50, 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%";
@@ -3035,29 +3045,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: h49, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) {
- if (!preview || typeof h49 !== "function") return null;
+function RoomInvitePreview({ h: h50, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) {
+ if (!preview || typeof h50 !== "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 h49(
+ return h50(
"div",
{ className: "fs-preview", "data-flybot": at, role: "status" },
- h49("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"),
- preview.lead ? h49("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null,
- preview.people ? h49("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null,
- preview.expiresAt ? h49("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null,
- onJoin || onClear ? h49(
+ h50("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"),
+ preview.lead ? h50("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null,
+ preview.people ? h50("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null,
+ preview.expiresAt ? h50("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null,
+ onJoin || onClear ? h50(
"div",
{ className: "fs-preview-foot", "data-flybot": child("foot") },
- showClear && onClear ? h49("button", {
+ showClear && onClear ? h50("button", {
type: "button",
className: "fs-btn fs-btn-sm",
"data-flybot": child("clear"),
disabled: !!busy,
onClick: onClear
}, clearLabel) : null,
- onJoin ? h49("button", {
+ onJoin ? h50("button", {
type: "button",
className: "fs-btn fs-btn-sm fs-btn-primary",
"data-flybot": child("join"),
@@ -3687,12 +3697,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 h49 = dt / steps;
+ const h50 = 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) * h49;
- value.x += value.v * h49;
+ value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h50;
+ value.x += value.v * h50;
}
if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) {
value.x = value.to;
@@ -7089,12 +7099,12 @@ 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;
+ const visit = (call5) => {
+ const id = call5.callId ?? call5;
if (seen.has(id)) return;
seen.add(id);
- calls.push(call4);
- for (const child of call4.subCalls ?? []) visit(child);
+ calls.push(call5);
+ for (const child of call5.subCalls ?? []) visit(child);
};
visit(root);
return calls;
@@ -7108,20 +7118,20 @@ function useShownCards(actions, sessionId, callIds) {
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);
+function renderCall(call5, { sessionId, roster, actions, useUi, findBot }) {
+ const tool = toolName(call5);
+ const done = call5.kind === "tool-result";
+ const { text: arg, value: argValue } = toolArgs(call5);
const self = roster.byId[sessionId];
switch (tool) {
case "message_bot": {
const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" };
- if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
+ if (done && call5.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
return (0, import_react9.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
}
case "stop_bot": {
const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" };
- if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
+ if (done && call5.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
return (0, import_react9.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
}
case "create_bot": {
@@ -7134,7 +7144,7 @@ function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) {
return done && target ? (0, import_react9.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null;
}
case "group_relay":
- return (0, import_react9.createElement)(GroupReplies, { sessionId, callId: call4.callId, done, content: call4.content, roster, actions, useUi });
+ return (0, import_react9.createElement)(GroupReplies, { sessionId, callId: call5.callId, done, content: call5.content, roster, actions, useUi });
case "create_group":
case "update_group":
case "delete_group":
@@ -7146,25 +7156,25 @@ function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) {
post_to_group: ["Posted in", "Couldn't post in the group"]
}[tool];
if (!done) return null;
- const result = contentText(call4.content);
+ const result = contentText(call5.content);
const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result);
- if (call4.isError || !named) return (0, import_react9.createElement)("div", { className: "bt-event", title: result }, t(verbs[1]));
+ if (call5.isError || !named) return (0, import_react9.createElement)("div", { className: "bt-event", title: result }, t(verbs[1]));
const name = named[2];
const room2 = roster.rooms.find((entry) => entry.name === name);
return (0, import_react9.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions });
}
case "ask_user": {
const question = roster.questions?.[sessionId];
- if (!question || question.callId !== call4.callId) return (0, import_react9.createElement)("span", { className: "bt-q-gone", hidden: true });
+ if (!question || question.callId !== call5.callId) return (0, import_react9.createElement)("span", { className: "bt-q-gone", hidden: true });
return (0, import_react9.createElement)(PendingQuestion, { sessionId, question, actions, inline: true });
}
case "request_secret":
- return (0, import_react9.createElement)(SecretToolCell, { sessionId, callId: call4.callId, name: arg("name"), done, content: call4.content, roster, actions });
+ return (0, import_react9.createElement)(SecretToolCell, { sessionId, callId: call5.callId, name: arg("name"), done, content: call5.content, roster, actions });
case "ask_user_question": {
if (!done) return null;
let parsed = null;
try {
- parsed = JSON.parse(contentText(call4.content));
+ parsed = JSON.parse(contentText(call5.content));
} catch {
parsed = null;
}
@@ -7180,7 +7190,7 @@ function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) {
}
case "remember":
case "forget":
- return done && !call4.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null;
+ return done && !call5.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call5.content), roster, actions, findBot }) : null;
default:
return null;
}
@@ -7192,14 +7202,14 @@ function ToolCell({ node, sessionId, useRoster, useUi, actions }) {
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((call4) => DRAWN.has(toolName(call4)));
- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call4) => call4.kind !== "tool-result");
+ const drawn = calls.filter((call5) => DRAWN.has(toolName(call5)));
+ const running = root.kind === "tool-result" ? void 0 : calls.findLast((call5) => call5.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((call4) => CARDS.has(toolName(call4)) && call4.callId).map((call4) => call4.callId));
+ useShownCards(actions, sessionId, drawn.filter((call5) => CARDS.has(toolName(call5)) && call5.callId).map((call5) => call5.callId));
if (drawn.length === 0) return null;
const env = { sessionId, roster, actions, useUi, findBot };
- return (0, import_react9.createElement)(import_react9.Fragment, null, drawn.map((call4, index) => (0, import_react9.createElement)(import_react9.Fragment, { key: call4.callId ?? index }, renderCall(call4, env))));
+ return (0, import_react9.createElement)(import_react9.Fragment, null, drawn.map((call5, index) => (0, import_react9.createElement)(import_react9.Fragment, { key: call5.callId ?? index }, renderCall(call5, env))));
}
// src/client/awaiting.js
@@ -9135,23 +9145,231 @@ var KANBAN_CSS = `
}
`;
-// src/client/sidebar.js
+// src/client/projects-nav.js
var import_react16 = require("react");
var import_react_dom3 = require("react-dom");
+var call4 = (endpoint, payload) => call(`flybot/${endpoint}`, payload);
+var PROJECTS_NAV_CSS = `
+.bt-projects-row { display: flex; align-items: center; gap: 8px; }
+.bt-projects-chevron { margin-left: auto; display: inline-flex; transition: transform 120ms ease; }
+.bt-projects-row[aria-expanded="true"] .bt-projects-chevron { transform: rotate(90deg); }
+.bt-projects-list { display: flex; flex-direction: column; margin: 0 0 4px; padding-left: 30px; }
+.bt-project-entry { display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left; border: 0; background: none; color: inherit; font: inherit; padding: 4px 8px; border-radius: 6px; cursor: pointer; }
+.bt-project-entry:hover, .bt-project-entry:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; }
+.bt-project-entry[aria-current="true"] { background: var(--bt-hover, rgba(127,127,127,.14)); }
+.bt-project-entry-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.bt-project-entry-role { margin-left: auto; opacity: .62; font-size: .85em; white-space: nowrap; }
+.bt-projects-empty, .bt-projects-note { padding: 2px 8px 6px; opacity: .7; font-size: .85em; }
+.bt-project-menu { position: fixed; z-index: 60; min-width: 168px; padding: 4px; border-radius: 8px; background: var(--bt-menu-bg, #1f2126); border: 1px solid var(--bt-menu-border, rgba(255,255,255,.12)); box-shadow: 0 8px 24px rgba(0,0,0,.32); }
+.bt-project-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: none; color: inherit; font: inherit; text-align: left; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
+.bt-project-menu-item:hover, .bt-project-menu-item:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; }
+.bt-project-menu-item[aria-checked="true"]::before { content: "✓"; margin-right: 2px; }
+.bt-project-menu-item:not([aria-checked="true"])::before { content: " "; margin-right: 2px; }
+.bt-project-menu-item[disabled] { opacity: .6; cursor: default; }
+`;
+var ROLE_LABELS = { owner: "Owner", maintainer: "Maintainer", member: "Member" };
+function roleText(role) {
+ if (role === null || role === void 0 || role === "") return "";
+ return t(ROLE_LABELS[role] ?? String(role));
+}
+function projectHash(id) {
+ return `#project=${encodeURIComponent(String(id ?? ""))}`;
+}
+function projectName(project) {
+ const name = typeof project?.name === "string" ? project.name.trim() : "";
+ return name === "" ? `#${project?.id ?? ""}` : name;
+}
+function projectMenuItems(project) {
+ const current = project?.role ?? null;
+ const choices = Array.isArray(project?.choices) ? project.choices : [];
+ const items = current === null ? [] : [{ role: current, label: roleText(current), current: true, pick: false }];
+ for (const role of choices) {
+ if (role === current) continue;
+ items.push({ role, label: roleText(role), current: false, pick: true });
+ }
+ return items;
+}
+function roleNote(answer) {
+ if (answer?.changed === true) return t("Role changed.");
+ const reason = typeof answer?.reason === "string" && answer.reason !== "" ? answer.reason : t("refused");
+ return t("Role unchanged: {reason}", { reason });
+}
+function ProjectList({ projects, error, note, onOpen, onRole }) {
+ const rows = projects === null ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, t("Reading Projects…")) : error !== "" ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, error) : projects.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-projects-empty" }, t("No Projects yet")) : projects.map((project) => (0, import_react16.createElement)(
+ "button",
+ {
+ key: String(project.id),
+ type: "button",
+ className: "bt-project-entry",
+ "data-project-id": String(project.id),
+ "aria-current": project.current === true ? "true" : void 0,
+ title: roleText(project.role) === "" ? projectName(project) : `${projectName(project)} · ${roleText(project.role)}`,
+ onClick: () => onOpen?.(project.id),
+ onContextMenu: (event) => {
+ event.preventDefault();
+ onRole?.(project, event);
+ }
+ },
+ (0, import_react16.createElement)("span", { className: "bt-project-entry-name" }, projectName(project)),
+ roleText(project.role) === "" ? null : (0, import_react16.createElement)("span", { className: "bt-project-entry-role" }, roleText(project.role))
+ ));
+ return (0, import_react16.createElement)(
+ "div",
+ { className: "bt-projects-list", role: "group", "aria-label": t("Projects") },
+ rows,
+ note === "" || note === void 0 ? null : (0, import_react16.createElement)("div", { className: "bt-projects-note", role: "status" }, note)
+ );
+}
+function Chevron() {
+ return (0, import_react16.createElement)(
+ "svg",
+ { width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
+ (0, import_react16.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })
+ );
+}
+function ProjectsMark() {
+ return (0, import_react16.createElement)(
+ "svg",
+ { width: 18, height: 18, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinejoin: "round", "aria-hidden": true },
+ (0, import_react16.createElement)("path", { d: "M2.5 5.5h6l1.5 2h7.5v8a1 1 0 0 1-1 1h-13a1 1 0 0 1-1-1z" }),
+ (0, import_react16.createElement)("path", { d: "M2.5 5.5v-1a1 1 0 0 1 1-1h4l1.5 2" })
+ );
+}
+function ProjectRoleMenu({ project, x, y, onPick, onClose }) {
+ const ref = (0, import_react16.useRef)(null);
+ (0, import_react16.useEffect)(() => {
+ const onDown = (event) => {
+ if (!ref.current?.contains(event.target)) onClose();
+ };
+ const onKey = (event) => {
+ if (event.key === "Escape") onClose();
+ };
+ window.addEventListener("mousedown", onDown, true);
+ window.addEventListener("keydown", onKey, true);
+ return () => {
+ window.removeEventListener("mousedown", onDown, true);
+ window.removeEventListener("keydown", onKey, true);
+ };
+ }, [onClose]);
+ const items = projectMenuItems(project);
+ return (0, import_react_dom3.createPortal)((0, import_react16.createElement)(
+ "div",
+ {
+ ref,
+ className: "bt-project-menu",
+ role: "menu",
+ "aria-label": t("My role"),
+ style: { left: `${x}px`, top: `${y}px` },
+ "data-project-id": String(project?.id ?? "")
+ },
+ (0, import_react16.createElement)("div", { className: "bt-projects-note" }, projectName(project)),
+ items.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, t("No role to switch here.")) : items.map((item) => (0, import_react16.createElement)("button", {
+ key: item.role,
+ type: "button",
+ role: "menuitemradio",
+ className: "bt-project-menu-item",
+ "aria-checked": item.current ? "true" : "false",
+ disabled: !item.pick,
+ onClick: () => {
+ if (item.pick) onPick(item.role);
+ else onClose();
+ }
+ }, item.label))
+ ), document.body);
+}
+function ProjectsNav({ wide = true, request, onOpen } = {}) {
+ const read = request ?? call4;
+ const open = (0, import_react16.useCallback)((id) => {
+ if (onOpen) {
+ onOpen(id);
+ return;
+ }
+ if (typeof window !== "undefined") window.location.hash = projectHash(id);
+ }, [onOpen]);
+ const [expanded, setExpanded] = (0, import_react16.useState)(false);
+ const [projects, setProjects] = (0, import_react16.useState)(null);
+ const [error, setError] = (0, import_react16.useState)("");
+ const [note, setNote] = (0, import_react16.useState)("");
+ const [menu, setMenu] = (0, import_react16.useState)(null);
+ const load = (0, import_react16.useCallback)(async () => {
+ try {
+ const listing = await read("projects");
+ const list = Array.isArray(listing?.projects) ? listing.projects : [];
+ setProjects(list);
+ setError("");
+ } catch (failure) {
+ setProjects([]);
+ setError(failure?.message ?? String(failure));
+ }
+ }, [read]);
+ (0, import_react16.useEffect)(() => {
+ if (!expanded) return;
+ void load();
+ }, [expanded, load]);
+ const toggle = () => {
+ setNote("");
+ if (!wide) {
+ void open("");
+ return;
+ }
+ setExpanded((value) => !value);
+ };
+ const listed = expanded && wide;
+ return (0, import_react16.createElement)(
+ "div",
+ { className: "bt-projects" },
+ (0, import_react16.createElement)(
+ "button",
+ {
+ type: "button",
+ className: "bt-row bt-projects-row",
+ "aria-expanded": listed ? "true" : "false",
+ "aria-label": t("Projects"),
+ title: wide ? void 0 : t("Projects"),
+ onClick: toggle
+ },
+ (0, import_react16.createElement)("span", { className: "bt-projects-mark" }, (0, import_react16.createElement)(ProjectsMark)),
+ wide ? (0, import_react16.createElement)("span", { className: "bt-projects-label" }, t("Projects")) : null,
+ wide ? (0, import_react16.createElement)("span", { className: "bt-projects-chevron", "aria-hidden": true }, (0, import_react16.createElement)(Chevron)) : null
+ ),
+ listed ? (0, import_react16.createElement)(ProjectList, { projects, error, note, onOpen: open, onRole: (project, event) => setMenu({ project, x: event.clientX, y: event.clientY }) }) : null,
+ menu === null ? null : (0, import_react16.createElement)(ProjectRoleMenu, {
+ project: menu.project,
+ x: menu.x,
+ y: menu.y,
+ onClose: () => setMenu(null),
+ onPick: async (role) => {
+ const project = menu.project;
+ setMenu(null);
+ try {
+ const answer = await read("project-role", { id: project.id, role });
+ setNote(roleNote(answer));
+ if (answer?.changed === true) void load();
+ } catch (failure) {
+ setNote(failure?.message ?? String(failure));
+ }
+ }
+ })
+ );
+}
+
+// src/client/sidebar.js
+var import_react17 = require("react");
+var import_react_dom4 = require("react-dom");
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
function useMarkedUnread(id, active, marked, actions) {
- (0, import_react16.useEffect)(() => {
+ (0, import_react17.useEffect)(() => {
if (active && marked) actions.clearUnread(id);
}, [active, marked]);
}
-var PinMark = () => (0, import_react16.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react16.createElement)(PinMarkIcon));
+var PinMark = () => (0, import_react17.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react17.createElement)(PinMarkIcon));
var useApproval = (useWork, match) => useWork ? useWork((work) => (work.approvals ?? []).some(match)) : false;
var useGroupWork = (useWork, pick2) => useWork ? useWork((work) => pick2(work.rooms ?? {}).join(",")) : "";
var workKeys = (text2) => text2 === "" ? [] : text2.split(",").map((key) => {
const [id, tool] = key.split("|");
return { id, tool: tool || void 0 };
});
-var workLine = (lead, tool, more) => (0, import_react16.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`);
+var workLine = (lead, tool, more) => (0, import_react17.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`);
function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked, asRow = false }) {
const id = bot.id;
const part = currentPart(bot);
@@ -9182,7 +9400,7 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
const secondLine = awayLine || preview || (remote ? bot.remote?.gone ? t("no longer shared") : t("Friend's Bot") : "");
const label = [shown, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", ");
if (tile && wide) {
- return (0, import_react16.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react16.createElement)(
+ return (0, import_react17.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react17.createElement)(
"button",
{
type: "button",
@@ -9194,15 +9412,15 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
onClick: () => actions.openSession(id),
onContextMenu
},
- (0, import_react16.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
- (0, import_react16.createElement)("span", { className: "bt-tile-name" }, shown),
+ (0, import_react17.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
+ (0, import_react17.createElement)("span", { className: "bt-tile-name" }, shown),
// The Main Bot's tile gets the same unread dot as a row (#420); a wait shows on its avatar.
- unread && !waiting && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null,
- bot.role ? (0, import_react16.createElement)("span", { className: "bt-chip" }, bot.role) : null
+ unread && !waiting && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null,
+ bot.role ? (0, import_react17.createElement)("span", { className: "bt-chip" }, bot.role) : null
));
}
- const unreadMark = wide && waiting && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null;
- const row = (0, import_react16.createElement)(
+ const unreadMark = wide && waiting && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null;
+ const row = (0, import_react17.createElement)(
"button",
{
type: "button",
@@ -9215,24 +9433,24 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
onClick: () => actions.openSession(id),
onContextMenu
},
- (0, import_react16.createElement)(BotAvatar, { bot, size: 36, state, main }),
- pinned && !wide ? (0, import_react16.createElement)(PinMark) : null,
- wide ? (0, import_react16.createElement)(
+ (0, import_react17.createElement)(BotAvatar, { bot, size: 36, state, main }),
+ pinned && !wide ? (0, import_react17.createElement)(PinMark) : null,
+ wide ? (0, import_react17.createElement)(
"span",
{ className: "bt-row-body" },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"span",
{ className: "bt-row-name" },
- (0, import_react16.createElement)("span", { className: "bt-row-title" }, shown),
- bot.remote ? (0, import_react16.createElement)(RemoteMark, { remote: bot.remote }) : null,
- pinned ? (0, import_react16.createElement)(PinMark) : null
+ (0, import_react17.createElement)("span", { className: "bt-row-title" }, shown),
+ bot.remote ? (0, import_react17.createElement)(RemoteMark, { remote: bot.remote }) : null,
+ pinned ? (0, import_react17.createElement)(PinMark) : null
),
- secondLine ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, secondLine) : null
+ secondLine ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, secondLine) : null
) : null,
unreadMark
);
const settingsLabel = bot.remote ? t("Bot settings") : t("Edit Bot");
- const settings = wide ? (0, import_react16.createElement)("button", {
+ const settings = wide ? (0, import_react17.createElement)("button", {
type: "button",
className: "bt-row-settings",
"aria-label": settingsLabel,
@@ -9243,8 +9461,8 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
keepSidebarStill(event.currentTarget);
actions.openSettings("bots", id);
}
- }, (0, import_react16.createElement)(GearIcon)) : null;
- if (settings) return (0, import_react16.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
+ }, (0, import_react17.createElement)(GearIcon)) : null;
+ if (settings) return (0, import_react17.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
return row;
}
function keepSidebarStill(node) {
@@ -9257,14 +9475,14 @@ function keepSidebarStill(node) {
if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset));
}
function RemoteMark({ remote }) {
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"span",
{ className: "bt-row-remote", title: remote.gone ? `${remote.handle} · ${t("no longer shared")}` : remote.handle, "aria-label": t("Friend's Bot") },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"svg",
{ width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
- (0, import_react16.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
- (0, import_react16.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
+ (0, import_react17.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
+ (0, import_react17.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
)
);
}
@@ -9276,15 +9494,15 @@ function isPastFaceRoom(room2) {
return status === "candidate" && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now();
}
function FaceMark() {
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"span",
{ className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"svg",
{ width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
- (0, import_react16.createElement)("circle", { cx: 9, cy: 8, r: 3 }),
- (0, import_react16.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }),
- (0, import_react16.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" })
+ (0, import_react17.createElement)("circle", { cx: 9, cy: 8, r: 3 }),
+ (0, import_react17.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }),
+ (0, import_react17.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" })
)
);
}
@@ -9309,7 +9527,7 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus,
const alert = question || waiting;
const face = room2.origin === "face-to-face";
const label = [hover, face ? t("Face-to-face") : "", pinned ? t("Pinned") : "", members.length > 0 ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : ""].filter(Boolean).join(", ");
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"button",
{
type: "button",
@@ -9326,20 +9544,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus,
actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY });
}
},
- (0, import_react16.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
- pinned && !wide ? (0, import_react16.createElement)(PinMark) : null,
- wide ? (0, import_react16.createElement)(
+ (0, import_react17.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
+ pinned && !wide ? (0, import_react17.createElement)(PinMark) : null,
+ wide ? (0, import_react17.createElement)(
"span",
{ className: "bt-row-body" },
- (0, import_react16.createElement)("span", { className: "bt-row-name" }, (0, import_react16.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react16.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react16.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react16.createElement)(PinMark) : null),
- workingLine ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, preview) : null
+ (0, import_react17.createElement)("span", { className: "bt-row-name" }, (0, import_react17.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react17.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react17.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react17.createElement)(PinMark) : null),
+ workingLine ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, preview) : null
) : null,
- wide && alert && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
+ wide && alert && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
);
}
function AgentSessionRow({ session, agent, actions, renaming, index }) {
- const input = (0, import_react16.useRef)(null);
- (0, import_react16.useLayoutEffect)(() => {
+ const input = (0, import_react17.useRef)(null);
+ (0, import_react17.useLayoutEffect)(() => {
if (!renaming) return;
input.current?.focus();
input.current?.select();
@@ -9360,10 +9578,10 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
}
})();
};
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"div",
{ className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } },
- (0, import_react16.createElement)("input", {
+ (0, import_react17.createElement)("input", {
ref: input,
defaultValue: session.title,
"aria-label": t("Rename"),
@@ -9376,7 +9594,7 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
})
);
}
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"button",
{
type: "button",
@@ -9390,8 +9608,8 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) {
actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY });
}
},
- (0, import_react16.createElement)("span", { className: "bt-agent-sub-title" }, session.title),
- (0, import_react16.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "")
+ (0, import_react17.createElement)("span", { className: "bt-agent-sub-title" }, session.title),
+ (0, import_react17.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "")
);
}
function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) {
@@ -9426,10 +9644,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
const preview = sessions[0]?.title ?? t("New session");
const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", ");
const open = active && wide;
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"div",
{ className: "bt-agent" },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"button",
{
type: "button",
@@ -9444,30 +9662,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY });
}
},
- (0, import_react16.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react16.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })),
- wide ? (0, import_react16.createElement)(
+ (0, import_react17.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react17.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })),
+ wide ? (0, import_react17.createElement)(
"span",
{ className: "bt-row-body" },
- (0, import_react16.createElement)("span", { className: "bt-row-name" }, "DSH Agent"),
- preview ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, preview) : null
+ (0, import_react17.createElement)("span", { className: "bt-row-name" }, "DSH Agent"),
+ preview ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, preview) : null
) : null,
- wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
+ wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
),
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"div",
{ className: "bt-agent-sub", "data-open": open || void 0 },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"div",
{ className: "bt-agent-sub-in" },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"button",
{ type: "button", className: "bt-agent-sub-row bt-agent-new", style: { "--i": 0 }, onClick: () => {
void actions.newAgentSession(agent.id).catch((error) => actions.reportError(error, "agent-session"));
} },
- (0, import_react16.createElement)(PlusIcon),
- (0, import_react16.createElement)("span", { className: "bt-agent-sub-title" }, t("New session"))
+ (0, import_react17.createElement)(PlusIcon),
+ (0, import_react17.createElement)("span", { className: "bt-agent-sub-title" }, t("New session"))
),
- sessions.map((session, index) => (0, import_react16.createElement)(AgentSessionRow, {
+ sessions.map((session, index) => (0, import_react17.createElement)(AgentSessionRow, {
key: session.id,
session,
agent,
@@ -9481,8 +9699,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide,
}
var GLIDE = springEasing(1e3, 63);
function useListGlide(listRef, layout, key) {
- const tops = (0, import_react16.useRef)({ layout, map: /* @__PURE__ */ new Map() });
- (0, import_react16.useLayoutEffect)(() => {
+ const tops = (0, import_react17.useRef)({ layout, map: /* @__PURE__ */ new Map() });
+ (0, import_react17.useLayoutEffect)(() => {
const list = listRef.current;
if (!list) return;
const next = /* @__PURE__ */ new Map();
@@ -9499,7 +9717,7 @@ function useListGlide(listRef, layout, key) {
tops.current = { layout, map: next };
}, [layout, key]);
}
-var NodeActions = class extends import_react16.Component {
+var NodeActions = class extends import_react17.Component {
constructor(props) {
super(props);
this.state = { failed: false };
@@ -9511,32 +9729,32 @@ var NodeActions = class extends import_react16.Component {
console.error("flybot: nodeActions failed", error);
}
render() {
- return this.state.failed ? null : (0, import_react16.createElement)(this.props.component, { bot: this.props.bot });
+ return this.state.failed ? null : (0, import_react17.createElement)(this.props.component, { bot: this.props.bot });
}
};
function TreeNode({ node, rowProps, markedUnread, collapsed, toggle, actions }) {
const { bot, depth, children } = node;
const closed = collapsed[bot.id] === true;
- return (0, import_react16.createElement)(
- import_react16.Fragment,
+ return (0, import_react17.createElement)(
+ import_react17.Fragment,
null,
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"div",
{ className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } },
- children.length ? (0, import_react16.createElement)(
+ children.length ? (0, import_react17.createElement)(
"button",
{ type: "button", className: "bt-tree-toggle", "aria-expanded": !closed, "aria-label": `${closed ? t("Expand") : t("Collapse")} ${bot.name}`, onClick: () => toggle(bot.id) },
- (0, import_react16.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾")
- ) : (0, import_react16.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }),
- (0, import_react16.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }),
- typeof actions === "function" ? (0, import_react16.createElement)(NodeActions, { component: actions, bot }) : null
+ (0, import_react17.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾")
+ ) : (0, import_react17.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }),
+ (0, import_react17.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }),
+ typeof actions === "function" ? (0, import_react17.createElement)(NodeActions, { component: actions, bot }) : null
),
- closed ? null : children.map((child) => (0, import_react16.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions }))
+ closed ? null : children.map((child) => (0, import_react17.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions }))
);
}
function TeamSidebar(props) {
const { wide, useRoster, useUi, useSessions, actions } = props;
- const listRef = (0, import_react16.useRef)(null);
+ const listRef = (0, import_react17.useRef)(null);
const roster = useRoster((value) => value);
const markedUnread = useUi((value) => value.unread);
props.useRegistry((value) => value);
@@ -9545,7 +9763,7 @@ function TeamSidebar(props) {
for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0;
return JSON.stringify(stamps);
});
- const order = (0, import_react16.useMemo)(() => {
+ const order = (0, import_react17.useMemo)(() => {
const stamps = JSON.parse(updated);
const entries2 = [
// Own Bots, friends' Bots, groups and face rooms share one list. A remote chat
@@ -9560,7 +9778,7 @@ function TeamSidebar(props) {
}, [roster, updated]);
const main = roster.byId[roster.mainBotId];
const structure = useSidebarStructure();
- const [collapsed, setCollapsed] = (0, import_react16.useState)({});
+ const [collapsed, setCollapsed] = (0, import_react17.useState)({});
const toggle = (id) => setCollapsed((value) => ({ ...value, [id]: !value[id] }));
const inTree = (entry) => entry.kind === "bot" && !entry.item.remote;
const listed = (entry) => inTree(entry) && structure?.include?.(entry.item.id) !== false;
@@ -9574,42 +9792,43 @@ function TeamSidebar(props) {
event.preventDefault();
actions.openPalette(event.key);
};
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"div",
{ className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch },
- wide ? (0, import_react16.createElement)(
+ wide ? (0, import_react17.createElement)(
"div",
{ className: "bt-side-top" },
- (0, import_react16.createElement)("span", { className: "bt-brand" }, (0, import_react16.createElement)(BrandMark, { size: 22 }), (0, import_react16.createElement)(BrandName)),
- (0, import_react16.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
- (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react16.createElement)(SearchIcon)),
- (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react16.createElement)(PlusIcon))
- ) : (0, import_react16.createElement)(
- import_react16.Fragment,
+ (0, import_react17.createElement)("span", { className: "bt-brand" }, (0, import_react17.createElement)(BrandMark, { size: 22 }), (0, import_react17.createElement)(BrandName)),
+ (0, import_react17.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
+ (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react17.createElement)(SearchIcon)),
+ (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react17.createElement)(PlusIcon))
+ ) : (0, import_react17.createElement)(
+ import_react17.Fragment,
null,
- (0, import_react16.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
- (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react16.createElement)(PlusIcon))
+ (0, import_react17.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
+ (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react17.createElement)(PlusIcon))
),
- wide && roster.readOnly ? (0, import_react16.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
- (0, import_react16.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
- (0, import_react16.createElement)(KanbanRow, { wide, useUi, actions }),
- (0, import_react16.createElement)(
+ wide && roster.readOnly ? (0, import_react17.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
+ (0, import_react17.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
+ (0, import_react17.createElement)(KanbanRow, { wide, useUi, actions }),
+ (0, import_react17.createElement)(ProjectsNav, { wide }),
+ (0, import_react17.createElement)(
"div",
{ ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") },
- tree ? (0, import_react16.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react16.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react16.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react16.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")),
- order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react16.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react16.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react16.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
- wide ? (0, import_react16.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react16.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null
+ tree ? (0, import_react17.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react17.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react17.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react17.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")),
+ order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react17.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react17.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react17.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
+ wide ? (0, import_react17.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react17.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null
)
);
}
function UpdateButton({ useUpdate, actions }) {
const update = useUpdate((value) => value);
- const [open, setOpen] = (0, import_react16.useState)(false);
- const [box, setBox] = (0, import_react16.useState)(null);
- const [error, setError] = (0, import_react16.useState)("");
- const [dshError, setDshError] = (0, import_react16.useState)("");
- const button = (0, import_react16.useRef)(null);
- (0, import_react16.useEffect)(() => {
+ const [open, setOpen] = (0, import_react17.useState)(false);
+ const [box, setBox] = (0, import_react17.useState)(null);
+ const [error, setError] = (0, import_react17.useState)("");
+ const [dshError, setDshError] = (0, import_react17.useState)("");
+ const button = (0, import_react17.useRef)(null);
+ (0, import_react17.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return;
@@ -9652,10 +9871,10 @@ function UpdateButton({ useUpdate, actions }) {
const message = error || (phase === "failed" ? update.error : "");
const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : "");
const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update FlyBot core {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current });
- return (0, import_react16.createElement)(
- import_react16.Fragment,
+ return (0, import_react17.createElement)(
+ import_react17.Fragment,
null,
- (0, import_react16.createElement)("button", {
+ (0, import_react17.createElement)("button", {
ref: button,
type: "button",
className: "bt-round bt-update",
@@ -9665,21 +9884,21 @@ function UpdateButton({ useUpdate, actions }) {
"aria-haspopup": "dialog",
"aria-expanded": open,
onClick: toggle
- }, (0, import_react16.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react16.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
- open && box ? (0, import_react_dom3.createPortal)((0, import_react16.createElement)(
+ }, (0, import_react17.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react17.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
+ open && box ? (0, import_react_dom4.createPortal)((0, import_react17.createElement)(
"div",
{ className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("FlyBot core update") : t("DSH update"), style: box },
- update?.available || phase !== "idle" ? (0, import_react16.createElement)(
- import_react16.Fragment,
+ update?.available || phase !== "idle" ? (0, import_react17.createElement)(
+ import_react17.Fragment,
null,
- (0, import_react16.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })),
- (0, import_react16.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })),
- message ? (0, import_react16.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })),
+ (0, import_react17.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })),
+ message ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
+ (0, import_react17.createElement)(
"div",
{ className: "bt-update-actions" },
- update.notesUrl ? (0, import_react16.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
- phase === "installed" ? null : (0, import_react16.createElement)("button", {
+ update.notesUrl ? (0, import_react17.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
+ phase === "installed" ? null : (0, import_react17.createElement)("button", {
type: "button",
className: "bt-update-go",
disabled: phase === "installing",
@@ -9687,17 +9906,17 @@ function UpdateButton({ useUpdate, actions }) {
}, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update"))
)
) : null,
- dshRow ? (0, import_react16.createElement)(
+ dshRow ? (0, import_react17.createElement)(
"div",
{ className: "bt-update-dsh" },
- (0, import_react16.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
- (0, import_react16.createElement)("div", { className: "bt-update-text" }, dshText),
- dshMessage ? (0, import_react16.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
+ (0, import_react17.createElement)("div", { className: "bt-update-text" }, dshText),
+ dshMessage ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
+ (0, import_react17.createElement)(
"div",
{ className: "bt-update-actions" },
- dsh.kind === "app" ? dsh.available ? (0, import_react16.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => {
- }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react16.createElement)("button", {
+ dsh.kind === "app" ? dsh.available ? (0, import_react17.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => {
+ }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react17.createElement)("button", {
type: "button",
className: "bt-update-go",
disabled: dshPhase === "installing",
@@ -9710,23 +9929,23 @@ function UpdateButton({ useUpdate, actions }) {
}
function ConnectApps({ wide, actions }) {
if (!wide) return null;
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"span",
{ className: "bt-connect-wrap" },
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"button",
{ type: "button", className: "bt-connect bt-connect-foot", onClick: () => actions.openConnectors() },
- (0, import_react16.createElement)(PlugIcon),
+ (0, import_react17.createElement)(PlugIcon),
t("Connect apps")
)
);
}
function YouAvatar() {
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"span",
{ className: "bt-you-wrap" },
- (0, import_react16.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react16.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })),
- (0, import_react16.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings"))
+ (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })),
+ (0, import_react17.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings"))
);
}
var openShellSettings = null;
@@ -9758,7 +9977,7 @@ function openHarnessOnboarding(id) {
var MODELS_STEP_ID = "flybot-models";
var MODELS_STEP_ORDER = 1e3;
function ModelsSettingsStep({ explicit, complete, openSection }) {
- (0, import_react16.useLayoutEffect)(() => {
+ (0, import_react17.useLayoutEffect)(() => {
if (explicit) openSection("models");
complete();
}, []);
@@ -9796,32 +10015,32 @@ function pressSettingsShortcut() {
}));
}
function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) {
- const [open, setOpen] = (0, import_react16.useState)(false);
- (0, import_react16.useLayoutEffect)(() => {
+ const [open, setOpen] = (0, import_react17.useState)(false);
+ (0, import_react17.useLayoutEffect)(() => {
if (!seat) return void 0;
actions.seatAccount(true);
return () => actions.seatAccount(false);
}, [seat]);
- const [box, setBox] = (0, import_react16.useState)(null);
- const button = (0, import_react16.useRef)(null);
- (0, import_react16.useEffect)(() => {
+ const [box, setBox] = (0, import_react17.useState)(null);
+ const button = (0, import_react17.useRef)(null);
+ (0, import_react17.useEffect)(() => {
if (settingsOpen) setOpen(false);
}, [settingsOpen]);
- (0, import_react16.useEffect)(() => {
+ (0, import_react17.useEffect)(() => {
if (!openSettings) return void 0;
openShellSettings = openSettings;
return () => {
if (openShellSettings === openSettings) openShellSettings = null;
};
}, [openSettings]);
- (0, import_react16.useEffect)(() => {
+ (0, import_react17.useEffect)(() => {
if (!openOnboarding) return void 0;
openShellOnboarding = openOnboarding;
return () => {
if (openShellOnboarding === openOnboarding) openShellOnboarding = null;
};
}, [openOnboarding]);
- (0, import_react16.useEffect)(() => {
+ (0, import_react17.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return;
@@ -9858,23 +10077,23 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi
[t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")]
]
];
- return (0, import_react16.createElement)(
- import_react16.Fragment,
+ return (0, import_react17.createElement)(
+ import_react17.Fragment,
null,
- (0, import_react16.createElement)(
+ (0, import_react17.createElement)(
"button",
{ ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle },
- (0, import_react16.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react16.createElement)(BotMark, { look: MAIN_LOOK, size: 24 }))
+ (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 }))
),
- open && box ? (0, import_react_dom3.createPortal)((0, import_react16.createElement)(
+ open && box ? (0, import_react_dom4.createPortal)((0, import_react17.createElement)(
"div",
{ className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box },
- (0, import_react16.createElement)(FlyBotAccountHeader, { actions }),
- groups.map((group, index) => (0, import_react16.createElement)(
- import_react16.Fragment,
+ (0, import_react17.createElement)(FlyBotAccountHeader, { actions }),
+ groups.map((group, index) => (0, import_react17.createElement)(
+ import_react17.Fragment,
{ key: index },
- index > 0 ? (0, import_react16.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
- group.map(([label, Icon, run, hint]) => (0, import_react16.createElement)("button", {
+ index > 0 ? (0, import_react17.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
+ group.map(([label, Icon, run, hint]) => (0, import_react17.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
@@ -9882,7 +10101,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi
setOpen(false);
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react16.createElement)(Icon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react16.createElement)("span", { className: "bt-menu-hint" }, hint) : null))
+ }, (0, import_react17.createElement)(Icon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, hint) : null))
))
), document.body) : null
);
@@ -9892,8 +10111,8 @@ function useLandOnMainBot(roster, useSessions, actions) {
const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0);
return row ? `${row.id}|${row.blank ? 1 : 0}` : "";
});
- const lastBot = (0, import_react16.useRef)(null);
- (0, import_react16.useEffect)(() => {
+ const lastBot = (0, import_react17.useRef)(null);
+ (0, import_react17.useEffect)(() => {
if (!roster.ready || !roster.mainBotId) return void 0;
const [id, blank3] = mainView.split("|");
const bot = id === "" ? void 0 : roster.byId[id];
@@ -9914,16 +10133,16 @@ function useLandOnMainBot(roster, useSessions, actions) {
function FooterAccount(props) {
const seated = props.useUi((value) => value.accountSeated === true);
if (seated) return null;
- return (0, import_react16.createElement)("span", { className: "bt-foot-account" }, (0, import_react16.createElement)(AccountLauncher, { ...props, seat: false }));
+ return (0, import_react17.createElement)("span", { className: "bt-foot-account" }, (0, import_react17.createElement)(AccountLauncher, { ...props, seat: false }));
}
function BotReturn({ wide, useSurface, actions }) {
const mode = useSurface((value) => value.mode);
if (mode !== "agent") return null;
- return (0, import_react16.createElement)(
+ return (0, import_react17.createElement)(
"button",
{ type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") },
- (0, import_react16.createElement)(FlyBotMark, { size: 20 }),
- wide ? (0, import_react16.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
+ (0, import_react17.createElement)(FlyBotMark, { size: 20 }),
+ wide ? (0, import_react17.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
);
}
var RETURN_CSS = `
@@ -9937,10 +10156,10 @@ var RETURN_CSS = `
.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
`;
function BrandMark({ size = 24 }) {
- return (0, import_react16.createElement)("span", { className: "bt-brand-mark" }, (0, import_react16.createElement)(FlyBotMark, { size }));
+ return (0, import_react17.createElement)("span", { className: "bt-brand-mark" }, (0, import_react17.createElement)(FlyBotMark, { size }));
}
function BrandName() {
- return (0, import_react16.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot"));
+ return (0, import_react17.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot"));
}
var SIDEBAR_CSS = `
.bt-side-top .bt-brand{cursor:default;padding-left:6px}
@@ -10040,11 +10259,11 @@ function roomBusyAckVisible({ ack, pendingBusy }) {
}
// src/client/busy-ack.js
-var import_react17 = require("react");
-var import_react_dom4 = require("react-dom");
+var import_react18 = require("react");
+var import_react_dom5 = require("react-dom");
function useRunningHost2(active) {
- const [host, setHost] = (0, import_react17.useState)(null);
- (0, import_react17.useEffect)(() => {
+ const [host, setHost] = (0, import_react18.useState)(null);
+ (0, import_react18.useEffect)(() => {
if (!active) {
setHost(null);
return void 0;
@@ -10078,11 +10297,11 @@ function BusyAckBubble({ sessionId, useRoster, useBusyAcks, useSessionStatus, us
const visible = room2 ? roomBusyAckVisible({ ack: memberAck, pendingBusy }) : running && busyAckVisible({ hostAck, pendingBusy, streamingText });
const bot = room2 ? roster.byId[memberAck?.botId] : roster.byId[sessionId];
const host = useRunningHost2(visible);
- const shown = (0, import_react17.useRef)(null);
+ const shown = (0, import_react18.useRef)(null);
if (visible) shown.current = room2 ? { text: memberAck?.text ?? "", at: memberAck?.at ?? Date.now() } : { text: busyAckText(hostAck), at: hostAck?.at ?? Date.now() };
const text2 = shown.current?.text ?? "";
if (!visible || !host || !shown.current || !bot) return null;
- return (0, import_react_dom4.createPortal)((0, import_react17.createElement)(
+ return (0, import_react_dom5.createPortal)((0, import_react18.createElement)(
"div",
{
className: "bt-busy-ack",
@@ -10092,22 +10311,22 @@ function BusyAckBubble({ sessionId, useRoster, useBusyAcks, useSessionStatus, us
"aria-busy": text2 ? void 0 : "true",
"data-pending": text2 ? void 0 : ""
},
- (0, import_react17.createElement)(BotMark, { bot, size: 28, state: "thinking", live: true }),
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)(BotMark, { bot, size: 28, state: "thinking", live: true }),
+ (0, import_react18.createElement)(
"div",
{ className: "bt-astack" },
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)(
"div",
{ className: "bt-msg" },
- (0, import_react17.createElement)(
+ (0, import_react18.createElement)(
"div",
{ className: "bt-msg-line" },
- text2 ? (0, import_react17.createElement)("div", { className: "bt-bubble" }, text2) : (0, import_react17.createElement)(
+ text2 ? (0, import_react18.createElement)("div", { className: "bt-bubble" }, text2) : (0, import_react18.createElement)(
"div",
{ className: "bt-bubble bt-busy-dots", "aria-hidden": "true" },
- (0, import_react17.createElement)("span", null),
- (0, import_react17.createElement)("span", null),
- (0, import_react17.createElement)("span", null)
+ (0, import_react18.createElement)("span", null),
+ (0, import_react18.createElement)("span", null),
+ (0, import_react18.createElement)("span", null)
)
)
)
@@ -10126,15 +10345,15 @@ var BUSY_ACK_CSS = `
`;
// src/client/header.js
-var import_react18 = require("react");
-var import_react_dom5 = require("react-dom");
-var GoArrow = () => (0, import_react18.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react18.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" }));
+var import_react19 = require("react");
+var import_react_dom6 = require("react-dom");
+var GoArrow = () => (0, import_react19.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_react19.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_react18.useState)(false);
- (0, import_react18.useEffect)(() => {
+ const [shown, setShown] = (0, import_react19.useState)(false);
+ (0, import_react19.useEffect)(() => {
const left = seen + MEMORY_NOTICE_MS - Date.now();
setShown(seen > 0 && left > 0);
if (seen === 0 || left <= 0) return void 0;
@@ -10144,16 +10363,16 @@ function useMemoryNotice(ids, useMemoryNews) {
return shown;
}
function MemoryNotice({ shown }) {
- const inner = (0, import_react18.useRef)(null);
- const [width, setWidth] = (0, import_react18.useState)(0);
+ const inner = (0, import_react19.useRef)(null);
+ const [width, setWidth] = (0, import_react19.useState)(0);
const label = t("Memory updated");
- (0, import_react18.useLayoutEffect)(() => {
+ (0, import_react19.useLayoutEffect)(() => {
if (shown) setWidth(inner.current?.scrollWidth ?? 0);
}, [label, shown]);
- return (0, import_react18.createElement)(
+ return (0, import_react19.createElement)(
"span",
{ className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } },
- (0, import_react18.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react18.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react18.createElement)(MemoryIcon)), label)
+ (0, import_react19.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react19.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react19.createElement)(MemoryIcon)), label)
);
}
function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useBusyAcks, useMemoryNews, useTurnClock, useUi, useLive, actions }) {
@@ -10169,7 +10388,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_react18.useEffect)(() => {
+ (0, import_react19.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");
@@ -10188,10 +10407,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_react18.createElement)(
+ return (0, import_react19.createElement)(
"div",
{ className: "bt-head" },
- (0, import_react18.createElement)(
+ (0, import_react19.createElement)(
"button",
{
type: "button",
@@ -10201,20 +10420,20 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe
"aria-label": memoryNotice ? t("Memory updated. Open {name}'s memory", { name: roster.byId[changed2]?.name ?? full2 }) : t("View conversation details"),
onClick: () => memoryNotice ? actions.openMemory(changed2) : actions.toggleDetails(sessionId)
},
- bot ? (0, import_react18.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react18.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null,
- (0, import_react18.createElement)("span", { className: face ? "bt-pill-name bt-pill-name-face" : "bt-pill-name", title: face ? full2 : void 0 }, name || title || t("New chat")),
- (0, import_react18.createElement)(MemoryNotice, { shown: memoryNotice }),
- (0, import_react18.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react18.createElement)(GoArrow))
+ bot ? (0, import_react19.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react19.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null,
+ (0, import_react19.createElement)("span", { className: face ? "bt-pill-name bt-pill-name-face" : "bt-pill-name", title: face ? full2 : void 0 }, name || title || t("New chat")),
+ (0, import_react19.createElement)(MemoryNotice, { shown: memoryNotice }),
+ (0, import_react19.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react19.createElement)(GoArrow))
),
- (0, import_react18.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
- (0, import_react18.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }),
- (0, import_react18.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }),
- (0, import_react18.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive })
+ (0, import_react19.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
+ (0, import_react19.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }),
+ (0, import_react19.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }),
+ (0, import_react19.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive })
);
}
function useTranscriptView() {
- const [view, setView] = (0, import_react18.useState)(null);
- (0, import_react18.useEffect)(() => {
+ const [view, setView] = (0, import_react19.useState)(null);
+ (0, import_react19.useEffect)(() => {
let frame = 0;
let watched = null;
const observer = new MutationObserver(() => schedule());
@@ -10279,22 +10498,22 @@ function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnCl
});
const view = useTranscriptView();
const unpinned = view?.unpinned === true;
- const [release, setRelease] = (0, import_react18.useState)({ base: null, dismissed: false });
- const [above, setAbove] = (0, import_react18.useState)({ seen: false, dismissed: false });
- (0, import_react18.useEffect)(() => {
+ const [release, setRelease] = (0, import_react19.useState)({ base: null, dismissed: false });
+ const [above, setAbove] = (0, import_react19.useState)({ seen: false, dismissed: false });
+ (0, import_react19.useEffect)(() => {
setRelease({ base: unpinned ? replies : null, dismissed: false });
}, [unpinned]);
- (0, import_react18.useEffect)(() => {
+ (0, import_react19.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_react18.useEffect)(() => {
+ (0, import_react19.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_react18.useEffect)(() => {
+ (0, import_react19.useEffect)(() => {
const scroller = document.querySelector("[data-conversation-scroll]");
if (!scroller) return void 0;
scroller.toggleAttribute("data-bt-news", showDown);
@@ -10302,16 +10521,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_react18.createElement)(
+ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react19.createElement)(
"div",
{ className: "bt-news", "data-direction": direction, style },
- (0, import_react18.createElement)(
+ (0, import_react19.createElement)(
"button",
{ type: "button", className: "bt-news-jump", onClick: onJump },
- (0, import_react18.createElement)("span", { className: "bt-news-ico" }, (0, import_react18.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
+ (0, import_react19.createElement)("span", { className: "bt-news-ico" }, (0, import_react19.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
label(count)
),
- (0, import_react18.createElement)("button", {
+ (0, import_react19.createElement)("button", {
type: "button",
className: "bt-news-x",
"aria-label": t("Dismiss new messages"),
@@ -10319,7 +10538,7 @@ function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnCl
event.stopPropagation();
onDismiss();
}
- }, (0, import_react18.createElement)(CloseIcon))
+ }, (0, import_react19.createElement)(CloseIcon))
);
const toLatest = () => {
const scroller = document.querySelector("[data-conversation-scroll]");
@@ -10328,9 +10547,9 @@ function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnCl
else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" });
};
const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" });
- return (0, import_react_dom5.createPortal)(
- (0, import_react18.createElement)(
- import_react18.Fragment,
+ return (0, import_react_dom6.createPortal)(
+ (0, import_react19.createElement)(
+ import_react19.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
@@ -10456,18 +10675,18 @@ function installHeaderEffects({ sessions, roster, actions, doc = globalThis.docu
}
// src/client/composer.js
-var import_react19 = require("react");
-var import_react_dom6 = require("react-dom");
+var import_react20 = require("react");
+var import_react_dom7 = require("react-dom");
var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition;
var keepComposerFocus = (event) => event.preventDefault();
function useDictation(onText) {
- const [listening, setListening] = (0, import_react19.useState)(false);
- const [note, setNote] = (0, import_react19.useState)("");
- const recognition = (0, import_react19.useRef)(null);
- const latest = (0, import_react19.useRef)(onText);
+ const [listening, setListening] = (0, import_react20.useState)(false);
+ const [note, setNote] = (0, import_react20.useState)("");
+ const recognition = (0, import_react20.useRef)(null);
+ const latest = (0, import_react20.useRef)(onText);
latest.current = onText;
- (0, import_react19.useEffect)(() => () => recognition.current?.abort(), []);
- (0, import_react19.useEffect)(() => {
+ (0, import_react20.useEffect)(() => () => recognition.current?.abort(), []);
+ (0, import_react20.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
@@ -10521,14 +10740,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_react19.createElement)(
+ return (0, import_react20.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_react19.createElement)(
+ return (0, import_react20.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 })
@@ -10539,7 +10758,7 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) {
if (final && inputActions) inputActions.insertText(text2, inputActions.captureInsertion());
});
const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null);
- (0, import_react19.useEffect)(() => {
+ (0, import_react20.useEffect)(() => {
if (!quote || !inputActions) return;
actions.clearQuote(quote.token);
const excerpt = quote.text.replace(/\s+/g, " ").trim();
@@ -10557,10 +10776,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) {
}, [quote?.token]);
if (sessionId === void 0) return null;
const pluginMic = actions.speech() !== void 0;
- return (0, import_react19.createElement)(
- import_react19.Fragment,
+ return (0, import_react20.createElement)(
+ import_react20.Fragment,
null,
- pluginMic ? null : (0, import_react19.createElement)("button", {
+ pluginMic ? null : (0, import_react20.createElement)("button", {
type: "button",
className: "bt-mic",
"aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"),
@@ -10568,16 +10787,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_react19.createElement)(MicIcon)),
- (0, import_react19.createElement)("button", {
+ }, (0, import_react20.createElement)(MicIcon)),
+ (0, import_react20.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_react19.createElement)(WaveIcon, { size: 18 })),
- dictation.note ? (0, import_react19.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null
+ }, (0, import_react20.createElement)(WaveIcon, { size: 18 })),
+ dictation.note ? (0, import_react20.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null
);
}
function menuIcons(roster) {
@@ -10585,17 +10804,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_react19.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size }));
+ if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react20.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size }));
return marks.get(id);
};
}
function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
const roster = useRoster((value) => value);
- const [open, setOpen] = (0, import_react19.useState)(null);
+ const [open, setOpen] = (0, import_react20.useState)(null);
const [shown, leaving] = useLinger(open, 120);
- const button = (0, import_react19.useRef)(null);
- const menu = (0, import_react19.useRef)(null);
- (0, import_react19.useEffect)(() => {
+ const button = (0, import_react20.useRef)(null);
+ const menu = (0, import_react20.useRef)(null);
+ (0, import_react20.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null);
@@ -10636,10 +10855,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
actions.referenceChat(sessionId, inputActions?.captureInsertion?.());
};
const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat();
- return (0, import_react19.createElement)(
- import_react19.Fragment,
+ return (0, import_react20.createElement)(
+ import_react20.Fragment,
null,
- (0, import_react19.createElement)("button", {
+ (0, import_react20.createElement)("button", {
ref: button,
type: "button",
className: "bt-plus",
@@ -10649,8 +10868,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
"aria-expanded": Boolean(open),
onMouseDown: keepComposerFocus,
onClick: toggle
- }, (0, import_react19.createElement)(PlusIcon)),
- shown ? (0, import_react_dom6.createPortal)((0, import_react19.createElement)(
+ }, (0, import_react20.createElement)(PlusIcon)),
+ shown ? (0, import_react_dom7.createPortal)((0, import_react20.createElement)(
"div",
{
ref: menu,
@@ -10660,9 +10879,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
style: { left: shown.left, bottom: shown.bottom },
"data-leaving": leaving || void 0
},
- (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react19.createElement)(PaperclipIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
- canReference ? (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react19.createElement)(ChatsIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
- (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react19.createElement)(ClockIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task")))
+ (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react20.createElement)(PaperclipIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
+ canReference ? (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react20.createElement)(ChatsIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
+ (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react20.createElement)(ClockIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task")))
), document.body) : null
);
}
@@ -11018,8 +11237,8 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) {
}
// src/client/model-menu.js
-var import_react20 = require("react");
-var import_react_dom7 = require("react-dom");
+var import_react21 = require("react");
+var import_react_dom8 = require("react-dom");
var MENU_EXIT_MS = 120;
function groupModels(models) {
const groups = /* @__PURE__ */ new Map();
@@ -11041,8 +11260,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_react20.useState)(void 0);
- (0, import_react20.useEffect)(() => {
+ const [fetched, setFetched] = (0, import_react21.useState)(void 0);
+ (0, import_react21.useEffect)(() => {
setFetched(void 0);
if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0;
let live = true;
@@ -11067,12 +11286,12 @@ function effortRows(reasoning) {
];
}
function EffortRows({ reasoning, current, onPick }) {
- return (0, import_react20.createElement)(
- import_react20.Fragment,
+ return (0, import_react21.createElement)(
+ import_react21.Fragment,
null,
- (0, import_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- (0, import_react20.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
- effortRows(reasoning).map((row) => (0, import_react20.createElement)(
+ (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
+ (0, import_react21.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
+ effortRows(reasoning).map((row) => (0, import_react21.createElement)(
"button",
{
key: row.effort ?? "default",
@@ -11081,17 +11300,17 @@ function EffortRows({ reasoning, current, onPick }) {
"aria-checked": row.effort === current,
onClick: () => onPick(row.effort)
},
- (0, import_react20.createElement)("span", { className: "bt-menu-label" }, row.label),
- (0, import_react20.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react20.createElement)(CheckIcon) : null)
+ (0, import_react21.createElement)("span", { className: "bt-menu-label" }, row.label),
+ (0, import_react21.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react21.createElement)(CheckIcon) : null)
))
);
}
function ModelRows({ roster, current, onPick }) {
- return groupModels(roster.models).map(([provider, list]) => (0, import_react20.createElement)(
- import_react20.Fragment,
+ return groupModels(roster.models).map(([provider, list]) => (0, import_react21.createElement)(
+ import_react21.Fragment,
{ key: list[0].provider ?? provider },
- (0, import_react20.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
- list.map((model) => (0, import_react20.createElement)(
+ (0, import_react21.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
+ list.map((model) => (0, import_react21.createElement)(
"button",
{
key: model.ref,
@@ -11101,20 +11320,20 @@ function ModelRows({ roster, current, onPick }) {
title: model.ref,
onClick: () => onPick(model.ref)
},
- (0, import_react20.createElement)("span", { className: "bt-menu-label" }, model.name),
- model.ref === roster.defaultModel ? (0, import_react20.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null,
- (0, import_react20.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react20.createElement)(CheckIcon) : null)
+ (0, import_react21.createElement)("span", { className: "bt-menu-label" }, model.name),
+ model.ref === roster.defaultModel ? (0, import_react21.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null,
+ (0, import_react21.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react21.createElement)(CheckIcon) : null)
))
));
}
-var Caret = () => (0, import_react20.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react20.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" }));
+var Caret = () => (0, import_react21.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_react21.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" }));
function useAnchoredMenu() {
- const [place, setPlace] = (0, import_react20.useState)(null);
+ const [place, setPlace] = (0, import_react21.useState)(null);
const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS);
- const button = (0, import_react20.useRef)(null);
- const menu = (0, import_react20.useRef)(null);
+ const button = (0, import_react21.useRef)(null);
+ const menu = (0, import_react21.useRef)(null);
const close = () => setPlace(null);
- (0, import_react20.useEffect)(() => {
+ (0, import_react21.useEffect)(() => {
if (place === null) return void 0;
const onDown = (event) => {
if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return;
@@ -11177,10 +11396,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_react20.createElement)(
- import_react20.Fragment,
+ return (0, import_react21.createElement)(
+ import_react21.Fragment,
null,
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"button",
{
ref: button,
@@ -11193,38 +11412,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_react20.createElement)(
+ (0, import_react21.createElement)(
"span",
{ className: "bt-model-copy" },
- (0, import_react20.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")),
- shown ? (0, import_react20.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null
+ (0, import_react21.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")),
+ shown ? (0, import_react21.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null
),
- (0, import_react20.createElement)(Caret)
+ (0, import_react21.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react20.createElement)(
+ shownPlace ? (0, import_react_dom8.createPortal)((0, import_react21.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_react20.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react20.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")),
- effortRows(reasoning).length > 0 ? (0, import_react20.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
- (0, import_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
+ models.length > 0 ? (0, import_react21.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react21.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")),
+ effortRows(reasoning).length > 0 ? (0, import_react21.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
+ (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
+ (0, import_react21.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
close();
openHarnessModels();
- } }, (0, import_react20.createElement)(SlidersIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…")))
+ } }, (0, import_react21.createElement)(SlidersIcon), (0, import_react21.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_react20.useState)("");
- const [error, setError] = (0, import_react20.useState)("");
+ const [busy, setBusy] = (0, import_react21.useState)("");
+ const [error, setError] = (0, import_react21.useState)("");
const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready;
- (0, import_react20.useEffect)(() => {
+ (0, import_react21.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_react20.createElement)(RemoteGoneDock, { bot }) : null;
+ if (bot.remote) return bot.remote.gone ? (0, import_react21.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;
@@ -11237,12 +11456,12 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) {
}).finally(() => setBusy(""));
};
const text2 = 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_react20.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react20.createElement)(KeyHelpLink, { key: "key-help" })] : [
- ...groupModels(models).map(([provider, list]) => (0, import_react20.createElement)(
+ const choices = models.length === 0 ? [...setupChoices(actions).map(([label, run]) => (0, import_react21.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react21.createElement)(KeyHelpLink, { key: "key-help" })] : [
+ ...groupModels(models).map(([provider, list]) => (0, import_react21.createElement)(
"div",
{ key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider },
- (0, import_react20.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
- (0, import_react20.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react20.createElement)("button", {
+ (0, import_react21.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
+ (0, import_react21.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react21.createElement)("button", {
key: model.ref,
type: "button",
className: "bt-model-chip",
@@ -11251,46 +11470,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_react20.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)))
+ }, model.name, busy === model.ref ? (0, import_react21.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)))
)),
- (0, import_react20.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react20.createElement)(SlidersIcon), t("Manage models"))
+ (0, import_react21.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react21.createElement)(SlidersIcon), t("Manage models"))
];
- return (0, import_react20.createElement)(
+ return (0, import_react21.createElement)(
"div",
{ className: "bt-model-dock" },
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"div",
{ className: "bt-astack", "data-lead": "" },
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"div",
{ className: "bt-msg" },
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"div",
{ className: "bt-msg-line" },
- (0, import_react20.createElement)(BotMark, { bot, size: 22 }),
- (0, import_react20.createElement)("div", { className: "bt-bubble" }, text2)
+ (0, import_react21.createElement)(BotMark, { bot, size: 22 }),
+ (0, import_react21.createElement)("div", { className: "bt-bubble" }, text2)
)
)
),
- (0, import_react20.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices),
- error ? (0, import_react20.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null
+ (0, import_react21.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices),
+ error ? (0, import_react21.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null
);
}
function RemoteGoneDock({ bot }) {
- return (0, import_react20.createElement)(
+ return (0, import_react21.createElement)(
"div",
{ className: "bt-model-dock", "data-flybot": "remote-gone" },
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"div",
{ className: "bt-astack", "data-lead": "" },
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"div",
{ className: "bt-msg" },
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)(
"div",
{ className: "bt-msg-line" },
- (0, import_react20.createElement)(BotMark, { bot, size: 22 }),
- (0, import_react20.createElement)("div", { className: "bt-bubble" }, t("{name} ({handle}) is no longer shared with you, so messages to it cannot be delivered. It runs on its owner's machine, so another model cannot stand in. Ask its owner for a new invite.", { name: bot.name, handle: bot.remote.handle }))
+ (0, import_react21.createElement)(BotMark, { bot, size: 22 }),
+ (0, import_react21.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 }))
)
)
)
@@ -11305,7 +11524,7 @@ function setupChoices(actions) {
}
var KEY_HELP_URL = "https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md";
function KeyHelpLink() {
- return (0, import_react20.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_react21.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 text2 = String(message ?? "");
@@ -11329,22 +11548,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_react20.createElement)(
+ return (0, import_react21.createElement)(
"div",
{ className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 },
- (0, import_react20.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
- (0, import_react20.createElement)(
+ (0, import_react21.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
+ (0, import_react21.createElement)(
"div",
{ className: "bt-turn-error-copy" },
- (0, import_react20.createElement)("span", { className: "bt-turn-error-title" }, title),
- (0, import_react20.createElement)("span", { className: "bt-turn-error-message" }, message),
- code ? (0, import_react20.createElement)("code", { className: "bt-turn-error-code" }, code) : null
+ (0, import_react21.createElement)("span", { className: "bt-turn-error-title" }, title),
+ (0, import_react21.createElement)("span", { className: "bt-turn-error-message" }, message),
+ code ? (0, import_react21.createElement)("code", { className: "bt-turn-error-code" }, code) : null
),
- choices.length > 0 ? (0, import_react20.createElement)(
+ choices.length > 0 ? (0, import_react21.createElement)(
"div",
{ className: "bt-turn-error-actions" },
- choices.map(([label, run]) => (0, import_react20.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)),
- deepseek ? (0, import_react20.createElement)(KeyHelpLink, { key: "key-help" }) : null
+ choices.map(([label, run]) => (0, import_react21.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)),
+ deepseek ? (0, import_react21.createElement)(KeyHelpLink, { key: "key-help" }) : null
) : null
);
}
@@ -11352,14 +11571,14 @@ function DeepSeekSignIn({ useUi, actions }) {
const open = useUi((state) => state.signIn);
const entry = open ? actions.signInEntry?.() : null;
if (!entry) return null;
- return (0, import_react20.createElement)(SignInHost, { key: open, entry, actions });
+ return (0, import_react21.createElement)(SignInHost, { key: open, entry, actions });
}
function SignInHost({ entry, actions }) {
- const [ops] = (0, import_react20.useState)(() => entry.inject?.() ?? {});
+ const [ops] = (0, import_react21.useState)(() => entry.inject?.() ?? {});
const close = () => actions.closeSignIn();
- const useHook = (source) => (selector) => (0, import_react20.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot()));
- const [hooks] = (0, import_react20.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) }));
- return (0, import_react20.createElement)(entry.component, {
+ const useHook = (source) => (selector) => (0, import_react21.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot()));
+ const [hooks] = (0, import_react21.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) }));
+ return (0, import_react21.createElement)(entry.component, {
...ops,
...hooks,
t: actions.accountT(),
@@ -11418,7 +11637,7 @@ var MODEL_CSS = `
`;
// src/client/model-test.js
-var import_react21 = require("react");
+var import_react22 = 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.",
@@ -11443,12 +11662,12 @@ function testOutcome(result, modelName) {
}
var testModel = (id) => call("test-model", { id });
function ModelTestRow({ bot, modelName, test = testModel }) {
- const [state, setState] = (0, import_react21.useState)({ busy: false, outcome: void 0 });
- const live = (0, import_react21.useRef)(true);
- (0, import_react21.useEffect)(() => () => {
+ const [state, setState] = (0, import_react22.useState)({ busy: false, outcome: void 0 });
+ const live = (0, import_react22.useRef)(true);
+ (0, import_react22.useEffect)(() => () => {
live.current = false;
}, []);
- (0, import_react21.useEffect)(() => {
+ (0, import_react22.useEffect)(() => {
setState({ busy: false, outcome: void 0 });
}, [bot.id, bot.model]);
const start = () => {
@@ -11462,21 +11681,21 @@ function ModelTestRow({ bot, modelName, test = testModel }) {
});
};
const { busy, outcome } = state;
- return (0, import_react21.createElement)(
+ return (0, import_react22.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-model-test" },
- (0, import_react21.createElement)(
+ (0, import_react22.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")),
- (0, import_react21.createElement)(
+ (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")),
+ (0, import_react22.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_react21.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
+ outcome?.code ? (0, import_react22.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
)
),
- (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection"))
+ (0, import_react22.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 = `
@@ -11496,17 +11715,17 @@ var MODEL_TEST_CSS = `
`;
// src/client/folder-menu.js
-var import_react22 = require("react");
-var import_react_dom8 = require("react-dom");
+var import_react23 = require("react");
+var import_react_dom9 = require("react-dom");
var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path);
-var Tick = ({ on }) => (0, import_react22.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react22.createElement)(CheckIcon) : null);
+var Tick = ({ on }) => (0, import_react23.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react23.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_react22.useState)(null);
- const [view, setView] = (0, import_react22.useState)("list");
- const [draft, setDraft] = (0, import_react22.useState)("");
- const [listing, setListing] = (0, import_react22.useState)(null);
- const [problem, setProblem] = (0, import_react22.useState)("");
+ const [info, setInfo] = (0, import_react23.useState)(null);
+ const [view, setView] = (0, import_react23.useState)("list");
+ const [draft, setDraft] = (0, import_react23.useState)("");
+ const [listing, setListing] = (0, import_react23.useState)(null);
+ const [problem, setProblem] = (0, import_react23.useState)("");
const fallback = group ? t("Each Bot's own folder") : t("Team workspace");
const toggle = () => {
if (isOpen) {
@@ -11539,35 +11758,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_react22.createElement)(
+ const listView = (0, import_react23.createElement)(
"div",
{ key: "list", className: "bt-folder-view" },
- (0, import_react22.createElement)(
+ (0, import_react23.createElement)(
"button",
{ type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") },
- (0, import_react22.createElement)("span", { className: "bt-menu-label" }, fallback),
- (0, import_react22.createElement)(Tick, { on: !value })
+ (0, import_react23.createElement)("span", { className: "bt-menu-label" }, fallback),
+ (0, import_react23.createElement)(Tick, { on: !value })
),
- folders.length > 0 ? (0, import_react22.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null,
- folders.map((folder) => (0, import_react22.createElement)(
+ folders.length > 0 ? (0, import_react23.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null,
+ folders.map((folder) => (0, import_react23.createElement)(
"button",
{ key: folder.path, type: "button", role: "menuitemradio", "aria-checked": folder.path === value, title: folder.path, onClick: () => choose(folder.path) },
- (0, import_react22.createElement)("span", { className: "bt-folder-copy" }, (0, import_react22.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react22.createElement)("span", { className: "bt-folder-path" }, folder.path)),
- (0, import_react22.createElement)(Tick, { on: folder.path === value })
+ (0, import_react23.createElement)("span", { className: "bt-folder-copy" }, (0, import_react23.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react23.createElement)("span", { className: "bt-folder-path" }, folder.path)),
+ (0, import_react23.createElement)(Tick, { on: folder.path === value })
)),
- (0, import_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- info?.picker === "native" ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null,
- info?.picker === "browse" ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null,
- (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
+ (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
+ info?.picker === "native" ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null,
+ info?.picker === "browse" ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null,
+ (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
setDraft(value ?? "");
setView("type");
- } }, (0, import_react22.createElement)(PencilIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…")))
+ } }, (0, import_react23.createElement)(PencilIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…")))
);
- const typeView = (0, import_react22.createElement)(
+ const typeView = (0, import_react23.createElement)(
"div",
{ key: "type", className: "bt-folder-view" },
- (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react22.createElement)(ChevronLeftIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
- (0, import_react22.createElement)("input", {
+ (0, import_react23.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react23.createElement)(ChevronLeftIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
+ (0, import_react23.createElement)("input", {
className: "bt-folder-input",
autoFocus: true,
value: draft,
@@ -11579,33 +11798,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false }
if (event.key === "Enter" && !event.nativeEvent.isComposing) typed();
}
}),
- (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder"))
+ (0, import_react23.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_react22.createElement)(
+ const browseView = listing ? (0, import_react23.createElement)(
"div",
{ key: `browse:${listing.path}`, className: "bt-folder-view" },
- (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react22.createElement)(ChevronLeftIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
- (0, import_react22.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path),
- parent ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null,
- inside.map((entry) => (0, import_react22.createElement)(
+ (0, import_react23.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react23.createElement)(ChevronLeftIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
+ (0, import_react23.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path),
+ parent ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null,
+ inside.map((entry) => (0, import_react23.createElement)(
"button",
{ key: entry.path, type: "button", role: "menuitem", onClick: () => browse(entry.path) },
- (0, import_react22.createElement)(FolderIcon),
- (0, import_react22.createElement)("span", { className: "bt-menu-label" }, entry.name),
- (0, import_react22.createElement)(ChevronRightIcon)
+ (0, import_react23.createElement)(FolderIcon),
+ (0, import_react23.createElement)("span", { className: "bt-menu-label" }, entry.name),
+ (0, import_react23.createElement)(ChevronRightIcon)
)),
- inside.length === 0 ? (0, import_react22.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null,
- listing.truncated ? (0, import_react22.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null,
- (0, import_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
- (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder"))
+ inside.length === 0 ? (0, import_react23.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null,
+ listing.truncated ? (0, import_react23.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null,
+ (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
+ (0, import_react23.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_react22.createElement)(
- import_react22.Fragment,
+ return (0, import_react23.createElement)(
+ import_react23.Fragment,
null,
- (0, import_react22.createElement)(
+ (0, import_react23.createElement)(
"button",
{
ref: button,
@@ -11618,19 +11837,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false }
title: value,
onClick: toggle
},
- (0, import_react22.createElement)(
+ (0, import_react23.createElement)(
"span",
{ className: "bt-model-copy" },
- (0, import_react22.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback),
- value ? (0, import_react22.createElement)("span", { className: "bt-model-provider" }, value) : null
+ (0, import_react23.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback),
+ value ? (0, import_react23.createElement)("span", { className: "bt-model-provider" }, value) : null
),
- (0, import_react22.createElement)(Caret)
+ (0, import_react23.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom8.createPortal)((0, import_react22.createElement)(
+ shownPlace ? (0, import_react_dom9.createPortal)((0, import_react23.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_react22.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
+ problem ? (0, import_react23.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
), document.body) : null
);
}
@@ -11650,7 +11869,7 @@ var FOLDER_CSS = `
`;
// src/client/schedules.js
-var import_react23 = require("react");
+var import_react24 = require("react");
var POLL_MS4 = 3e4;
var shared = null;
var listeners = /* @__PURE__ */ new Set();
@@ -11659,8 +11878,8 @@ function publish(view) {
for (const listener of listeners) listener(view);
}
function useSchedules(actions) {
- const [view, setView] = (0, import_react23.useState)(shared);
- (0, import_react23.useEffect)(() => {
+ const [view, setView] = (0, import_react24.useState)(shared);
+ (0, import_react24.useEffect)(() => {
listeners.add(setView);
let stopped = false;
let timer;
@@ -11733,9 +11952,9 @@ function lastRunText(run) {
}
}
function TaskRow({ task, actions, onView, onEdit, onCopy, showBot, roster }) {
- const [confirming, setConfirming] = (0, import_react23.useState)(false);
- const [busy, setBusy] = (0, import_react23.useState)(false);
- (0, import_react23.useEffect)(() => {
+ const [confirming, setConfirming] = (0, import_react24.useState)(false);
+ const [busy, setBusy] = (0, import_react24.useState)(false);
+ (0, import_react24.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
@@ -11756,7 +11975,7 @@ function TaskRow({ task, actions, onView, onEdit, onCopy, showBot, roster }) {
onView(next);
}, () => setBusy(false));
};
- const [retryError, setRetryError] = (0, import_react23.useState)("");
+ const [retryError, setRetryError] = (0, import_react24.useState)("");
const retry = () => {
setBusy(true);
setRetryError("");
@@ -11772,25 +11991,25 @@ function TaskRow({ task, actions, onView, onEdit, onCopy, showBot, roster }) {
const stoppable = task.status === "active" || task.status === "paused";
const failed = task.lastRun?.result === "failed";
const last = lastRunText(task.lastRun);
- return (0, import_react23.createElement)(
+ return (0, import_react24.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_react23.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
- (0, import_react23.createElement)(
+ showBot ? (0, import_react24.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
+ (0, import_react24.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react23.createElement)("span", { className: "bt-task-title" }, task.title),
- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")),
- task.prompt && task.prompt !== task.title ? (0, import_react23.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null,
- last ? (0, import_react23.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null,
- retryError ? (0, import_react23.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null
+ (0, import_react24.createElement)("span", { className: "bt-task-title" }, task.title),
+ (0, import_react24.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_react24.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null,
+ last ? (0, import_react24.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null,
+ retryError ? (0, import_react24.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null
),
- (0, import_react23.createElement)("span", { className: "bt-task-next" }, nextText(task)),
- failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, title: t("Run the task once now"), onClick: retry }, t("Retry")) : null,
- onEdit ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react23.createElement)(PencilIcon)) : null,
- onCopy ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Copy to another Bot or group chat"), title: t("Copy to…"), onClick: () => onCopy(task) }, (0, import_react23.createElement)(DuplicateIcon)) : null,
- stoppable ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null,
- (0, import_react23.createElement)("button", {
+ (0, import_react24.createElement)("span", { className: "bt-task-next" }, nextText(task)),
+ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react24.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_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react24.createElement)(PencilIcon)) : null,
+ onCopy ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Copy to another Bot or group chat"), title: t("Copy to…"), onClick: () => onCopy(task) }, (0, import_react24.createElement)(DuplicateIcon)) : null,
+ stoppable ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null,
+ (0, import_react24.createElement)("button", {
type: "button",
className: "bt-mini bt-task-remove",
"data-confirm": confirming || void 0,
@@ -11798,7 +12017,7 @@ function TaskRow({ task, actions, onView, onEdit, onCopy, 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_react23.createElement)(TrashIcon))
+ }, confirming ? t("Delete") : (0, import_react24.createElement)(TrashIcon))
);
}
var pad = (number) => String(number).padStart(2, "0");
@@ -11831,17 +12050,17 @@ function formStart(task) {
}
function TaskForm({ roster, actions, start, task, onDone, onCancel }) {
const first = formStart(task);
- const [target, setTarget] = (0, import_react23.useState)(task ? task.botId ?? task.roomId : start ?? "");
- const [title, setTitle] = (0, import_react23.useState)(first.title);
- const [prompt, setPrompt] = (0, import_react23.useState)(first.prompt);
- const [kind, setKind] = (0, import_react23.useState)(first.kind);
- const [at, setAt] = (0, import_react23.useState)(first.at);
- const [time, setTime] = (0, import_react23.useState)(first.time);
- const [weekdays, setWeekdays] = (0, import_react23.useState)(first.weekdays);
- const [count, setCount] = (0, import_react23.useState)(first.count);
- const [unit, setUnit] = (0, import_react23.useState)(first.unit);
- const [busy, setBusy] = (0, import_react23.useState)(false);
- const [error, setError] = (0, import_react23.useState)("");
+ const [target, setTarget] = (0, import_react24.useState)(task ? task.botId ?? task.roomId : start ?? "");
+ const [title, setTitle] = (0, import_react24.useState)(first.title);
+ const [prompt, setPrompt] = (0, import_react24.useState)(first.prompt);
+ const [kind, setKind] = (0, import_react24.useState)(first.kind);
+ const [at, setAt] = (0, import_react24.useState)(first.at);
+ const [time, setTime] = (0, import_react24.useState)(first.time);
+ const [weekdays, setWeekdays] = (0, import_react24.useState)(first.weekdays);
+ const [count, setCount] = (0, import_react24.useState)(first.count);
+ const [unit, setUnit] = (0, import_react24.useState)(first.unit);
+ const [busy, setBusy] = (0, import_react24.useState)(false);
+ const [error, setError] = (0, import_react24.useState)("");
const timing = () => {
if (kind === "cron") return { kind: "keep" };
if (kind === "at") return { kind, at: new Date(at).toISOString(), timeZone: first.timeZone };
@@ -11868,70 +12087,70 @@ 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_react23.createElement)(
+ return (0, import_react24.createElement)(
"form",
{ className: "bt-set-card bt-task-form", onSubmit: submit },
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"label",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Send to"))),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Send to"))),
+ (0, import_react24.createElement)(
"select",
{ className: "bt-select bt-task-bot", value: target, onChange: (event) => setTarget(event.target.value) },
- (0, import_react23.createElement)("option", { value: "" }, t("Pick a Bot or group chat")),
- (0, import_react23.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react23.createElement)("option", { key: bot.id, value: bot.id }, bot.name))),
- roster.rooms.length > 0 ? (0, import_react23.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react23.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null
+ (0, import_react24.createElement)("option", { value: "" }, t("Pick a Bot or group chat")),
+ (0, import_react24.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react24.createElement)("option", { key: bot.id, value: bot.id }, bot.name))),
+ roster.rooms.length > 0 ? (0, import_react24.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react24.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null
)
),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"label",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
- (0, import_react23.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) })
+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
+ (0, import_react24.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) })
),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("What to do"))),
- (0, import_react23.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) })
+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("What to do"))),
+ (0, import_react24.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) })
),
- (0, import_react23.createElement)(
+ (0, import_react24.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" }, t("When"))),
- (0, import_react23.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react23.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("When"))),
+ (0, import_react24.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react24.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))),
+ (0, import_react24.createElement)(
"div",
{ key: kind, className: "bt-task-when bt-veil" },
- kind === "cron" ? (0, import_react23.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null,
- kind === "at" ? (0, import_react23.createElement)("input", { className: "bt-input", type: "datetime-local", value: at, "aria-label": t("Date and time"), onChange: (event) => setAt(event.target.value) }) : null,
- kind === "weekly" ? (0, import_react23.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react23.createElement)("button", {
+ kind === "cron" ? (0, import_react24.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null,
+ kind === "at" ? (0, import_react24.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_react24.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react24.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_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: time, "aria-label": t("Time"), onChange: (event) => setTime(event.target.value) }) : null,
- kind === "every" ? (0, import_react23.createElement)(
- import_react23.Fragment,
+ kind === "daily" || kind === "weekly" ? (0, import_react24.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_react24.createElement)(
+ import_react24.Fragment,
null,
- (0, import_react23.createElement)("span", { className: "bt-task-every" }, t("Every")),
- (0, import_react23.createElement)("input", { className: "bt-input bt-task-count", type: "number", min: 1, value: count, "aria-label": t("How many"), onChange: (event) => setCount(event.target.value) }),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)("span", { className: "bt-task-every" }, t("Every")),
+ (0, import_react24.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_react24.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_react23.createElement)("option", { key: id, value: id }, t(label)))
+ UNITS.map(([id, , label]) => (0, import_react24.createElement)("option", { key: id, value: id }, t(label)))
)
) : null
)
),
- error ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
- (0, import_react23.createElement)(
+ error ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
+ (0, import_react24.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)(
+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
+ (0, import_react24.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")
@@ -11944,21 +12163,21 @@ function openPlugins(actions) {
actions.selectPanel("plugins");
}
function AutomationOff({ actions }) {
- return (0, import_react23.createElement)(
+ return (0, import_react24.createElement)(
"div",
{ className: "bt-set-card bt-task-off" },
- (0, import_react23.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react23.createElement)(ClockIcon)),
- (0, import_react23.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")),
- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")),
- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
+ (0, import_react24.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react24.createElement)(ClockIcon)),
+ (0, import_react24.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")),
+ (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")),
+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
);
}
function QuietHoursCard({ view, actions }) {
const quiet = view.quietHours ?? { on: false };
- const [busy, setBusy] = (0, import_react23.useState)(false);
- const [error, setError] = (0, import_react23.useState)("");
- const [from, setFrom] = (0, import_react23.useState)(quiet.from ?? "22:00");
- const [to, setTo] = (0, import_react23.useState)(quiet.to ?? "07:00");
+ const [busy, setBusy] = (0, import_react24.useState)(false);
+ const [error, setError] = (0, import_react24.useState)("");
+ const [from, setFrom] = (0, import_react24.useState)(quiet.from ?? "22:00");
+ const [to, setTo] = (0, import_react24.useState)(quiet.to ?? "07:00");
const unchecked = view.quietUnchecked === true;
const refresh = () => {
void Promise.resolve(actions.schedules?.()).then((next) => {
@@ -11976,23 +12195,23 @@ function QuietHoursCard({ view, actions }) {
refresh();
}, (failure) => setError(hostText(failure?.message ?? String(failure)))).finally(() => setBusy(false));
};
- return (0, import_react23.createElement)(
+ return (0, import_react24.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react23.createElement)("h3", null, t("Quiet hours")),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)("h3", null, t("Quiet hours")),
+ (0, import_react24.createElement)(
"div",
{ className: "bt-set-card" },
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")),
- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists."))
+ (0, import_react24.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")),
+ (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists."))
),
- (0, import_react23.createElement)("button", {
+ (0, import_react24.createElement)("button", {
type: "button",
className: "bt-soft bt-soft-sm",
disabled: busy,
@@ -12000,30 +12219,30 @@ function QuietHoursCard({ view, actions }) {
onClick: () => save(quiet.on ? { on: false } : { on: true, from, to })
}, quiet.on ? t("Turn off") : t("Turn on"))
),
- quiet.on ? (0, import_react23.createElement)(
+ quiet.on ? (0, import_react24.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Window"))),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Window"))),
+ (0, import_react24.createElement)(
"div",
{ className: "bt-task-when" },
- (0, import_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }),
- (0, import_react23.createElement)("span", { className: "bt-task-every" }, "–"),
- (0, import_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }),
- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window"))
+ (0, import_react24.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }),
+ (0, import_react24.createElement)("span", { className: "bt-task-every" }, "–"),
+ (0, import_react24.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }),
+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window"))
)
) : null,
- error ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
- quiet.on && unchecked ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : null
+ error ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
+ quiet.on && unchecked ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : null
)
);
}
function SchedulesPage({ roster, actions, start }) {
const view = useSchedules(actions);
- const [adding, setAdding] = (0, import_react23.useState)(Boolean(start));
- const [editing, setEditing] = (0, import_react23.useState)(null);
- if (view === null) return (0, import_react23.createElement)("div", { className: "bt-set-hint" }, t("Loading…"));
- if (!view.available) return (0, import_react23.createElement)(AutomationOff, { actions });
+ const [adding, setAdding] = (0, import_react24.useState)(Boolean(start));
+ const [editing, setEditing] = (0, import_react24.useState)(null);
+ if (view === null) return (0, import_react24.createElement)("div", { className: "bt-set-hint" }, t("Loading…"));
+ if (!view.available) return (0, import_react24.createElement)(AutomationOff, { actions });
const shown = (next) => {
publish(next);
setAdding(false);
@@ -12034,37 +12253,37 @@ function SchedulesPage({ roster, actions, start }) {
setEditing(task);
};
const byChat = [
- ...roster.bots.map((bot) => [bot.id, (0, import_react23.createElement)(BotAvatar, { bot, size: 18, badge: false, live: false }), bot.name, view.tasks.filter((task) => task.botId === bot.id)]),
- ...roster.rooms.map((room2) => [room2.id, (0, import_react23.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)])
+ ...roster.bots.map((bot) => [bot.id, (0, import_react24.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_react24.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)])
].filter(([, , , tasks]) => tasks.length > 0);
return [
- (0, import_react23.createElement)(QuietHoursCard, { key: "quiet", view, actions }),
- adding ? (0, import_react23.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react23.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => {
+ (0, import_react24.createElement)(QuietHoursCard, { key: "quiet", view, actions }),
+ adding ? (0, import_react24.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react24.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => {
setEditing(null);
setAdding(true);
- } }, (0, import_react23.createElement)(PlusIcon), t("New task"))),
- byChat.length === 0 && !adding ? (0, import_react23.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react23.createElement)(
+ } }, (0, import_react24.createElement)(PlusIcon), t("New task"))),
+ byChat.length === 0 && !adding ? (0, import_react24.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react24.createElement)(
"section",
{ key: id, className: "bt-set-section" },
- (0, import_react23.createElement)("h3", { className: "bt-task-head" }, avatar, name),
- (0, import_react23.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) })
+ (0, import_react24.createElement)("h3", { className: "bt-task-head" }, avatar, name),
+ (0, import_react24.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 [copying, setCopying] = (0, import_react23.useState)(null);
+ const [copying, setCopying] = (0, import_react24.useState)(null);
const source = copying ? tasks.find((task) => task.id === copying.id && task.sessionId === copying.sessionId) : void 0;
- const rows = tasks.filter((task) => task !== open && task !== source).map((task) => (0, import_react23.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit, onCopy: actions.scheduleCreate ? setCopying : void 0 }));
- return (0, import_react23.createElement)(
- import_react23.Fragment,
+ const rows = tasks.filter((task) => task !== open && task !== source).map((task) => (0, import_react24.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit, onCopy: actions.scheduleCreate ? setCopying : void 0 }));
+ return (0, import_react24.createElement)(
+ import_react24.Fragment,
null,
- open ? (0, import_react23.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null,
- source ? (0, import_react23.createElement)(CopyRow, { key: `copy-${source.id}`, task: source, roster, actions, onView: (next) => {
+ open ? (0, import_react24.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null,
+ source ? (0, import_react24.createElement)(CopyRow, { key: `copy-${source.id}`, task: source, roster, actions, onView: (next) => {
setCopying(null);
onDone(next);
}, onCancel: () => setCopying(null) }) : null,
- rows.length + extra.length > 0 ? (0, import_react23.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null
+ rows.length + extra.length > 0 ? (0, import_react24.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null
);
}
function CopyRow({ task, roster, actions, onView, onCancel }) {
@@ -12072,9 +12291,9 @@ function CopyRow({ task, roster, actions, onView, onCancel }) {
...roster.bots.filter((bot) => bot.id !== task.botId).map((bot) => [bot.id, bot.name]),
...roster.rooms.filter((room2) => room2.id !== task.roomId).map((room2) => [room2.id, room2.name])
];
- const [target, setTarget] = (0, import_react23.useState)("");
- const [busy, setBusy] = (0, import_react23.useState)(false);
- const [error, setError] = (0, import_react23.useState)("");
+ const [target, setTarget] = (0, import_react24.useState)("");
+ const [busy, setBusy] = (0, import_react24.useState)(false);
+ const [error, setError] = (0, import_react24.useState)("");
const submit = (event) => {
event.preventDefault();
if (busy || target === "") return;
@@ -12085,40 +12304,40 @@ function CopyRow({ task, roster, actions, onView, onCancel }) {
setError(hostText(failure?.message ?? String(failure)));
});
};
- return (0, import_react23.createElement)(
+ return (0, import_react24.createElement)(
"form",
{ className: "bt-set-row bt-task-copy", onSubmit: submit },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Copy “{name}” to", { name: task.title }))),
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Copy “{name}” to", { name: task.title }))),
+ (0, import_react24.createElement)(
"select",
{ className: "bt-select bt-task-bot", value: target, "aria-label": t("Copy to"), onChange: (event) => setTarget(event.target.value) },
- (0, import_react23.createElement)("option", { value: "" }, t("Pick a Bot or group chat")),
- others.map(([id, name]) => (0, import_react23.createElement)("option", { key: id, value: id }, name))
+ (0, import_react24.createElement)("option", { value: "" }, t("Pick a Bot or group chat")),
+ others.map(([id, name]) => (0, import_react24.createElement)("option", { key: id, value: id }, name))
),
- (0, import_react23.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm", disabled: busy || target === "" }, busy ? t("Copying…") : t("Copy")),
- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
- error ? (0, import_react23.createElement)("span", { className: "bt-danger bt-set-hint", role: "alert" }, error) : null
+ (0, import_react24.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm", disabled: busy || target === "" }, busy ? t("Copying…") : t("Copy")),
+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
+ error ? (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint", role: "alert" }, error) : null
);
}
function BotSchedules({ bot, roster, actions }) {
const view = useSchedules(actions);
- const [editing, setEditing] = (0, import_react23.useState)(null);
+ const [editing, setEditing] = (0, import_react24.useState)(null);
if (view === null) return null;
const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : [];
- return (0, import_react23.createElement)(
+ return (0, import_react24.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react23.createElement)("h3", null, t("Scheduled tasks")),
- !view.available ? (0, import_react23.createElement)(
+ (0, import_react24.createElement)("h3", null, t("Scheduled tasks")),
+ !view.available ? (0, import_react24.createElement)(
"div",
{ className: "bt-set-card" },
- (0, import_react23.createElement)(
+ (0, import_react24.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))),
- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
+ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))),
+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
)
- ) : (0, import_react23.createElement)(TaskList, {
+ ) : (0, import_react24.createElement)(TaskList, {
tasks,
editing,
roster,
@@ -12129,23 +12348,23 @@ function BotSchedules({ bot, roster, actions }) {
setEditing(null);
},
onCancel: () => setEditing(null),
- extra: [(0, import_react23.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react23.createElement)(PlusIcon), t("New task"))]
+ extra: [(0, import_react24.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react24.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_react23.createElement)(
+ return (0, import_react24.createElement)(
"div",
null,
- (0, import_react23.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")),
- tasks.length === 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-more", onClick: () => actions.openSettings("schedules", bot.id) }, view?.available === false ? t("Switch on Automation tasks on the Plugins page.") : t("No scheduled tasks yet. Add one")) : (0, import_react23.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react23.createElement)(
+ (0, import_react24.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")),
+ tasks.length === 0 ? (0, import_react24.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_react24.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react24.createElement)(
"button",
{ key: task.id, type: "button", className: "bt-task-line", onClick: () => actions.openSettings("schedules") },
- (0, import_react23.createElement)(ClockIcon),
- (0, import_react23.createElement)("span", { className: "bt-task-line-title" }, task.title),
- (0, import_react23.createElement)("span", { className: "bt-task-line-when" }, timingText(task))
+ (0, import_react24.createElement)(ClockIcon),
+ (0, import_react24.createElement)("span", { className: "bt-task-line-title" }, task.title),
+ (0, import_react24.createElement)("span", { className: "bt-task-line-when" }, timingText(task))
)))
);
}
@@ -12197,7 +12416,7 @@ var SCHEDULE_CSS = `
`;
// src/client/capability-settings.js
-var import_react24 = require("react");
+var import_react25 = require("react");
var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] });
var parseMap = (text2, label) => {
let value;
@@ -12211,11 +12430,11 @@ var parseMap = (text2, 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_react24.createElement)(
+ return (0, import_react25.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t(label))),
- (0, import_react24.createElement)(multiline ? "textarea" : "input", {
+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t(label))),
+ (0, import_react25.createElement)(multiline ? "textarea" : "input", {
className: multiline ? "bt-input bt-input-area" : "bt-input",
value,
disabled,
@@ -12227,7 +12446,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl
);
}
function CapabilityForm({ draft, setDraft, items, actions, onClose }) {
- const [error, setError] = (0, import_react24.useState)("");
+ const [error, setError] = (0, import_react25.useState)("");
const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value }));
const submit = async (event) => {
event.preventDefault();
@@ -12258,95 +12477,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_react24.createElement)(
+ return (0, import_react25.createElement)(
"form",
{ className: "bt-connector-form", onSubmit: submit },
- (0, import_react24.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }),
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }),
+ (0, import_react25.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Type"))),
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Type"))),
+ (0, import_react25.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_react24.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack")))
+ ["mcp", "skill", "pack"].map((kind) => (0, import_react25.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack")))
)
),
- (0, import_react24.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
- draft.kind === "mcp" ? (0, import_react24.createElement)(
+ (0, import_react25.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
+ draft.kind === "mcp" ? (0, import_react25.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Connection"))),
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Connection"))),
+ (0, import_react25.createElement)(
"select",
{ className: "bt-input", value: draft.transport, onChange: (event) => set("transport", event.target.value) },
- (0, import_react24.createElement)("option", { value: "stdio" }, t("Local command")),
- (0, import_react24.createElement)("option", { value: "streamable-http" }, t("HTTP server"))
+ (0, import_react25.createElement)("option", { value: "stdio" }, t("Local command")),
+ (0, import_react25.createElement)("option", { value: "streamable-http" }, t("HTTP server"))
)
) : null,
- draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react24.createElement)(
+ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react25.createElement)(
"div",
null,
- (0, import_react24.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }),
- (0, import_react24.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }),
- (0, import_react24.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') })
+ (0, import_react25.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }),
+ (0, import_react25.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }),
+ (0, import_react25.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_react24.createElement)(
+ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react25.createElement)(
"div",
null,
- (0, import_react24.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }),
- (0, import_react24.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') })
+ (0, import_react25.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }),
+ (0, import_react25.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_react24.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null,
- draft.kind === "pack" ? (0, import_react24.createElement)(
+ draft.kind === "skill" ? (0, import_react25.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null,
+ draft.kind === "pack" ? (0, import_react25.createElement)(
"div",
{ className: "bt-set-card" },
- available.length === 0 ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react24.createElement)(
+ available.length === 0 ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react25.createElement)(
"label",
{ key: item.name, 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" }, item.name)),
- (0, import_react24.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) })
+ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, item.name)),
+ (0, import_react25.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_react24.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
- (0, import_react24.createElement)(
+ error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
+ (0, import_react25.createElement)(
"div",
{ className: "bt-connector-form-row" },
- (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")),
- (0, import_react24.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability"))
+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")),
+ (0, import_react25.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_react24.createElement)(
+ return (0, import_react25.createElement)(
"div",
{ className: "bt-connector" },
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"div",
{ className: "bt-connector-head" },
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"span",
{ className: "bt-connector-text" },
- (0, import_react24.createElement)("span", { className: "bt-connector-name" }, item.name),
- (0, import_react24.createElement)("span", { className: "bt-connector-status" }, secondary)
+ (0, import_react25.createElement)("span", { className: "bt-connector-name" }, item.name),
+ (0, import_react25.createElement)("span", { className: "bt-connector-status" }, secondary)
),
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)(
"span",
{ className: "bt-connector-actions" },
- item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null,
- (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")),
- (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete"))
+ item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null,
+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")),
+ (0, import_react25.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_react24.useState)(null);
- const [confirming, setConfirming] = (0, import_react24.useState)("");
- (0, import_react24.useEffect)(() => {
+ const [draft, setDraft] = (0, import_react25.useState)(null);
+ const [confirming, setConfirming] = (0, import_react25.useState)("");
+ (0, import_react25.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(""), 4e3);
return () => clearTimeout(timer);
@@ -12369,20 +12588,20 @@ function CapabilitiesPage({ roster, actions, run }) {
setConfirming("");
void run(actions.deleteCapability(item.name));
};
- return (0, import_react24.createElement)(
+ return (0, import_react25.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react24.createElement)("h3", null, t("Shared capabilities")),
- (0, import_react24.createElement)(
+ (0, import_react25.createElement)("h3", null, t("Shared capabilities")),
+ (0, import_react25.createElement)(
"div",
{ className: "bt-actions" },
- (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability"))
+ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability"))
),
- draft ? (0, import_react24.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
- (0, import_react24.createElement)(
+ draft ? (0, import_react25.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
+ (0, import_react25.createElement)(
"div",
{ className: "bt-set-card bt-connectors-card" },
- items.length === 0 ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react24.createElement)(CapabilityRow, {
+ items.length === 0 ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react25.createElement)(CapabilityRow, {
key: item.name,
item,
confirming: confirming === item.name,
@@ -12398,10 +12617,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_react24.createElement)(
+ return (0, import_react25.createElement)(
Card2,
{ title: t("Capabilities") },
- items.map((item) => (0, import_react24.createElement)(SwitchRow2, {
+ items.map((item) => (0, import_react25.createElement)(SwitchRow2, {
key: item.name,
label: item.name,
hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0,
@@ -12412,7 +12631,7 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow
}
// src/client/connector-bots.js
-var import_react25 = require("react");
+var import_react26 = 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));
@@ -12428,37 +12647,37 @@ function usageText(roster, id) {
return own === 0 ? t("Off for every Bot. Choose which Bots may use it.") : own === bots.length ? t("On for all {total} Bots", { total: bots.length }) : t("On for {count} of {total} Bots", { count: own, total: bots.length });
}
function BotSwitch({ bot, connector, name, actions, run, SwitchRow: SwitchRow2 }) {
- return (0, import_react25.createElement)(SwitchRow2, { label: name, hint: ownHint(bot, connector.id), on: isOn(bot, connector.id), disabled: false, onChange: (on) => void run(actions.setBotConnector(bot.id, connector.id, on)) });
+ return (0, import_react26.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_react25.createElement)(
+ return (0, import_react26.createElement)(
"div",
{ className: "bt-connector-bots" },
- bots.length === 0 ? (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react25.createElement)(
+ bots.length === 0 ? (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react26.createElement)(
"div",
{ key: bot.id, className: "bt-connector-bot" },
- (0, import_react25.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
+ (0, import_react26.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
)),
- (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("On by default for each of your Bots, on every turn: in its own chat, in groups, on schedules and when a friend uses it. A friend's own Bot never gets it."))
+ (0, import_react26.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_react25.createElement)(
+ return (0, import_react26.createElement)(
Card2,
{ title: t("Connectors") },
- connected.map((connector) => (0, import_react25.createElement)(
+ connected.map((connector) => (0, import_react26.createElement)(
"div",
{ key: connector.id, className: "bt-connector-bot" },
- (0, import_react25.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
+ (0, import_react26.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
))
);
}
// src/client/memory-panel.js
-var import_react26 = require("react");
+var import_react27 = require("react");
var POLL_MS5 = 1500;
var MENU_EXIT_MS2 = 120;
var counts = /* @__PURE__ */ new Map();
@@ -12469,8 +12688,8 @@ function noteCount(botId, count) {
for (const listener of countListeners) listener();
}
function useMemoryCount(botId, actions) {
- const [count, setCount] = (0, import_react26.useState)(counts.get(botId) ?? null);
- (0, import_react26.useEffect)(() => {
+ const [count, setCount] = (0, import_react27.useState)(counts.get(botId) ?? null);
+ (0, import_react27.useEffect)(() => {
const listener = () => setCount(counts.get(botId) ?? null);
countListeners.add(listener);
listener();
@@ -12489,16 +12708,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_react26.createElement)(
+ return (0, import_react27.createElement)(
"div",
null,
- (0, import_react26.createElement)("div", { className: "bt-section-title" }, t("Memory")),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Memory")),
+ (0, import_react27.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_react26.createElement)(MemoryIcon),
- (0, import_react26.createElement)("span", { className: "bt-mem-row-copy" }, label),
- (0, import_react26.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react26.createElement)(ChevronRightIcon))
+ (0, import_react27.createElement)(MemoryIcon),
+ (0, import_react27.createElement)("span", { className: "bt-mem-row-copy" }, label),
+ (0, import_react27.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react27.createElement)(ChevronRightIcon))
)
);
}
@@ -12513,45 +12732,45 @@ function ago(at, now) {
return format.format(-Math.round(hours / 24), "day");
}
function Skeleton({ label }) {
- return (0, import_react26.createElement)(
+ return (0, import_react27.createElement)(
"div",
{ className: "bt-mem-skel", "aria-busy": true },
- [0, 1].map((block) => (0, import_react26.createElement)(
+ [0, 1].map((block) => (0, import_react27.createElement)(
"div",
{ key: block, className: "bt-mem-skel-block" },
- (0, import_react26.createElement)("span", { className: "bt-mem-skel-head" }),
- (0, import_react26.createElement)("span", null),
- (0, import_react26.createElement)("span", null),
- (0, import_react26.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } })
+ (0, import_react27.createElement)("span", { className: "bt-mem-skel-head" }),
+ (0, import_react27.createElement)("span", null),
+ (0, import_react27.createElement)("span", null),
+ (0, import_react27.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } })
)),
- label ? (0, import_react26.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
+ label ? (0, import_react27.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
);
}
function Summary({ bot, view, regenerate }) {
if (view.entries.length === 0) {
- return (0, import_react26.createElement)(
+ return (0, import_react27.createElement)(
"div",
{ className: "bt-mem-empty" },
- (0, import_react26.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react26.createElement)(MemoryIcon)),
- (0, import_react26.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })),
- (0, import_react26.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name }))
+ (0, import_react27.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react27.createElement)(MemoryIcon)),
+ (0, import_react27.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })),
+ (0, import_react27.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_react26.createElement)(
+ const failed = view.error && !view.summarizing ? (0, import_react27.createElement)(
"div",
{ className: "bt-mem-failed", role: "alert" },
- (0, import_react26.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })),
- (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again"))
+ (0, import_react27.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_react27.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again"))
) : null;
- if (!view.summary) return (0, import_react26.createElement)(import_react26.Fragment, null, failed, failed ? null : (0, import_react26.createElement)(Skeleton, { label: t("Writing the summary…") }));
- return (0, import_react26.createElement)(
- import_react26.Fragment,
+ if (!view.summary) return (0, import_react27.createElement)(import_react27.Fragment, null, failed, failed ? null : (0, import_react27.createElement)(Skeleton, { label: t("Writing the summary…") }));
+ return (0, import_react27.createElement)(
+ import_react27.Fragment,
null,
failed,
- (0, import_react26.createElement)(
+ (0, import_react27.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_react26.createElement)("h3", { key: index }, block.text) : (0, import_react26.createElement)("p", { key: index }, block.text))
+ summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react27.createElement)("h3", { key: index }, block.text) : (0, import_react27.createElement)("p", { key: index }, block.text))
)
);
}
@@ -12563,9 +12782,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => {
return groups;
}, []);
function EntryRow({ entry, remove }) {
- const [confirming, setConfirming] = (0, import_react26.useState)(false);
- const [busy, setBusy] = (0, import_react26.useState)(false);
- (0, import_react26.useEffect)(() => {
+ const [confirming, setConfirming] = (0, import_react27.useState)(false);
+ const [busy, setBusy] = (0, import_react27.useState)(false);
+ (0, import_react27.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
@@ -12580,16 +12799,16 @@ function EntryRow({ entry, remove }) {
setBusy(true);
if (!await remove(entry)) setBusy(false);
};
- return (0, import_react26.createElement)(
+ return (0, import_react27.createElement)(
"li",
{ className: "bt-mem-entry", "data-busy": busy || void 0 },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-mem-entry-copy" },
- (0, import_react26.createElement)("span", { className: "bt-mem-entry-text" }, entry.text),
- meta ? (0, import_react26.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null
+ (0, import_react27.createElement)("span", { className: "bt-mem-entry-text" }, entry.text),
+ meta ? (0, import_react27.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null
),
- (0, import_react26.createElement)("button", {
+ (0, import_react27.createElement)("button", {
type: "button",
className: "bt-mem-remove",
"data-confirm": confirming || void 0,
@@ -12597,7 +12816,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_react26.createElement)(TrashIcon))
+ }, confirming ? t("Remove") : (0, import_react27.createElement)(TrashIcon))
);
}
function Entries({ bot, view, remove }) {
@@ -12606,25 +12825,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_react26.createElement)(
+ return (0, import_react27.createElement)(
"div",
{ className: "bt-mem-entries" },
- sections.map((section) => (0, import_react26.createElement)(
+ sections.map((section) => (0, import_react27.createElement)(
"section",
{ key: section.scope, className: "bt-mem-section" },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-mem-section-head" },
- (0, import_react26.createElement)("h3", null, section.title),
- section.hint ? (0, import_react26.createElement)("span", null, section.hint) : null,
- (0, import_react26.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) }))
+ (0, import_react27.createElement)("h3", null, section.title),
+ section.hint ? (0, import_react27.createElement)("span", null, section.hint) : null,
+ (0, import_react27.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_react26.createElement)(
- import_react26.Fragment,
+ groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react27.createElement)(
+ import_react27.Fragment,
{ key: group.key },
- group.topic ? (0, import_react26.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null,
- (0, import_react26.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react26.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove })))
- )) : (0, import_react26.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet"))
+ group.topic ? (0, import_react27.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null,
+ (0, import_react27.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react27.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove })))
+ )) : (0, import_react27.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet"))
))
);
}
@@ -12636,40 +12855,40 @@ function changeLine(change) {
return change.scope === "team" ? t("Added to team memory: {text}", { text: text2 }) : t("Added: {text}", { text: text2 });
}
function Reply({ answer, dismiss }) {
- return (0, import_react26.createElement)(
+ return (0, import_react27.createElement)(
"div",
{ className: "bt-mem-reply", role: "status" },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-mem-reply-head" },
- (0, import_react26.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked),
- answer.pending ? null : (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react26.createElement)(CloseIcon))
+ (0, import_react27.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked),
+ answer.pending ? null : (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react27.createElement)(CloseIcon))
),
- answer.pending ? (0, import_react26.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react26.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react26.createElement)(
- import_react26.Fragment,
+ answer.pending ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react27.createElement)(
+ import_react27.Fragment,
null,
- answer.reply ? (0, import_react26.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null,
- answer.changes.length > 0 ? (0, import_react26.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react26.createElement)(
+ answer.reply ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null,
+ answer.changes.length > 0 ? (0, import_react27.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react27.createElement)(
"li",
{ key: index, "data-ok": change.ok || void 0 },
- change.ok ? (0, import_react26.createElement)(CheckIcon) : (0, import_react26.createElement)(CloseIcon),
- (0, import_react26.createElement)("span", null, changeLine(change))
+ change.ok ? (0, import_react27.createElement)(CheckIcon) : (0, import_react27.createElement)(CloseIcon),
+ (0, import_react27.createElement)("span", null, changeLine(change))
))) : null
)
);
}
function useMemoryView(botId, actions) {
- const [view, setView] = (0, import_react26.useState)(null);
- const [error, setError] = (0, import_react26.useState)("");
- const timer = (0, import_react26.useRef)(0);
- const latest = (0, import_react26.useRef)(0);
- const alive = (0, import_react26.useRef)(true);
- const show = (0, import_react26.useCallback)((next) => {
+ const [view, setView] = (0, import_react27.useState)(null);
+ const [error, setError] = (0, import_react27.useState)("");
+ const timer = (0, import_react27.useRef)(0);
+ const latest = (0, import_react27.useRef)(0);
+ const alive = (0, import_react27.useRef)(true);
+ const show = (0, import_react27.useCallback)((next) => {
setView(next);
setError("");
noteCount(botId, next.entries.length);
}, [botId]);
- const load = (0, import_react26.useCallback)(async (flags = {}) => {
+ const load = (0, import_react27.useCallback)(async (flags = {}) => {
clearTimeout(timer.current);
const ticket = ++latest.current;
try {
@@ -12681,7 +12900,7 @@ function useMemoryView(botId, actions) {
if (alive.current && ticket === latest.current) setError(failure?.message ?? String(failure));
}
}, [botId]);
- (0, import_react26.useEffect)(() => {
+ (0, import_react27.useEffect)(() => {
alive.current = true;
void load({ summarize: true });
return () => {
@@ -12692,8 +12911,8 @@ function useMemoryView(botId, actions) {
return { view, error, load, show };
}
function useNow3(every) {
- const [now, setNow] = (0, import_react26.useState)(Date.now());
- (0, import_react26.useEffect)(() => {
+ const [now, setNow] = (0, import_react27.useState)(Date.now());
+ (0, import_react27.useEffect)(() => {
const id = setInterval(() => setNow(Date.now()), every);
return () => clearInterval(id);
}, [every]);
@@ -12701,18 +12920,18 @@ function useNow3(every) {
}
function MemoryDialog({ bot, actions, leaving = false }) {
const { view, error, load, show } = useMemoryView(bot.id, actions);
- const [page, setPage] = (0, import_react26.useState)("summary");
- const [menu, setMenu] = (0, import_react26.useState)(false);
+ const [page, setPage] = (0, import_react27.useState)("summary");
+ const [menu, setMenu] = (0, import_react27.useState)(false);
const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2);
- const [text2, setText] = (0, import_react26.useState)("");
- const [answer, setAnswer] = (0, import_react26.useState)(null);
- const menuRef = (0, import_react26.useRef)(null);
- const dotsRef = (0, import_react26.useRef)(null);
- const inputRef = (0, import_react26.useRef)(null);
+ const [text2, setText] = (0, import_react27.useState)("");
+ const [answer, setAnswer] = (0, import_react27.useState)(null);
+ const menuRef = (0, import_react27.useRef)(null);
+ const dotsRef = (0, import_react27.useRef)(null);
+ const inputRef = (0, import_react27.useRef)(null);
const now = useNow3(3e4);
- const close = (0, import_react26.useCallback)(() => actions.closeMemory(), []);
+ const close = (0, import_react27.useCallback)(() => actions.closeMemory(), []);
useEscape(close);
- (0, import_react26.useEffect)(() => {
+ (0, import_react27.useEffect)(() => {
if (!menu) return void 0;
const onDown = (event) => {
if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return;
@@ -12774,52 +12993,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_react26.createElement)(
+ return (0, import_react27.createElement)(
"div",
{ className: "bt-mem-layer", "data-leaving": leaving || void 0 },
- (0, import_react26.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
+ (0, import_react27.createElement)(
"div",
{ className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-mem-head" },
- page === "entries" ? (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react26.createElement)(ChevronLeftIcon)) : null,
- (0, import_react26.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
- (0, import_react26.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
- (0, import_react26.createElement)("span", { className: "bt-mem-head-gap" }),
- (0, import_react26.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react26.createElement)(DotsIcon)),
- (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react26.createElement)(CloseIcon)),
- menuShown ? (0, import_react26.createElement)(
+ page === "entries" ? (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react27.createElement)(ChevronLeftIcon)) : null,
+ (0, import_react27.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
+ (0, import_react27.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
+ (0, import_react27.createElement)("span", { className: "bt-mem-head-gap" }),
+ (0, import_react27.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_react27.createElement)(DotsIcon)),
+ (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react27.createElement)(CloseIcon)),
+ menuShown ? (0, import_react27.createElement)(
"div",
{ ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 },
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"button",
{ type: "button", role: "menuitem", onClick: openEntries },
- (0, import_react26.createElement)("span", { className: "bt-menu-label" }, t("All entries")),
- (0, import_react26.createElement)("span", { className: "bt-menu-hint" }, String(count))
+ (0, import_react27.createElement)("span", { className: "bt-menu-label" }, t("All entries")),
+ (0, import_react27.createElement)("span", { className: "bt-menu-hint" }, String(count))
),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"button",
{ type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate },
- (0, import_react26.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary"))
+ (0, import_react27.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary"))
)
) : null
),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-mem-body" },
- error ? (0, import_react26.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null,
- view === null ? error ? null : (0, import_react26.createElement)(Skeleton) : page === "entries" ? (0, import_react26.createElement)(Entries, { bot, view, remove }) : (0, import_react26.createElement)(Summary, { bot, view, regenerate })
+ error ? (0, import_react27.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null,
+ view === null ? error ? null : (0, import_react27.createElement)(Skeleton) : page === "entries" ? (0, import_react27.createElement)(Entries, { bot, view, remove }) : (0, import_react27.createElement)(Summary, { bot, view, regenerate })
),
- (0, import_react26.createElement)(
+ (0, import_react27.createElement)(
"div",
{ className: "bt-mem-foot" },
- answer ? (0, import_react26.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
- (0, import_react26.createElement)(
+ answer ? (0, import_react27.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
+ (0, import_react27.createElement)(
"form",
{ className: "bt-mem-ask", onSubmit: ask },
- (0, import_react26.createElement)("input", {
+ (0, import_react27.createElement)("input", {
ref: inputRef,
className: "bt-mem-input",
value: text2,
@@ -12829,10 +13048,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_react26.createElement)(
+ (0, import_react27.createElement)(
"button",
{ type: "submit", className: "bt-mem-send", disabled: busy || text2.trim() === "", "aria-label": t("Send") },
- busy ? (0, import_react26.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react26.createElement)(ArrowUpIcon)
+ busy ? (0, import_react27.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react27.createElement)(ArrowUpIcon)
)
)
)
@@ -12841,7 +13060,7 @@ function MemoryDialog({ bot, actions, leaving = false }) {
}
// src/client/standing-approvals.js
-var import_react27 = require("react");
+var import_react28 = require("react");
var standingOf = (target, roster) => {
const me = roster?.self?.memberId;
return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me);
@@ -12850,19 +13069,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_react27.createElement)(
+ return (0, import_react28.createElement)(
Card2,
{ title: t("Always allowed") },
- grants.map((grant) => (0, import_react27.createElement)(
+ grants.map((grant) => (0, import_react28.createElement)(
"div",
{ key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind },
- (0, import_react27.createElement)(
+ (0, import_react28.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react27.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind),
- (0, import_react27.createElement)("span", { className: "bt-set-hint" }, hint)
+ (0, import_react28.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind),
+ (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint)
),
- (0, import_react27.createElement)("button", {
+ (0, import_react28.createElement)("button", {
type: "button",
className: "bt-soft bt-soft-sm",
"aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }),
@@ -12873,7 +13092,7 @@ function StandingApprovalsCard({ scope, target, roster, actions, run, Card: Card
}
// src/client/bot-settings.js
-var import_react28 = require("react");
+var import_react29 = 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"));
@@ -12897,8 +13116,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_react28.useState)(null);
- const reverting = (0, import_react28.useRef)(false);
+ const [draft, setDraft] = (0, import_react29.useState)(null);
+ const reverting = (0, import_react29.useRef)(false);
const commit = () => {
const next = draft?.trim();
setDraft(null);
@@ -12909,7 +13128,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_react28.createElement)(multiline ? "textarea" : "input", {
+ return (0, import_react29.createElement)(multiline ? "textarea" : "input", {
className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input",
value: draft ?? value ?? "",
placeholder,
@@ -12930,29 +13149,29 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu
});
}
function Row({ label, hint, stack = false, children }) {
- return (0, import_react28.createElement)(
+ return (0, import_react29.createElement)(
"label",
{ className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" },
- (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) : null),
+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react29.createElement)("span", { className: "bt-set-hint" }, hint) : null),
children
);
}
function Card({ title, children }) {
- return (0, import_react28.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react28.createElement)("h3", null, title) : null, (0, import_react28.createElement)("div", { className: "bt-set-card" }, children));
+ return (0, import_react29.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react29.createElement)("h3", null, title) : null, (0, import_react29.createElement)("div", { className: "bt-set-card" }, children));
}
function Switch({ on, label, disabled, onChange }) {
- return (0, import_react28.createElement)(
+ return (0, import_react29.createElement)(
"button",
{ type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) },
- (0, import_react28.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true })
+ (0, import_react29.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true })
);
}
function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) {
- return (0, import_react28.createElement)(
+ return (0, import_react29.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react28.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) : null),
- (0, import_react28.createElement)(Switch, { on, label, disabled, onChange })
+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react29.createElement)("span", { className: "bt-set-hint" }, hint) : null),
+ (0, import_react29.createElement)(Switch, { on, label, disabled, onChange })
);
}
function modelHint(bot, roster) {
@@ -12963,22 +13182,22 @@ function modelHint(bot, roster) {
return t("Applies from the next message.");
}
function ShapeRing({ shape, size }) {
- const maskId = (0, import_react28.useRef)("");
+ const maskId = (0, import_react29.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_react28.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } });
+ return (0, import_react29.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_react28.createElement)(
- import_react28.Fragment,
+ return (0, import_react29.createElement)(
+ import_react29.Fragment,
null,
- (0, import_react28.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react28.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react28.createElement)("button", {
+ (0, import_react29.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react29.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react29.createElement)("button", {
key: id,
type: "button",
className: "bt-shape",
@@ -12986,8 +13205,8 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) {
title: t(shapeOf(id).label),
"aria-pressed": !hasPhoto && look.shape === id,
onClick: () => onShape(id)
- }, (0, import_react28.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react28.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))),
- (0, import_react28.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react28.createElement)("button", {
+ }, (0, import_react29.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react29.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))),
+ (0, import_react29.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react29.createElement)("button", {
key: color,
type: "button",
className: "bt-color",
@@ -12995,19 +13214,19 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) {
title: t(capitalize(color)),
"aria-pressed": look.color === color,
onClick: () => onColor(color)
- }, (0, import_react28.createElement)("span", { style: { background: paintCss(color) } }))))
+ }, (0, import_react29.createElement)("span", { style: { background: paintCss(color) } }))))
);
}
function AvatarEditor({ bot, actions, run, onClose }) {
const look = lookOf(bot);
- const [tab, setTab] = (0, import_react28.useState)(look.image ? "upload" : "bot");
- const [over, setOver] = (0, import_react28.useState)(false);
- const ref = (0, import_react28.useRef)(null);
- const fileRef = (0, import_react28.useRef)(null);
+ const [tab, setTab] = (0, import_react29.useState)(look.image ? "upload" : "bot");
+ const [over, setOver] = (0, import_react29.useState)(false);
+ const ref = (0, import_react29.useRef)(null);
+ const fileRef = (0, import_react29.useRef)(null);
const upload = (file) => {
if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } })));
};
- (0, import_react28.useEffect)(() => {
+ (0, import_react29.useEffect)(() => {
const onDown = (event) => {
if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return;
onClose();
@@ -13025,7 +13244,7 @@ function AvatarEditor({ bot, actions, run, onClose }) {
window.removeEventListener("keydown", onKey, true);
};
}, [onClose]);
- (0, import_react28.useEffect)(() => {
+ (0, import_react29.useEffect)(() => {
if (tab !== "upload") return void 0;
const onPaste = (event) => {
const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/"));
@@ -13039,32 +13258,32 @@ function AvatarEditor({ bot, actions, run, onClose }) {
}, [tab, bot]);
const hasPhoto = Boolean(look.image);
const glide = useGlide(tab);
- return (0, import_react28.createElement)(
+ return (0, import_react29.createElement)(
"div",
{ ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ className: "bt-avedit-top" },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" },
- (0, import_react28.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react28.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label)))
+ (0, import_react29.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react29.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label)))
),
- (0, import_react28.createElement)("button", {
+ (0, import_react29.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_react28.createElement)(CharacterGrid, {
+ tab === "bot" ? (0, import_react29.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_react28.createElement)(
+ tab === "upload" ? (0, import_react29.createElement)(
"div",
{
className: "bt-dropzone",
@@ -13080,15 +13299,15 @@ function AvatarEditor({ bot, actions, run, onClose }) {
upload(event.dataTransfer.files?.[0]);
}
},
- hasPhoto ? (0, import_react28.createElement)("img", { src: look.image, alt: "" }) : null,
- (0, import_react28.createElement)("div", null, t("Drag, drop, or paste an image")),
- (0, import_react28.createElement)(
+ hasPhoto ? (0, import_react29.createElement)("img", { src: look.image, alt: "" }) : null,
+ (0, import_react29.createElement)("div", null, t("Drag, drop, or paste an image")),
+ (0, import_react29.createElement)(
"div",
{ className: "bt-actions", style: { justifyContent: "center" } },
- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")),
- hasPhoto ? (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null
+ (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")),
+ hasPhoto ? (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null
),
- (0, import_react28.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
+ (0, import_react29.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
const file = event.target.files?.[0];
event.target.value = "";
upload(file);
@@ -13099,22 +13318,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_react28.createElement)(
+ return (0, import_react29.createElement)(
Card,
{ title: t("Memory") },
- (0, import_react28.createElement)(
+ (0, import_react29.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_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label)),
- (0, import_react28.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react28.createElement)(ChevronRightIcon))
+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label)),
+ (0, import_react29.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react29.createElement)(ChevronRightIcon))
)
);
}
function BotSettingsForm({ bot, roster, actions, run }) {
- const [editing, setEditing] = (0, import_react28.useState)(false);
- const [confirming, setConfirming] = (0, import_react28.useState)(false);
- const closeEditor = (0, import_react28.useCallback)(() => setEditing(false), []);
- (0, import_react28.useEffect)(() => {
+ const [editing, setEditing] = (0, import_react29.useState)(false);
+ const [confirming, setConfirming] = (0, import_react29.useState)(false);
+ const closeEditor = (0, import_react29.useCallback)(() => setEditing(false), []);
+ (0, import_react29.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 4e3);
return () => clearTimeout(timer);
@@ -13129,116 +13348,116 @@ function BotSettingsForm({ bot, roster, actions, run }) {
if (confirming) void run(actions.deleteBot(bot.id));
else setConfirming(true);
};
- (0, import_react28.useEffect)(() => {
+ (0, import_react29.useEffect)(() => {
void actions.refreshModels?.();
}, [bot.id]);
- return (0, import_react28.createElement)(
+ return (0, import_react29.createElement)(
"div",
{ className: "bt-bs" },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ className: "bt-bs-hero" },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ className: "bt-avatar-anchor" },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"button",
{ type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) },
- (0, import_react28.createElement)(BotMark, { bot, size: 64, live: true }),
- (0, import_react28.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react28.createElement)(PencilIcon))
+ (0, import_react29.createElement)(BotMark, { bot, size: 64, live: true }),
+ (0, import_react29.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react29.createElement)(PencilIcon))
),
- editing ? (0, import_react28.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
+ editing ? (0, import_react29.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
),
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ className: "bt-bs-who" },
- (0, import_react28.createElement)("span", { className: "bt-bs-name" }, bot.name),
- (0, import_react28.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
+ (0, import_react29.createElement)("span", { className: "bt-bs-name" }, bot.name),
+ (0, import_react29.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
)
),
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
Card,
{ title: t("Profile") },
- (0, import_react28.createElement)(Row, { label: t("Name") }, (0, import_react28.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })),
- (0, import_react28.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react28.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) }))
+ (0, import_react29.createElement)(Row, { label: t("Name") }, (0, import_react29.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })),
+ (0, import_react29.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react29.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) }))
),
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
Card,
{ title: t("Model") },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))),
- (0, import_react28.createElement)(ModelMenu, { bot, roster, actions, run })
+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))),
+ (0, import_react29.createElement)(ModelMenu, { bot, roster, actions, run })
),
- bot.model ? (0, import_react28.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null
+ bot.model ? (0, import_react29.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null
),
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
Card,
{ title: t("Working folder") },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
- (0, import_react28.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) })
+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
+ (0, import_react29.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) })
)
),
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
Card,
{ title: t("Instructions") },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
Row,
{ label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true },
- (0, import_react28.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) })
+ (0, import_react29.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_react28.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react28.createElement)(Card, { title: t("Sharing") }, children) }),
- (0, import_react28.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }),
- (0, import_react28.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }),
- roster.memory === false ? null : (0, import_react28.createElement)(MemoryCard, { bot, actions }),
- (0, import_react28.createElement)(BotSchedules, { bot, roster, actions }),
- (0, import_react28.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }),
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react29.createElement)(Card, { title: t("Sharing") }, children) }),
+ (0, import_react29.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }),
+ (0, import_react29.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }),
+ roster.memory === false ? null : (0, import_react29.createElement)(MemoryCard, { bot, actions }),
+ (0, import_react29.createElement)(BotSchedules, { bot, roster, actions }),
+ (0, import_react29.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }),
+ (0, import_react29.createElement)(
Card,
{ title: t("Main Bot") },
- onlyMain ? (0, import_react28.createElement)(
+ onlyMain ? (0, import_react29.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react28.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page."))
+ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page."))
),
- (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change"))
- ) : (0, import_react28.createElement)(SwitchRow, { label: t("Main Bot"), showLabel: false, hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) })
+ (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change"))
+ ) : (0, import_react29.createElement)(SwitchRow, { label: t("Main Bot"), showLabel: false, hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) })
),
// The Main Bot tile is always first and always shown, like in the context menu.
- isTile ? null : (0, import_react28.createElement)(
+ isTile ? null : (0, import_react29.createElement)(
Card,
{ title: t("Sidebar") },
- (0, import_react28.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }),
- isMainBot ? null : (0, import_react28.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) })
+ (0, import_react29.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_react29.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) })
),
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
Card,
{ title: t("More") },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"button",
{ type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate },
- (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react28.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))),
- (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon))
+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))),
+ (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon))
),
- isMainBot ? null : (0, import_react28.createElement)(
+ isMainBot ? null : (0, import_react29.createElement)(
"button",
{ type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove },
- (0, import_react28.createElement)(
+ (0, import_react29.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react28.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")),
- (0, import_react28.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team."))
+ (0, import_react29.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")),
+ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team."))
),
- (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon))
+ (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon))
)
)
);
@@ -13294,7 +13513,7 @@ ${MODEL_TEST_CSS}
`;
// src/client/group-settings.js
-var import_react29 = require("react");
+var import_react30 = 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."]
@@ -13326,9 +13545,9 @@ function faceMarks(person, roster, size) {
for (const seat of person?.seats ?? []) {
const bot = faceSeatBot(seat, roster);
if (!bot) continue;
- marks.push((0, import_react29.createElement)(BotMark, { key: seat.key, bot, size, live: false }));
+ marks.push((0, import_react30.createElement)(BotMark, { key: seat.key, bot, size, live: false }));
}
- if (marks.length === 0) return [(0, import_react29.createElement)(PersonMark, { key: "person", size, name: person?.name })];
+ if (marks.length === 0) return [(0, import_react30.createElement)(PersonMark, { key: "person", size, name: person?.name })];
return marks;
}
function faceRoomCount(room2, roster) {
@@ -13342,31 +13561,31 @@ var handleKey = (handle) => String(handle ?? "").trim().replace(/^@+/, "").toLow
var isLeadSeat = (seat, lead) => Boolean(handleKey(lead) && handleKey(seat.record?.handle) === handleKey(lead));
function FacePersonRow({ person, onOpenBot, lead, roster, className = "bt-set-row bt-bs-row bt-gs-member", size = 26 }) {
const bots = person.seats.flatMap((seat, index) => {
- const name = seat.local && onOpenBot ? (0, import_react29.createElement)("button", { key: seat.key, type: "button", className: "bt-face-bot", title: t("Open {name}'s chat", { name: seat.bot }), onClick: () => onOpenBot(seat.local.id) }, seat.bot) : (0, import_react29.createElement)("span", { key: seat.key, className: "bt-face-bot", title: seat.record?.handle || void 0 }, seat.bot);
- const marked = isLeadSeat(seat, lead) ? [name, (0, import_react29.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name];
- return index === 0 ? marked : [(0, import_react29.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked];
+ const name = seat.local && onOpenBot ? (0, import_react30.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_react30.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_react30.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name];
+ return index === 0 ? marked : [(0, import_react30.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked];
});
- return (0, import_react29.createElement)(
+ return (0, import_react30.createElement)(
"div",
{ className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"span",
{ className: "bt-gs-who bt-face-person" },
- (0, import_react29.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)),
+ (0, import_react30.createElement)(
"span",
{ className: "bt-face-person-text" },
- (0, import_react29.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
- (0, import_react29.createElement)("span", { className: "bt-face-bots" }, bots)
+ (0, import_react30.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
+ (0, import_react30.createElement)("span", { className: "bt-face-bots" }, bots)
)
)
);
}
function FaceLead({ room: room2, roster, pick: pick2 }) {
const lead = room2.face?.lead;
- const [open, setOpen] = (0, import_react29.useState)(false);
- const [busy, setBusy] = (0, import_react29.useState)(false);
- const [error, setError] = (0, import_react29.useState)("");
+ const [open, setOpen] = (0, import_react30.useState)(false);
+ const [busy, setBusy] = (0, import_react30.useState)(false);
+ const [error, setError] = (0, import_react30.useState)("");
const seats = faceSeats(room2, roster);
const current = seats.find((seat) => isLeadSeat(seat, lead?.handle));
if (!current) return null;
@@ -13383,30 +13602,30 @@ function FaceLead({ room: room2, roster, pick: pick2 }) {
setBusy(false);
}
};
- return (0, import_react29.createElement)(
+ return (0, import_react30.createElement)(
"div",
{ className: "bt-face-lead", "data-flybot": "face-lead" },
- (0, import_react29.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
+ (0, import_react30.createElement)(
"div",
{ className: "bt-set-row" },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react29.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)),
- (0, import_react29.createElement)("span", { className: "bt-set-hint" }, lead.picked ? t("Writes the plan and gives each Bot its part.") : t("Default: the first Bot of the room creator that joined."))
+ (0, import_react30.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)),
+ (0, import_react30.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_react29.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null
+ canPick ? (0, import_react30.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_react29.createElement)(
+ open && canPick ? seats.map((seat) => (0, import_react30.createElement)(
"div",
{ key: seat.key, className: "bt-set-row" },
- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)),
- seat === current ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead"))
+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)),
+ seat === current ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react30.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_react29.createElement)("button", { type: "button", className: "bt-more", disabled: busy, onClick: () => void choose(null) }, t("Use the default lead")) : null,
- canPick || !lead.supported ? null : (0, import_react29.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")),
- error ? (0, import_react29.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null
+ open && canPick && lead.picked ? (0, import_react30.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_react30.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")),
+ error ? (0, import_react30.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null
);
}
function seatedCount(room2) {
@@ -13422,9 +13641,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_react29.useState)(false);
- const [confirming, setConfirming] = (0, import_react29.useState)(false);
- (0, import_react29.useEffect)(() => {
+ const [adding, setAdding] = (0, import_react30.useState)(false);
+ const [confirming, setConfirming] = (0, import_react30.useState)(false);
+ (0, import_react30.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 4e3);
return () => clearTimeout(timer);
@@ -13462,41 +13681,41 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
if (done) onDeleted?.();
});
};
- return (0, import_react29.createElement)(
+ return (0, import_react30.createElement)(
"div",
{ className: "bt-bs" },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"div",
{ className: "bt-bs-hero" },
- (0, import_react29.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
+ (0, import_react30.createElement)(
"div",
{ className: "bt-bs-who" },
- (0, import_react29.createElement)("span", { className: "bt-bs-name" }, room2.name),
- (0, import_react29.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
+ (0, import_react30.createElement)("span", { className: "bt-bs-name" }, room2.name),
+ (0, import_react30.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
)
),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
Card,
{ title: t("Profile") },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
Row,
{ label: t("Name"), hint: t("Leave it empty to name the group after its members.") },
- (0, import_react29.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) })
+ (0, import_react30.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) })
),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
Row,
{ label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true },
- (0, import_react29.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) })
+ (0, import_react30.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) })
)
),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
Card,
{ title: t("Who answers") },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react29.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react29.createElement)("button", {
+ (0, import_react30.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react30.createElement)("button", {
key: id,
type: "button",
"aria-pressed": mode === id,
@@ -13506,108 +13725,108 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
if (mode !== id) void update({ mode: id });
}
}, t(label)))),
- (0, import_react29.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
+ (0, import_react30.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
)
),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
Card,
{ title: t("Working folder") },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
- (0, import_react29.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) })
+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
+ (0, import_react30.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) })
)
),
- face ? (0, import_react29.createElement)(
+ face ? (0, import_react30.createElement)(
Card,
{ title: t("Members · {count}", { count: people2.length }) },
- people2.map((person) => (0, import_react29.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
+ people2.map((person) => (0, import_react30.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
) : null,
- face ? null : (0, import_react29.createElement)(
+ face ? null : (0, import_react30.createElement)(
Card,
{ title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) },
- humans.map((person) => (0, import_react29.createElement)(
+ humans.map((person) => (0, import_react30.createElement)(
"div",
{ key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"span",
{ className: "bt-gs-who" },
- (0, import_react29.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id),
- (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Human")),
- person.role === "admin" ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Admin")) : null
+ (0, import_react30.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id),
+ (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Human")),
+ person.role === "admin" ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Admin")) : null
),
- humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null
+ humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react30.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null
)),
members.map((member) => {
const leads = room2.admin === member.id;
const boardHere = room2.origin !== "face-to-face" && room2.origin !== "inbound-share";
const taskLead = room2.taskLead === member.id;
- return (0, import_react29.createElement)(
+ return (0, import_react30.createElement)(
"div",
{ key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"button",
{ type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) },
- (0, import_react29.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
- (0, import_react29.createElement)("span", { className: "bt-row-title" }, member.name),
- leads ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Admin")) : null,
- taskLead ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null,
- (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Chief")) : null
+ (0, import_react30.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
+ (0, import_react30.createElement)("span", { className: "bt-row-title" }, member.name),
+ leads ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Admin")) : null,
+ taskLead ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null,
+ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Chief")) : null
),
- leads ? (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")),
- boardHere ? taskLead ? (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null,
- members.length > 2 ? (0, import_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null
+ leads ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")),
+ boardHere ? taskLead ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null,
+ members.length > 2 ? (0, import_react30.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_react29.createElement)(
+ remotes.map((person) => (0, import_react30.createElement)(
"div",
{ key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"span",
{ className: "bt-gs-who" },
- (0, import_react29.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
- (0, import_react29.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)),
+ (0, import_react30.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
+ (0, import_react30.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_react29.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_react30.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_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: remoteBotTitle(person) }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "bot", id: person.handle || person.id, remote: true }] }) }, "×")
+ room2.origin === "face-to-face" || room2.origin === "inbound-share" || members.length + remotes.length <= 2 ? null : (0, import_react30.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_react29.createElement)(
+ ...outsidePeople.map((person) => (0, import_react30.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_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)),
- (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add person"))
+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)),
+ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add person"))
)),
- ...outside.map((bot) => (0, import_react29.createElement)(
+ ...outside.map((bot) => (0, import_react30.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_react29.createElement)(BotMark, { bot, size: 22 }),
- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, bot.name)),
- (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add"))
+ (0, import_react30.createElement)(BotMark, { bot, size: 22 }),
+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, bot.name)),
+ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add"))
)),
- ...outsideRemotes.map((bot) => (0, import_react29.createElement)(
+ ...outsideRemotes.map((bot) => (0, import_react30.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_react29.createElement)(BotAvatar, { bot: lookBotForRemote({ handle: bot.handle, displayName: bot.name, avatar: bot.avatar, color: bot.color }, roster), size: 22, badge: false, live: false }),
- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))),
- (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot"))
+ (0, import_react30.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_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))),
+ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot"))
)),
- (0, import_react29.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel"))
- ] : outside.length + outsidePeople.length + outsideRemotes.length > 0 ? (0, import_react29.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react29.createElement)(PlusIcon), t("Add member")) : null,
- (0, import_react29.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react29.createElement)(
+ (0, import_react30.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_react30.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react30.createElement)(PlusIcon), t("Add member")) : null,
+ (0, import_react30.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react30.createElement)(
"span",
{ className: "bt-set-hint" },
// The host's two-Bot floor guards membership changes; this hint warns before one.
@@ -13615,37 +13834,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_react29.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
+ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react30.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
),
- (0, import_react29.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
+ (0, import_react30.createElement)(
Card,
{ title: t("Sidebar") },
- (0, import_react29.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }),
- (0, import_react29.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) })
+ (0, import_react30.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_react30.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) })
),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
Card,
{ title: t("More") },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"button",
{ type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react29.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")),
- (0, import_react29.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team."))
+ (0, import_react30.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")),
+ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team."))
),
- (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon))
+ (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon))
)
)
);
}
function NewGroupForm({ roster, actions, run, onCreated, onCancel }) {
- const [admin, setAdmin] = (0, import_react29.useState)(null);
- const [members, setMembers] = (0, import_react29.useState)([]);
- const [name, setName] = (0, import_react29.useState)("");
- const [busy, setBusy] = (0, import_react29.useState)(false);
+ const [admin, setAdmin] = (0, import_react30.useState)(null);
+ const [members, setMembers] = (0, import_react30.useState)([]);
+ const [name, setName] = (0, import_react30.useState)("");
+ const [busy, setBusy] = (0, import_react30.useState)(false);
const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]);
const pickAdmin = (id) => {
setAdmin(id);
@@ -13661,48 +13880,48 @@ function NewGroupForm({ roster, actions, run, onCreated, onCancel }) {
if (done && room2) onCreated(room2);
else setBusy(false);
};
- const chip = (bot, on, onClick, role) => (0, import_react29.createElement)("button", {
+ const chip = (bot, on, onClick, role) => (0, import_react30.createElement)("button", {
key: bot.id,
type: "button",
role,
className: "bt-ng-chip",
"aria-checked": on,
onClick
- }, (0, import_react29.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react29.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react29.createElement)(AdminStar) : null);
- return (0, import_react29.createElement)(
+ }, (0, import_react30.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react30.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react30.createElement)(AdminStar) : null);
+ return (0, import_react30.createElement)(
"div",
{ className: "bt-set-card bt-ng" },
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react29.createElement)(AdminStar), t("Choose the group admin"))),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react30.createElement)(AdminStar), t("Choose the group admin"))),
+ (0, import_react30.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_react29.createElement)(
+ admin === null ? null : (0, import_react30.createElement)(
"div",
{ className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" },
- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))),
+ (0, import_react30.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_react29.createElement)(
+ admin === null || members.length === 0 ? null : (0, import_react30.createElement)(
"label",
{ className: "bt-set-row bt-bs-row bt-ng-step" },
- (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
- (0, import_react29.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) })
+ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
+ (0, import_react30.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) })
),
- (0, import_react29.createElement)(
+ (0, import_react30.createElement)(
"div",
{ className: "bt-set-row bt-task-foot" },
- (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
- (0, import_react29.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat"))
+ (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
+ (0, import_react30.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat"))
)
);
}
@@ -13738,14 +13957,14 @@ button.bt-face-bot:hover{color:var(--bt-ink);text-decoration:underline;text-unde
`;
// src/client/palette.js
-var import_react30 = require("react");
+var import_react31 = require("react");
var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
var paletteText = (raw, names) => readableLine(String(raw ?? ""), names);
var PALETTE_EXIT_MS = 160;
function CommandPalette({ start, token, roster, actions, useSessions }) {
- const [query, setQuery] = (0, import_react30.useState)(start ?? "");
- const [cursor, setCursor] = (0, import_react30.useState)(0);
- const [leaving, setLeaving] = (0, import_react30.useState)(false);
+ const [query, setQuery] = (0, import_react31.useState)(start ?? "");
+ const [cursor, setCursor] = (0, import_react31.useState)(0);
+ const [leaving, setLeaving] = (0, import_react31.useState)(false);
const wanted = query.trim().toLowerCase();
const close = () => {
if (leaving) return;
@@ -13782,7 +14001,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_react30.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }),
+ avatar: (0, import_react31.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,
@@ -13793,7 +14012,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) {
})),
...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({
key: room2.id,
- avatar: (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
+ avatar: (0, import_react31.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,
@@ -13806,7 +14025,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_react30.createElement)("span", { className: "bt-palette-agent" }, (0, import_react30.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
+ avatar: (0, import_react31.createElement)("span", { className: "bt-palette-agent" }, (0, import_react31.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
title: "DSH Agent",
subtitle: t("Plain dsh session"),
run: () => actions.openAgent(agent.id)
@@ -13819,16 +14038,16 @@ function CommandPalette({ start, token, roster, actions, useSessions }) {
key: `${id}:${turn}`,
title: bot?.name ?? room2?.name ?? t("Bot"),
subtitle: snippet,
- avatar: bot ? (0, import_react30.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
+ avatar: bot ? (0, import_react31.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react31.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_react30.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") },
- { key: "cmd:bot", icon: (0, import_react30.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") },
- { key: "cmd:group", icon: (0, import_react30.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") },
- { key: "cmd:settings", icon: (0, import_react30.createElement)(GearIcon), title: t("Settings"), run: () => actions.openSettings("general") }
+ { key: "cmd:new", icon: (0, import_react31.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") },
+ { key: "cmd:bot", icon: (0, import_react31.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") },
+ { key: "cmd:group", icon: (0, import_react31.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") },
+ { key: "cmd:settings", icon: (0, import_react31.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);
@@ -13838,7 +14057,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) {
${shown}`, { axis: "y", ...QUICK_EDGES });
const listRef = glide.box;
useFlip(listRef, shown, "[data-flip]");
- (0, import_react30.useEffect)(() => {
+ (0, import_react31.useEffect)(() => {
listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" });
}, [active, query]);
const choose = (row) => {
@@ -13847,18 +14066,18 @@ ${shown}`, { axis: "y", ...QUICK_EDGES });
close();
};
let index = -1;
- return (0, import_react30.createElement)(
+ return (0, import_react31.createElement)(
"div",
{ className: "bt-palette-layer", "data-leaving": leaving || void 0 },
- (0, import_react30.createElement)("div", { className: "bt-backdrop", onMouseDown: close }),
- (0, import_react30.createElement)(
+ (0, import_react31.createElement)("div", { className: "bt-backdrop", onMouseDown: close }),
+ (0, import_react31.createElement)(
"div",
{ className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true },
- (0, import_react30.createElement)(
+ (0, import_react31.createElement)(
"div",
{ className: "bt-palette-head" },
- (0, import_react30.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react30.createElement)(SearchIcon)),
- (0, import_react30.createElement)("input", {
+ (0, import_react31.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react31.createElement)(SearchIcon)),
+ (0, import_react31.createElement)("input", {
autoFocus: true,
value: query,
placeholder: t("Search Bots, chats and messages"),
@@ -13884,29 +14103,29 @@ ${shown}`, { axis: "y", ...QUICK_EDGES });
}
}
}),
- query ? (0, import_react30.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
+ query ? (0, import_react31.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
setQuery("");
setCursor(0);
- } }, (0, import_react30.createElement)(CloseIcon)) : null
+ } }, (0, import_react31.createElement)(CloseIcon)) : null
),
- (0, import_react30.createElement)(
+ (0, import_react31.createElement)(
"div",
{ ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") },
- (0, import_react30.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- rows.length === 0 ? (0, import_react30.createElement)(
+ (0, import_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ rows.length === 0 ? (0, import_react31.createElement)(
"div",
{ className: "bt-palette-empty" },
- (0, import_react30.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react30.createElement)(SearchIcon)),
- (0, import_react30.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")),
- (0, import_react30.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message"))
- ) : sections.map(([title, items]) => (0, import_react30.createElement)(
- import_react30.Fragment,
+ (0, import_react31.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react31.createElement)(SearchIcon)),
+ (0, import_react31.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")),
+ (0, import_react31.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message"))
+ ) : sections.map(([title, items]) => (0, import_react31.createElement)(
+ import_react31.Fragment,
{ key: title },
- (0, import_react30.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
+ (0, import_react31.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
items.map((row) => {
index += 1;
const at = index;
- return (0, import_react30.createElement)(
+ return (0, import_react31.createElement)(
"button",
{
key: row.key,
@@ -13920,14 +14139,14 @@ ${shown}`, { axis: "y", ...QUICK_EDGES });
},
onClick: () => choose(row)
},
- (0, import_react30.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react30.createElement)("span", { className: "bt-palette-cmd" }, row.icon)),
- (0, import_react30.createElement)(
+ (0, import_react31.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react31.createElement)("span", { className: "bt-palette-cmd" }, row.icon)),
+ (0, import_react31.createElement)(
"span",
{ className: "bt-palette-text" },
- (0, import_react30.createElement)("span", { className: "bt-palette-title" }, row.title),
- row.subtitle ? (0, import_react30.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null
+ (0, import_react31.createElement)("span", { className: "bt-palette-title" }, row.title),
+ row.subtitle ? (0, import_react31.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null
),
- row.badge ? (0, import_react30.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null
+ row.badge ? (0, import_react31.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null
);
})
))
@@ -13969,7 +14188,7 @@ async function afterCreate({ id, text: text2, actions }) {
}
// src/client/create-bot.js
-var import_react31 = require("react");
+var import_react32 = require("react");
var pick = (list) => list[Math.floor(Math.random() * list.length)];
var randomLook = (avoid = {}) => {
const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape);
@@ -13977,17 +14196,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_react31.useState)(() => randomLook());
- const [image, setImage] = (0, import_react31.useState)(null);
- const [name, setName] = (0, import_react31.useState)(start?.name ?? "");
- const [model, setModel] = (0, import_react31.useState)(() => startModel(roster));
- const [effort, setEffort] = (0, import_react31.useState)(void 0);
- const [busy, setBusy] = (0, import_react31.useState)(false);
- const [error, setError] = (0, import_react31.useState)("");
- const fileRef = (0, import_react31.useRef)(null);
- const frame = (0, import_react31.useRef)(null);
+ const [look, setLook] = (0, import_react32.useState)(() => randomLook());
+ const [image, setImage] = (0, import_react32.useState)(null);
+ const [name, setName] = (0, import_react32.useState)(start?.name ?? "");
+ const [model, setModel] = (0, import_react32.useState)(() => startModel(roster));
+ const [effort, setEffort] = (0, import_react32.useState)(void 0);
+ const [busy, setBusy] = (0, import_react32.useState)(false);
+ const [error, setError] = (0, import_react32.useState)("");
+ const fileRef = (0, import_react32.useRef)(null);
+ const frame = (0, import_react32.useRef)(null);
useEscape(onBack);
- (0, import_react31.useEffect)(() => {
+ (0, import_react32.useEffect)(() => {
if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" });
}, []);
const fallbackName = freshBotName(roster, t("New Bot"));
@@ -14012,25 +14231,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_react31.createElement)(
+ const card = (0, import_react32.createElement)(
"div",
{ className: "bt-create-card" },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-create-preview" },
// The key remounts the mark, so every new pick plays the pop.
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"span",
{ key: `${look.shape}:${look.color}:${image ? "photo" : ""}`, className: "bt-create-pop" },
- (0, import_react31.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true })
+ (0, import_react32.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true })
),
- (0, import_react31.createElement)("span", { className: "bt-create-name" }, finalName)
+ (0, import_react32.createElement)("span", { className: "bt-create-name" }, finalName)
),
- (0, import_react31.createElement)("div", { className: "bt-create-step" }, t("Look")),
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)("div", { className: "bt-create-step" }, t("Look")),
+ (0, import_react32.createElement)(
"div",
{ className: "bt-create-looks" },
- (0, import_react31.createElement)(CharacterGrid, {
+ (0, import_react32.createElement)(CharacterGrid, {
look,
hasPhoto: Boolean(image),
onShape: (shape) => {
@@ -14042,27 +14261,27 @@ function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inlin
setLook((current) => ({ ...current, color }));
}
}),
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-actions bt-create-lookbar" },
- (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
+ (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
setImage(null);
setLook((current) => randomLook(current));
} }, t("Shuffle")),
- (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")),
- image ? (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null,
- (0, import_react31.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
+ (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")),
+ image ? (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null,
+ (0, import_react32.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_react31.createElement)(
+ (0, import_react32.createElement)(
"label",
{ className: "bt-field" },
- (0, import_react31.createElement)("span", { className: "bt-field-label" }, t("Name")),
- (0, import_react31.createElement)("input", {
+ (0, import_react32.createElement)("span", { className: "bt-field-label" }, t("Name")),
+ (0, import_react32.createElement)("input", {
className: "bt-input",
autoFocus: !inline,
value: name,
@@ -14077,59 +14296,59 @@ function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inlin
}
}
}),
- problem ? (0, import_react31.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react31.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null
+ problem ? (0, import_react32.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react32.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null
),
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-field" },
- (0, import_react31.createElement)("span", { className: "bt-field-label" }, t("Model")),
- (0, import_react31.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => {
+ (0, import_react32.createElement)("span", { className: "bt-field-label" }, t("Model")),
+ (0, import_react32.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_react31.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later."))
+ (0, import_react32.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_react31.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
- (0, import_react31.createElement)(
+ error ? (0, import_react32.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
+ (0, import_react32.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_react31.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null,
- (0, import_react31.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create"))
+ inline ? (0, import_react32.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null,
+ (0, import_react32.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_react31.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card);
- return (0, import_react31.createElement)(
+ if (inline) return (0, import_react32.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card);
+ return (0, import_react32.createElement)(
"div",
{ className: "bt-create", role: "dialog", "aria-label": t("Create Bot") },
- (0, import_react31.createElement)(
+ (0, import_react32.createElement)(
"div",
{ className: "bt-create-head" },
- canGoBack ? (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react31.createElement)(ChevronLeftIcon)) : (0, import_react31.createElement)("span", { className: "bt-create-gap" }),
- (0, import_react31.createElement)("span", { className: "bt-create-title" }, t("Create Bot")),
- (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react31.createElement)(CloseIcon))
+ canGoBack ? (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react32.createElement)(ChevronLeftIcon)) : (0, import_react32.createElement)("span", { className: "bt-create-gap" }),
+ (0, import_react32.createElement)("span", { className: "bt-create-title" }, t("Create Bot")),
+ (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react32.createElement)(CloseIcon))
),
- (0, import_react31.createElement)("div", { className: "bt-create-body" }, card)
+ (0, import_react32.createElement)("div", { className: "bt-create-body" }, card)
);
}
// src/client/new-chat.js
-var import_react32 = require("react");
+var import_react33 = require("react");
function NewChat({ mode, roster, actions }) {
- const [query, setQuery] = (0, import_react32.useState)("");
- const [picked, setPicked] = (0, import_react32.useState)([]);
- const [group, setGroup] = (0, import_react32.useState)(mode === "group");
- const [compose, setCompose] = (0, import_react32.useState)(mode !== "new");
- const [admin, setAdmin] = (0, import_react32.useState)(null);
- const [message, setMessage] = (0, import_react32.useState)("");
- const [busy, setBusy] = (0, import_react32.useState)(false);
- const [error, setError] = (0, import_react32.useState)("");
- const [fix, setFix] = (0, import_react32.useState)(null);
- const [fixNote, setFixNote] = (0, import_react32.useState)("");
- const [inviteLink, setInviteLink] = (0, import_react32.useState)("");
- const [cursor, setCursor] = (0, import_react32.useState)(0);
- const [draft, setDraft] = (0, import_react32.useState)(mode === "create" ? { name: "", fromList: false } : null);
- const inputRef = (0, import_react32.useRef)(null);
+ const [query, setQuery] = (0, import_react33.useState)("");
+ const [picked, setPicked] = (0, import_react33.useState)([]);
+ const [group, setGroup] = (0, import_react33.useState)(mode === "group");
+ const [compose, setCompose] = (0, import_react33.useState)(mode !== "new");
+ const [admin, setAdmin] = (0, import_react33.useState)(null);
+ const [message, setMessage] = (0, import_react33.useState)("");
+ const [busy, setBusy] = (0, import_react33.useState)(false);
+ const [error, setError] = (0, import_react33.useState)("");
+ const [fix, setFix] = (0, import_react33.useState)(null);
+ const [fixNote, setFixNote] = (0, import_react33.useState)("");
+ const [inviteLink, setInviteLink] = (0, import_react33.useState)("");
+ const [cursor, setCursor] = (0, import_react33.useState)(0);
+ const [draft, setDraft] = (0, import_react33.useState)(mode === "create" ? { name: "", fromList: false } : null);
+ const inputRef = (0, import_react33.useRef)(null);
const close = () => actions.closeOverlay();
useEscape(close);
const paneLeft = usePaneLeft();
@@ -14260,14 +14479,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_react32.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => {
+ options.push({ key: "chat", icon: (0, import_react33.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_react32.createElement)(BotIcon), label: t("New Bot"), hint: t("Set up a Bot with its own role"), run: createBot });
- options.push({ key: "group", icon: (0, import_react32.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => {
+ options.push({ key: "new", icon: (0, import_react33.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_react33.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => {
setGroup(true);
setCompose(true);
setCursor(0);
@@ -14276,7 +14495,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_react32.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react32.createElement)(PeopleIcon) }) : (0, import_react32.createElement)(PeopleIcon),
+ icon: action.icon ? (0, import_react33.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react33.createElement)(PeopleIcon) }) : (0, import_react33.createElement)(PeopleIcon),
label: t(action.label),
hint: action.hint ? t(action.hint) : void 0,
run: () => {
@@ -14288,7 +14507,7 @@ function NewChat({ mode, roster, actions }) {
});
}
if ((roster.agents ?? []).length === 0) {
- options.push({ key: "agent", icon: (0, import_react32.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => {
+ options.push({ key: "agent", icon: (0, import_react33.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => {
await actions.addAgent();
close();
}) });
@@ -14299,19 +14518,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_react32.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot });
+ if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react33.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_react32.useRef)(null);
+ const drop = (0, import_react33.useRef)(null);
useMorph(drop, listed);
if (draft) {
- return (0, import_react32.createElement)(
+ return (0, import_react33.createElement)(
"div",
{ className: "bt-pane", style: { left: paneLeft } },
- (0, import_react32.createElement)(CreateBot, {
+ (0, import_react33.createElement)(CreateBot, {
roster,
actions,
start: draft,
@@ -14331,7 +14550,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_react32.createElement)(
+ return (0, import_react33.createElement)(
"div",
{
className: "bt-pane",
@@ -14341,31 +14560,31 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
if (event.target === event.currentTarget) close();
} : void 0
},
- (0, import_react32.createElement)(
+ (0, import_react33.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_react32.createElement)(
+ menuOnly ? null : (0, import_react33.createElement)(
"div",
{ className: "bt-to" },
- (0, import_react32.createElement)("span", { className: "bt-to-label" }, t("To:")),
- adminBot ? (0, import_react32.createElement)(
+ (0, import_react33.createElement)("span", { className: "bt-to-label" }, t("To:")),
+ adminBot ? (0, import_react33.createElement)(
"span",
{ key: adminBot.id, className: "bt-token bt-token-admin", title: t("Admin") },
- (0, import_react32.createElement)(BotMark, { bot: adminBot, size: 16 }),
+ (0, import_react33.createElement)(BotMark, { bot: adminBot, size: 16 }),
adminBot.name,
- (0, import_react32.createElement)(AdminStar),
- (0, import_react32.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×")
+ (0, import_react33.createElement)(AdminStar),
+ (0, import_react33.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×")
) : null,
- choosingAdmin ? (0, import_react32.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react32.createElement)(AdminStar)) : null,
- pickedBots.map((bot) => (0, import_react32.createElement)(
+ choosingAdmin ? (0, import_react33.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react33.createElement)(AdminStar)) : null,
+ pickedBots.map((bot) => (0, import_react33.createElement)(
"span",
{ key: bot.id, className: "bt-token" },
- (0, import_react32.createElement)(BotMark, { bot, size: 16 }),
+ (0, import_react33.createElement)(BotMark, { bot, size: 16 }),
bot.name,
- (0, import_react32.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×")
+ (0, import_react33.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×")
)),
- (0, import_react32.createElement)("input", {
+ (0, import_react33.createElement)("input", {
ref: inputRef,
autoFocus: true,
value: query,
@@ -14389,20 +14608,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
} else if (event.key === "Backspace" && query === "") unpick();
}
}),
- group && admin !== null ? (0, import_react32.createElement)("button", {
+ group && admin !== null ? (0, import_react33.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_react32.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×")
+ (0, import_react33.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×")
),
- (0, import_react32.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react32.createElement)(
+ (0, import_react33.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react33.createElement)(
"div",
{ ref: glide.box, role: "listbox", "aria-label": t("Recipients"), className: "bt-drop-list" },
- (0, import_react32.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- options.length === 0 ? (0, import_react32.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react32.createElement)(
+ (0, import_react33.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ options.length === 0 ? (0, import_react33.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_react33.createElement)(
"button",
{
key: option.key,
@@ -14414,36 +14633,36 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
onMouseEnter: () => setCursor(index),
onClick: option.run
},
- option.bot ? (0, import_react32.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react32.createElement)("span", { className: "bt-option-icon" }, option.icon),
+ option.bot ? (0, import_react33.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react33.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_react32.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react32.createElement)("span", { className: "bt-option-copy" }, (0, import_react32.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react32.createElement)("span", { className: "bt-option-sub" }, option.hint)),
- option.bot && option.hint && index === active ? (0, import_react32.createElement)("span", { className: "bt-option-hint" }, option.hint) : null
+ option.bot || !option.hint ? (0, import_react33.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react33.createElement)("span", { className: "bt-option-copy" }, (0, import_react33.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react33.createElement)("span", { className: "bt-option-sub" }, option.hint)),
+ option.bot && option.hint && index === active ? (0, import_react33.createElement)("span", { className: "bt-option-hint" }, option.hint) : null
))
)),
- menuOnly ? null : (0, import_react32.createElement)("div", { className: "bt-nc-spacer" }),
- error && fix ? (0, import_react32.createElement)(
+ menuOnly ? null : (0, import_react33.createElement)("div", { className: "bt-nc-spacer" }),
+ error && fix ? (0, import_react33.createElement)(
"div",
{ className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" },
- (0, import_react32.createElement)("div", null, fix.bots.length === 1 ? t("{name} is not shared with {friend}, so it cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots[0].name, friend: fix.friends.join(", ") || t("that friend") }) : t("{name} are not shared with {friend}, so they cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots.map((bot) => bot.name).join(", "), friend: fix.friends.join(", ") || t("that friend") })),
- (0, import_react32.createElement)("div", { className: "bt-fix-actions" }, fix.bots.flatMap((bot) => [
- typeof globalThis.__flybot__?.social?.createInvite === "function" && globalThis.__flybot__.social.canInvite?.(bot.id) !== false ? (0, import_react32.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => {
+ (0, import_react33.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_react33.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_react33.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_react32.createElement)("button", { key: `out-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "leave-out", onClick: () => leaveOut(bot.id) }, t("Leave {name} out", { name: bot.name }))
+ (0, import_react33.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_react32.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null,
- inviteLink ? (0, import_react32.createElement)("input", { className: "bt-input bt-fix-link", type: "text", readOnly: true, value: inviteLink, "aria-label": t("Invite link"), "data-fix-link": "", autoFocus: true, onFocus: (event) => event.target.select(), onClick: (event) => event.target.select() }) : null
- ) : error ? (0, import_react32.createElement)("div", { className: "bt-error" }, error) : null,
- menuOnly ? null : (0, import_react32.createElement)(
+ fixNote ? (0, import_react33.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null,
+ inviteLink ? (0, import_react33.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_react33.createElement)("div", { className: "bt-error" }, error) : null,
+ menuOnly ? null : (0, import_react33.createElement)(
"div",
{ className: "bt-nc-compose" },
- (0, import_react32.createElement)(
+ (0, import_react33.createElement)(
"div",
{ className: "bt-nc-card" },
- (0, import_react32.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react32.createElement)(PlusIcon)),
- (0, import_react32.createElement)("textarea", {
+ (0, import_react33.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react33.createElement)(PlusIcon)),
+ (0, import_react33.createElement)("textarea", {
rows: 1,
value: message,
placeholder,
@@ -14456,7 +14675,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
}
}
}),
- (0, import_react32.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react32.createElement)(ArrowUpIcon))
+ (0, import_react33.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react33.createElement)(ArrowUpIcon))
)
)
)
@@ -14464,7 +14683,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES });
}
// src/client/exchange.js
-var import_react33 = require("react");
+var import_react34 = require("react");
var SHEET_EXIT_MS = 220;
function exchangeText(text2) {
if (typeof text2 !== "string") return "";
@@ -14482,23 +14701,23 @@ function exchangeText(text2) {
}
function ExchangeLine({ entry, roster, actions }) {
const bot = roster.byId[entry.from] ?? { id: entry.from, name: t("You"), color: "gray" };
- return (0, import_react33.createElement)(
+ return (0, import_react34.createElement)(
"div",
{ className: "bt-group-msg" },
- (0, import_react33.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name),
- (0, import_react33.createElement)(Bubbles, { texts: [exchangeText(entry.text)], roster, selfId: bot.id, actions, lead: (0, import_react33.createElement)("span", { className: "bt-lead" }, (0, import_react33.createElement)(BotMark, { bot, size: 22 })) })
+ (0, import_react34.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name),
+ (0, import_react34.createElement)(Bubbles, { texts: [exchangeText(entry.text)], roster, selfId: bot.id, actions, lead: (0, import_react34.createElement)("span", { className: "bt-lead" }, (0, import_react34.createElement)(BotMark, { bot, size: 22 })) })
);
}
function ExchangeDialog({ pair, roster, actions }) {
- const [entries2, setEntries] = (0, import_react33.useState)(null);
- const [leaving, setLeaving] = (0, import_react33.useState)(false);
+ const [entries2, setEntries] = (0, import_react34.useState)(null);
+ const [leaving, setLeaving] = (0, import_react34.useState)(false);
const close = () => {
if (leaving) return;
setLeaving(true);
setTimeout(() => actions.closeOverlay(), SHEET_EXIT_MS);
};
useEscape(close);
- (0, import_react33.useEffect)(() => {
+ (0, import_react34.useEffect)(() => {
let stop = false;
let timer;
let seen = null;
@@ -14522,42 +14741,42 @@ function ExchangeDialog({ pair, roster, actions }) {
const right = roster.byId[pair[1]];
let lastDay = 0;
const paneLeft = usePaneLeft();
- return (0, import_react33.createElement)(
+ return (0, import_react34.createElement)(
"div",
{ className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 },
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)(
"div",
{ className: "bt-dialog", role: "dialog", "aria-label": t("Bot exchange") },
- (0, import_react33.createElement)("div", { className: "bt-head" }, (0, import_react33.createElement)(
+ (0, import_react34.createElement)("div", { className: "bt-head" }, (0, import_react34.createElement)(
"span",
{ className: "bt-pill", style: { cursor: "default" } },
- left ? (0, import_react33.createElement)(BotMark, { bot: left, size: 22 }) : null,
+ left ? (0, import_react34.createElement)(BotMark, { bot: left, size: 22 }) : null,
left?.name ?? "?",
- (0, import_react33.createElement)("span", { className: "bt-swap" }, "⇄"),
- right ? (0, import_react33.createElement)(BotMark, { bot: right, size: 22 }) : null,
+ (0, import_react34.createElement)("span", { className: "bt-swap" }, "⇄"),
+ right ? (0, import_react34.createElement)(BotMark, { bot: right, size: 22 }) : null,
right?.name ?? "?"
)),
- (0, import_react33.createElement)(
+ (0, import_react34.createElement)(
"div",
{ className: "bt-dialog-log", role: "log", "aria-label": t("Bot exchange transcript") },
- entries2 === null ? (0, import_react33.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react33.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => {
+ entries2 === null ? (0, import_react34.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react34.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => {
const showDay = entry.time - lastDay > 30 * 60 * 1e3;
lastDay = entry.time;
- return (0, import_react33.createElement)(
- import_react33.Fragment,
+ return (0, import_react34.createElement)(
+ import_react34.Fragment,
{ key: entry.id },
- showDay ? (0, import_react33.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
- (0, import_react33.createElement)(ExchangeLine, { entry, roster, actions })
+ showDay ? (0, import_react34.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
+ (0, import_react34.createElement)(ExchangeLine, { entry, roster, actions })
);
})
),
- (0, import_react33.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react33.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat")))
+ (0, import_react34.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react34.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat")))
)
);
}
// src/client/voice.js
-var import_react34 = require("react");
+var import_react35 = require("react");
var VOICE_LABELS = {
listening: "Listening",
thinking: "Thinking",
@@ -14568,8 +14787,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_react34.useState)(0);
- (0, import_react34.useEffect)(() => {
+ const [level, setLevel] = (0, import_react35.useState)(0);
+ (0, import_react35.useEffect)(() => {
if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0;
let stream = null;
let audio = null;
@@ -14619,18 +14838,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_react34.useState)(() => actions.speech());
- const [phase, setPhase] = (0, import_react34.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
- const [catalog, setCatalog] = (0, import_react34.useState)(null);
- const [heard, setHeard] = (0, import_react34.useState)("");
- const [said, setSaid] = (0, import_react34.useState)("");
- const [note, setNote] = (0, import_react34.useState)("");
- const [serviceLevel, setServiceLevel] = (0, import_react34.useState)(0);
- const [cycle, setCycle] = (0, import_react34.useState)(0);
- const waiting = (0, import_react34.useRef)(null);
- const reply = (0, import_react34.useRef)(lastReply);
+ const [speech] = (0, import_react35.useState)(() => actions.speech());
+ const [phase, setPhase] = (0, import_react35.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
+ const [catalog, setCatalog] = (0, import_react35.useState)(null);
+ const [heard, setHeard] = (0, import_react35.useState)("");
+ const [said, setSaid] = (0, import_react35.useState)("");
+ const [note, setNote] = (0, import_react35.useState)("");
+ const [serviceLevel, setServiceLevel] = (0, import_react35.useState)(0);
+ const [cycle, setCycle] = (0, import_react35.useState)(0);
+ const waiting = (0, import_react35.useRef)(null);
+ const reply = (0, import_react35.useRef)(lastReply);
reply.current = lastReply;
- const turn = (0, import_react34.useRef)(0);
+ const turn = (0, import_react35.useRef)(0);
const paneLeft = usePaneLeft();
const close = () => {
turn.current += 1;
@@ -14650,12 +14869,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
dictation.stop();
submit(text2);
});
- (0, import_react34.useEffect)(() => {
+ (0, import_react35.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
}, [note]);
- (0, import_react34.useEffect)(() => {
+ (0, import_react35.useEffect)(() => {
if (!speech) return void 0;
let live = true;
speechCatalog(speech).then(({ catalog: next, usable }) => {
@@ -14670,16 +14889,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
};
}, [speech]);
const listen = phase === "listening";
- (0, import_react34.useEffect)(() => {
+ (0, import_react35.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_react34.useEffect)(() => {
+ (0, import_react35.useEffect)(() => {
if (phase !== "listening") dictation.stop();
}, [phase]);
- (0, import_react34.useEffect)(() => {
+ (0, import_react35.useEffect)(() => {
if (!speech || !catalog || !listen) return void 0;
const own = turn.current;
let capture = null;
@@ -14741,7 +14960,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
setServiceLevel(0);
};
}, [speech, catalog, listen, cycle]);
- (0, import_react34.useEffect)(() => {
+ (0, import_react35.useEffect)(() => {
if (phase !== "thinking" || running || waiting.current === null) return;
const [count, text2] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)];
if (count === waiting.current || !text2.trim()) return;
@@ -14761,7 +14980,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
window.speechSynthesis.cancel();
window.speechSynthesis.speak(utterance);
}, [phase, running, lastReply]);
- (0, import_react34.useEffect)(() => () => window.speechSynthesis?.cancel(), []);
+ (0, import_react35.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";
@@ -14778,50 +14997,50 @@ 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_react34.createElement)(
+ return (0, import_react35.createElement)(
"div",
{ className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
- (0, import_react34.createElement)(
+ (0, import_react35.createElement)(
"div",
{ className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase },
- (0, import_react34.createElement)(
+ (0, import_react35.createElement)(
"div",
{ className: "bt-voice-orb", style: { "--bt-level": level } },
- (0, import_react34.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
- (0, import_react34.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
- bot ? (0, import_react34.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react34.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
+ (0, import_react35.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
+ (0, import_react35.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
+ bot ? (0, import_react35.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react35.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
),
- (0, import_react34.createElement)("div", { className: "bt-voice-name" }, name),
- (0, import_react34.createElement)(
+ (0, import_react35.createElement)("div", { className: "bt-voice-name" }, name),
+ (0, import_react35.createElement)(
"div",
{ className: "bt-voice-status", role: "status" },
- phase === "thinking" ? (0, import_react34.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react34.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
+ phase === "thinking" ? (0, import_react35.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react35.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
),
- (0, import_react34.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
- openSetup ? (0, import_react34.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
+ (0, import_react35.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
+ openSetup ? (0, import_react35.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
close();
openSetup();
} }, t("Set up voice input")) : null,
- shownNote ? (0, import_react34.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
+ shownNote ? (0, import_react35.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
),
- (0, import_react34.createElement)(
+ (0, import_react35.createElement)(
"div",
{ className: "bt-voice-bar" },
- (0, import_react34.createElement)("button", {
+ (0, import_react35.createElement)("button", {
type: "button",
className: "bt-voice-btn",
"aria-pressed": phase === "paused",
disabled: blocked,
"aria-label": phase === "paused" ? t("Resume listening") : t("Pause"),
onClick: togglePause
- }, (0, import_react34.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react34.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
- (0, import_react34.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react34.createElement)(CloseIcon))
+ }, (0, import_react35.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react35.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
+ (0, import_react35.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react35.createElement)(CloseIcon))
)
);
}
// src/client/workflows.js
-var import_react35 = require("react");
+var import_react36 = require("react");
var READ_MS = 8e3;
var WORKFLOW_PLUGIN_HINT = "The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow.";
var COLORS = { Planner: "#8b5cf6", Writer: "#f0509a", Coder: "#1fbf6a", Reviewer: "#f5a524", Tester: "#14b8a6", Deployer: "#2f7cf6" };
@@ -14848,7 +15067,7 @@ function settleWorkflowRead(value, fallback = "Could not read workflows") {
if (templates.length === 0 && workflows.length === 0) return { phase: "empty", error: "", pack };
return { phase: "ready", error: "", pack };
}
-function readWorkflows(call4, { timeoutMs = READ_MS } = {}) {
+function readWorkflows(call5, { timeoutMs = READ_MS } = {}) {
return new Promise((resolve) => {
let settled = false;
const finish = (value) => {
@@ -14858,7 +15077,7 @@ function readWorkflows(call4, { timeoutMs = READ_MS } = {}) {
resolve(value);
};
const timer = setTimeout(() => finish(settleWorkflowRead(null)), timeoutMs);
- Promise.resolve().then(() => call4("workflow/state")).then((value) => finish(settleWorkflowRead(value))).catch((failure) => finish({ phase: "error", error: workflowCallError(failure), pack: null }));
+ Promise.resolve().then(() => call5("workflow/state")).then((value) => finish(settleWorkflowRead(value))).catch((failure) => finish({ phase: "error", error: workflowCallError(failure), pack: null }));
});
}
function moveNode(nodes, id, x, y) {
@@ -14946,9 +15165,9 @@ function measureCanvasWidth(el) {
return Number.isFinite(width) && width > 0 ? width : 0;
}
function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }) {
- const gesture = (0, import_react35.useRef)(null);
- const canvasRef = (0, import_react35.useRef)(null);
- const [measuredWidth, setCanvasWidth] = (0, import_react35.useState)(0);
+ const gesture = (0, import_react36.useRef)(null);
+ const canvasRef = (0, import_react36.useRef)(null);
+ const [measuredWidth, setCanvasWidth] = (0, import_react36.useState)(0);
const nodes = graph.nodes ?? [];
const edges = graph.edges ?? [];
const status = graph.run?.nodeStatus ?? null;
@@ -14957,7 +15176,7 @@ function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }
const canvasWidth = measuredWidth || knownWidth;
const { scale, pad: pad2 } = fitLayout(nodes, canvasWidth);
const drawnHeight = Math.round(height * scale);
- (0, import_react35.useEffect)(() => {
+ (0, import_react36.useEffect)(() => {
const el = canvasRef.current;
const measure = () => {
const width = measureCanvasWidth(el);
@@ -14986,14 +15205,14 @@ function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }
onCommit?.({ nodes: dropBot(nodes, current.bot, x, y), edges });
}
};
- return (0, import_react35.createElement)(
+ return (0, import_react36.createElement)(
"div",
{ className: "bt-wf-canvas", ref: canvasRef, onPointerUp: end },
- editing ? (0, import_react35.createElement)(
+ editing ? (0, import_react36.createElement)(
"div",
{ className: "bt-wf-palette" },
- (0, import_react35.createElement)("span", { className: "bt-wf-hint" }, t("Drag a Bot here")),
- locals.map((bot) => (0, import_react35.createElement)("button", {
+ (0, import_react36.createElement)("span", { className: "bt-wf-hint" }, t("Drag a Bot here")),
+ locals.map((bot) => (0, import_react36.createElement)("button", {
key: bot.id,
type: "button",
"data-bot": bot.id,
@@ -15006,11 +15225,11 @@ function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }
// A node's system prompt is the run Bot's prompt (plugins/workflow
// nodeConfig). The sidebar panel has this field, so the Settings editor
// writes the same graph instead of only drawing it (#1393).
- editing ? (0, import_react35.createElement)("div", { className: "bt-wf-prompts" }, nodes.map((node) => (0, import_react35.createElement)(
+ editing ? (0, import_react36.createElement)("div", { className: "bt-wf-prompts" }, nodes.map((node) => (0, import_react36.createElement)(
"label",
{ key: node.id, className: "bt-wf-prompt" },
- (0, import_react35.createElement)("span", null, `${node.name} · ${t("System prompt")}`),
- (0, import_react35.createElement)("textarea", {
+ (0, import_react36.createElement)("span", null, `${node.name} · ${t("System prompt")}`),
+ (0, import_react36.createElement)("textarea", {
"data-prompt": node.id,
rows: 3,
defaultValue: node.prompt || "",
@@ -15022,16 +15241,16 @@ function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }
}
})
))) : null,
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"div",
{ className: "bt-wf-stage", style: pad2 > 0 ? { height: drawnHeight, paddingLeft: pad2 } : { height: drawnHeight } },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"div",
{ className: "bt-wf-fit", style: { height, transform: `scale(${scale})` } },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"div",
{ className: "bt-wf-plane", "data-plane": "1", style: { height, "--bt-wf-wire-span": `${WIRE_SPAN}px` } },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"svg",
{
className: "bt-wf-wires",
@@ -15040,10 +15259,10 @@ function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }
height,
viewBox: `0 0 ${WIRE_SPAN * 2} ${height}`
},
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"defs",
null,
- (0, import_react35.createElement)("marker", {
+ (0, import_react36.createElement)("marker", {
id: "bt-wf-arrow",
viewBox: "0 0 10 10",
refX: "8",
@@ -15051,13 +15270,13 @@ function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }
markerWidth: "6",
markerHeight: "6",
orient: "auto"
- }, (0, import_react35.createElement)("path", { d: "M0 0 10 5 0 10z" }))
+ }, (0, import_react36.createElement)("path", { d: "M0 0 10 5 0 10z" }))
),
edges.map((edge) => {
const from = nodes.find((node) => node.id === edge.from);
const to = nodes.find((node) => node.id === edge.to);
if (!from || !to) return null;
- return (0, import_react35.createElement)("path", { key: `${edge.from}>${edge.to}`, d: wirePath(from, to), markerEnd: "url(#bt-wf-arrow)" });
+ return (0, import_react36.createElement)("path", { key: `${edge.from}>${edge.to}`, d: wirePath(from, to), markerEnd: "url(#bt-wf-arrow)" });
})
),
(graph.layers ?? []).filter((layer) => layer.length > 1).map((layer) => {
@@ -15067,15 +15286,15 @@ function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }
const right = Math.max(...members.map((node) => node.x ?? 0)) + FRAME_SIDE;
const top = Math.min(...members.map((node) => node.y ?? 0)) - FRAME_LABEL;
const bottom = Math.max(...members.map((node) => node.y ?? 0)) + CARD_H + FRAME_FOOT;
- return (0, import_react35.createElement)("div", {
+ return (0, import_react36.createElement)("div", {
key: layer.join("+"),
className: "bt-wf-par",
style: { left, top, width: right - left, height: bottom - top }
- }, (0, import_react35.createElement)("div", { className: "bt-wf-plabel" }, t("Parallel branch, then join")));
+ }, (0, import_react36.createElement)("div", { className: "bt-wf-plabel" }, t("Parallel branch, then join")));
}),
nodes.map((node) => {
const state = status ? status[node.id] ?? "queued" : "idle";
- return (0, import_react35.createElement)(
+ return (0, import_react36.createElement)(
"div",
{
key: node.id,
@@ -15087,7 +15306,7 @@ function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }
gesture.current = { kind: "move", id: node.id, ox: event.clientX, oy: event.clientY, x: node.x ?? 0, y: node.y ?? 0 };
}
},
- editing ? (0, import_react35.createElement)("button", {
+ editing ? (0, import_react36.createElement)("button", {
type: "button",
className: "bt-wf-port",
"data-port": "out",
@@ -15098,7 +15317,7 @@ function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }
gesture.current = { kind: "wire", from: node.id };
}
}) : null,
- editing ? (0, import_react35.createElement)("button", {
+ editing ? (0, import_react36.createElement)("button", {
type: "button",
className: "bt-wf-remove",
"data-remove": node.id,
@@ -15109,13 +15328,13 @@ function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }
onCommit?.(next);
}
}, "×") : null,
- (0, import_react35.createElement)("span", { className: `bt-wf-chip ${state}` }, statusLabel2(state)),
- (0, import_react35.createElement)("span", { className: "bt-wf-av", style: { background: COLORS[node.name] ?? "#4d6bfe" } }, (node.name ?? "?").slice(0, 1)),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)("span", { className: `bt-wf-chip ${state}` }, statusLabel2(state)),
+ (0, import_react36.createElement)("span", { className: "bt-wf-av", style: { background: COLORS[node.name] ?? "#4d6bfe" } }, (node.name ?? "?").slice(0, 1)),
+ (0, import_react36.createElement)(
"span",
{ className: "bt-wf-copy" },
- (0, import_react35.createElement)("span", { className: "bt-wf-name" }, node.name),
- (0, import_react35.createElement)("span", { className: "bt-wf-role" }, node.role ?? "")
+ (0, import_react36.createElement)("span", { className: "bt-wf-name" }, node.name),
+ (0, import_react36.createElement)("span", { className: "bt-wf-role" }, node.role ?? "")
)
);
})
@@ -15125,7 +15344,7 @@ function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }
);
}
function Row2({ item, source, onOpen, onRename, onDelete }) {
- return (0, import_react35.createElement)(
+ return (0, import_react36.createElement)(
"button",
{
type: "button",
@@ -15134,16 +15353,16 @@ function Row2({ item, source, onOpen, onRename, onDelete }) {
"data-source": source,
onClick: () => onOpen?.(item)
},
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"span",
{ className: "bt-set-copy" },
- (0, import_react35.createElement)("span", null, item.name),
- (0, import_react35.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "")
+ (0, import_react36.createElement)("span", null, item.name),
+ (0, import_react36.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "")
),
- source === "own" ? (0, import_react35.createElement)(
+ source === "own" ? (0, import_react36.createElement)(
"span",
{ className: "bt-set-actions" },
- (0, import_react35.createElement)("button", {
+ (0, import_react36.createElement)("button", {
type: "button",
className: "bt-set-action",
"aria-label": t("Rename"),
@@ -15153,7 +15372,7 @@ function Row2({ item, source, onOpen, onRename, onDelete }) {
onRename?.(item);
}
}, "✎"),
- (0, import_react35.createElement)("button", {
+ (0, import_react36.createElement)("button", {
type: "button",
className: "bt-set-action",
"aria-label": t("Delete"),
@@ -15164,7 +15383,7 @@ function Row2({ item, source, onOpen, onRename, onDelete }) {
}
}, "×")
) : null,
- (0, import_react35.createElement)("span", { className: "bt-chevron", "aria-hidden": true }, "›")
+ (0, import_react36.createElement)("span", { className: "bt-chevron", "aria-hidden": true }, "›")
);
}
function alertCopy(error) {
@@ -15177,61 +15396,61 @@ function WorkflowsView({ phase = "loading", pack = null, error = "", opened = nu
const templateIds = new Set(templates.map((item) => item.id));
const yours = (pack?.workflows ?? []).filter((item) => !templateIds.has(item.id));
const showUse = opened?.source === "factory" || opened?.fromFactory !== void 0;
- return (0, import_react35.createElement)(
+ return (0, import_react36.createElement)(
"div",
{ className: "bt-wf-page" },
- (0, import_react35.createElement)("p", { className: "bt-wf-hint" }, t("A workflow is a group of your Bots. Drag the flowchart to change who hands work to whom.")),
- error ? (0, import_react35.createElement)(
+ (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("A workflow is a group of your Bots. Drag the flowchart to change who hands work to whom.")),
+ error ? (0, import_react36.createElement)(
"div",
{ className: "bt-error", role: "alert" },
alertCopy(error),
- phase === "error" ? (0, import_react35.createElement)("button", { type: "button", onClick: () => onRetry?.() }, t("Retry")) : null
+ phase === "error" ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onRetry?.() }, t("Retry")) : null
) : null,
- phase === "loading" ? (0, import_react35.createElement)("p", { className: "bt-wf-status", role: "status" }, t("Reading workflows…")) : null,
- phase === "empty" ? (0, import_react35.createElement)("p", { className: "bt-wf-status" }, t("No workflows yet.")) : null,
- phase === "ready" && !graph ? (0, import_react35.createElement)(
+ phase === "loading" ? (0, import_react36.createElement)("p", { className: "bt-wf-status", role: "status" }, t("Reading workflows…")) : null,
+ phase === "empty" ? (0, import_react36.createElement)("p", { className: "bt-wf-status" }, t("No workflows yet.")) : null,
+ phase === "ready" && !graph ? (0, import_react36.createElement)(
"div",
{ className: "bt-wf-lists" },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react35.createElement)("h3", null, t("Factory presets")),
- (0, import_react35.createElement)("p", { className: "bt-wf-hint" }, t("Ready-made pipelines. Use one to start your own.")),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)("h3", null, t("Factory presets")),
+ (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("Ready-made pipelines. Use one to start your own.")),
+ (0, import_react36.createElement)(
"div",
{ className: "bt-set-card" },
- templates.length === 0 ? (0, import_react35.createElement)("div", { className: "bt-set-row" }, (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("No workflows yet."))) : templates.map((item) => {
+ templates.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No workflows yet."))) : templates.map((item) => {
const live = (pack.workflows ?? []).find((workflow) => workflow.id === item.id);
const row = live ? { ...item, name: live.name, preview: live.preview || chainOf(live) || chainOf(item) } : item;
- return (0, import_react35.createElement)(Row2, { key: item.id, item: row, source: "factory", onOpen: () => onOpen?.(item, "factory") });
+ return (0, import_react36.createElement)(Row2, { key: item.id, item: row, source: "factory", onOpen: () => onOpen?.(item, "factory") });
})
)
),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react35.createElement)("h3", null, t("Yours")),
- (0, import_react35.createElement)("p", { className: "bt-wf-hint" }, t("Workflows saved on this machine.")),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)("h3", null, t("Yours")),
+ (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("Workflows saved on this machine.")),
+ (0, import_react36.createElement)(
"div",
{ className: "bt-set-card" },
- yours.length === 0 ? (0, import_react35.createElement)("div", { className: "bt-set-row" }, (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react35.createElement)(Row2, { key: item.id, item, source: "own", onOpen: () => onOpen?.(item, "own"), onRename, onDelete }))
+ yours.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react36.createElement)(Row2, { key: item.id, item, source: "own", onOpen: () => onOpen?.(item, "own"), onRename, onDelete }))
)
)
) : null,
- graph ? (0, import_react35.createElement)(
+ graph ? (0, import_react36.createElement)(
"div",
{ className: "bt-wf-editor" },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"div",
{ className: "bt-wf-actions" },
- (0, import_react35.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")),
- showUse ? (0, import_react35.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null,
- opened?.source === "own" ? (0, import_react35.createElement)("button", { type: "button", onClick: () => onEditing?.(!editing) }, editing ? t("Finish editing") : t("Edit")) : null
+ (0, import_react36.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")),
+ showUse ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null,
+ opened?.source === "own" ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onEditing?.(!editing) }, editing ? t("Finish editing") : t("Edit")) : null
),
- (0, import_react35.createElement)("h3", { className: "bt-wf-title" }, graph.name),
- graph.description ? (0, import_react35.createElement)("p", { className: "bt-wf-hint" }, graph.description) : null,
- (graph.nodes ?? []).length === 0 ? (0, import_react35.createElement)("p", { className: "bt-wf-status" }, t("This workflow has no flowchart yet.")) : (0, import_react35.createElement)(GraphCanvas, {
+ (0, import_react36.createElement)("h3", { className: "bt-wf-title" }, graph.name),
+ graph.description ? (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, graph.description) : null,
+ (graph.nodes ?? []).length === 0 ? (0, import_react36.createElement)("p", { className: "bt-wf-status" }, t("This workflow has no flowchart yet.")) : (0, import_react36.createElement)(GraphCanvas, {
graph,
roster,
onCommit,
@@ -15242,66 +15461,66 @@ function WorkflowsView({ phase = "loading", pack = null, error = "", opened = nu
);
}
function RenameModal({ workflow, onClose, onConfirm }) {
- const [name, setName] = (0, import_react35.useState)(workflow?.name ?? "");
- return (0, import_react35.createElement)(
+ const [name, setName] = (0, import_react36.useState)(workflow?.name ?? "");
+ return (0, import_react36.createElement)(
"div",
{ className: "bt-modal-mask", onClick: (event) => {
if (event.target === event.currentTarget) onClose?.();
} },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"div",
{ className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Rename workflow") },
- (0, import_react35.createElement)("h3", null, t("Rename workflow")),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)("h3", null, t("Rename workflow")),
+ (0, import_react36.createElement)(
"label",
{ className: "bt-field" },
t("Name"),
- (0, import_react35.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true })
+ (0, import_react36.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true })
),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"div",
{ className: "bt-modal-actions" },
- (0, import_react35.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")),
- (0, import_react35.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename"))
+ (0, import_react36.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")),
+ (0, import_react36.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename"))
)
)
);
}
function DeleteModal({ workflow, onClose, onConfirm }) {
- return (0, import_react35.createElement)(
+ return (0, import_react36.createElement)(
"div",
{ className: "bt-modal-mask", onClick: (event) => {
if (event.target === event.currentTarget) onClose?.();
} },
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)(
"div",
{ className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") },
- (0, import_react35.createElement)("h3", null, t("Delete workflow")),
- (0, import_react35.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })),
- (0, import_react35.createElement)(
+ (0, import_react36.createElement)("h3", null, t("Delete workflow")),
+ (0, import_react36.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })),
+ (0, import_react36.createElement)(
"div",
{ className: "bt-modal-actions" },
- (0, import_react35.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")),
- (0, import_react35.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete"))
+ (0, import_react36.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")),
+ (0, import_react36.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete"))
)
)
);
}
-function WorkflowsPage({ call: call4, roster }) {
- const [phase, setPhase] = (0, import_react35.useState)("loading");
- const [pack, setPack] = (0, import_react35.useState)(null);
- const [error, setError] = (0, import_react35.useState)("");
- const [opened, setOpened] = (0, import_react35.useState)(null);
- const [editing, setEditing] = (0, import_react35.useState)(false);
- const [renaming, setRenaming] = (0, import_react35.useState)(null);
- const [deleting, setDeleting] = (0, import_react35.useState)(null);
- const invoke = typeof call4 === "function" ? call4 : () => Promise.reject(new Error("Could not read workflows"));
+function WorkflowsPage({ call: call5, roster }) {
+ const [phase, setPhase] = (0, import_react36.useState)("loading");
+ const [pack, setPack] = (0, import_react36.useState)(null);
+ const [error, setError] = (0, import_react36.useState)("");
+ const [opened, setOpened] = (0, import_react36.useState)(null);
+ const [editing, setEditing] = (0, import_react36.useState)(false);
+ const [renaming, setRenaming] = (0, import_react36.useState)(null);
+ const [deleting, setDeleting] = (0, import_react36.useState)(null);
+ const invoke = typeof call5 === "function" ? call5 : () => Promise.reject(new Error("Could not read workflows"));
const apply2 = (settled) => {
setPhase(settled.phase);
setPack(settled.pack);
setError(settled.error);
};
- (0, import_react35.useEffect)(() => {
+ (0, import_react36.useEffect)(() => {
let gone = false;
readWorkflows(invoke).then((settled) => {
if (!gone) apply2(settled);
@@ -15401,10 +15620,10 @@ function WorkflowsPage({ call: call4, roster }) {
setError(workflowCallError(failure, t("Could not save this workflow")));
});
};
- return (0, import_react35.createElement)(
+ return (0, import_react36.createElement)(
"div",
null,
- (0, import_react35.createElement)(WorkflowsView, {
+ (0, import_react36.createElement)(WorkflowsView, {
phase,
pack,
error,
@@ -15423,12 +15642,12 @@ function WorkflowsPage({ call: call4, roster }) {
onRename: rename,
onDelete: deleteWorkflow
}),
- renaming ? (0, import_react35.createElement)(RenameModal, {
+ renaming ? (0, import_react36.createElement)(RenameModal, {
workflow: renaming,
onClose: () => setRenaming(null),
onConfirm: (name) => confirmRename(renaming.id, name)
}) : null,
- deleting ? (0, import_react35.createElement)(DeleteModal, {
+ deleting ? (0, import_react36.createElement)(DeleteModal, {
workflow: deleting,
onClose: () => setDeleting(null),
onConfirm: () => confirmDelete(deleting.id)
@@ -15569,7 +15788,7 @@ function percent(part, whole) {
}
// src/client/usage-page.js
-var import_react36 = require("react");
+var import_react37 = require("react");
var HEAT_WEEKS = 53;
var TOP_SERIES = 7;
var RING_R = 42;
@@ -15616,9 +15835,9 @@ function titleOf(range, bucket) {
return dates({ year: "numeric", month: "long" }).format(bucket.start);
}
function useTween(value) {
- const [shown, setShown] = (0, import_react36.useState)(0);
- const from = (0, import_react36.useRef)(0);
- (0, import_react36.useEffect)(() => {
+ const [shown, setShown] = (0, import_react37.useState)(0);
+ const from = (0, import_react37.useRef)(0);
+ (0, import_react37.useEffect)(() => {
if (reducedMotion() || typeof requestAnimationFrame !== "function") {
from.current = value;
setShown(value);
@@ -15638,9 +15857,9 @@ function useTween(value) {
}, [value]);
return shown;
}
-var glyphBox = (size, icon3) => (0, import_react36.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react36.createElement)(icon3));
+var glyphBox = (size, icon3) => (0, import_react37.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react37.createElement)(icon3));
function ModelGlyph({ name, paint, size }) {
- return (0, import_react36.createElement)("span", {
+ return (0, import_react37.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))` }
@@ -15657,7 +15876,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_react36.createElement)(BotAvatar, { bot, size, badge: false, live: false })
+ glyph: (size) => (0, import_react37.createElement)(BotAvatar, { bot, size, badge: false, live: false })
};
}
if (owner.kind === "room") {
@@ -15667,7 +15886,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_react36.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
+ glyph: (size) => room2 ? (0, import_react37.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
};
}
if (owner.kind === "agent") {
@@ -15689,30 +15908,30 @@ function Tabs({ label, value, options, onChange, boxRef }) {
const names = options.map(([, text2]) => text2).join("\n");
const glide = useGlide(`${value}
${names}`, { box: boxRef });
- return (0, import_react36.createElement)(
+ return (0, import_react37.createElement)(
"div",
{ ref: glide.box, className: "bt-us-tabs", role: "group", "aria-label": label },
- (0, import_react36.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- options.map(([id, text2, icon3]) => (0, import_react36.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react36.createElement)(icon3) : null, text2))
+ (0, import_react37.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ options.map(([id, text2, icon3]) => (0, import_react37.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react37.createElement)(icon3) : null, text2))
);
}
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_react36.createElement)("div", { className: "bt-us-stat" }, (0, import_react36.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react36.createElement)("span", { className: "bt-us-stat-value" }, value));
- return (0, import_react36.createElement)(
+ const cell = (label, value) => (0, import_react37.createElement)("div", { className: "bt-us-stat" }, (0, import_react37.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react37.createElement)("span", { className: "bt-us-stat-value" }, value));
+ return (0, import_react37.createElement)(
"section",
{ className: "bt-us-stats", style: { "--i": 0 } },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-stat bt-us-stat-main" },
- (0, import_react36.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
+ (0, import_react37.createElement)(
"span",
{ className: "bt-us-stat-value" },
compact(shown),
- (0, import_react36.createElement)("small", null, t("tokens")),
- change === null ? null : (0, import_react36.createElement)(
+ (0, import_react37.createElement)("small", null, t("tokens")),
+ change === null ? null : (0, import_react37.createElement)(
"span",
{ className: "bt-us-delta", title: compareText(range) },
`${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%`
@@ -15730,83 +15949,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_react36.useState)(null);
+ const [hover, setHover] = (0, import_react37.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_react36.createElement)(
+ return (0, import_react37.createElement)(
"section",
{ className: "bt-us-card bt-us-chart", style: { "--i": 1 } },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-card-head" },
- (0, import_react36.createElement)("h3", null, t("Tokens over time"), (0, import_react36.createElement)("span", null, unitLabel(range.unit))),
- onBy ? (0, import_react36.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null
+ (0, import_react37.createElement)("h3", null, t("Tokens over time"), (0, import_react37.createElement)("span", null, unitLabel(range.unit))),
+ onBy ? (0, import_react37.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null
),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-chart-body" },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-plot", "data-hover": hover === null ? void 0 : "", onMouseLeave: () => setHover(null) },
- [1, 0.5].map((level) => (0, import_react36.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react36.createElement)("span", null, compact(top * level)))),
- (0, import_react36.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react36.createElement)("span", null, "0")),
- (0, import_react36.createElement)(
+ [1, 0.5].map((level) => (0, import_react37.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react37.createElement)("span", null, compact(top * level)))),
+ (0, import_react37.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react37.createElement)("span", null, "0")),
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-cols" },
- columns.map((column, at) => (0, import_react36.createElement)("div", {
+ columns.map((column, at) => (0, import_react37.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_react36.createElement)(
+ }, column.total > 0 ? (0, import_react37.createElement)(
"div",
{ className: "bt-us-stack", style: { height: `${column.total / top * 100}%`, "--i": at } },
- column.parts.map(([key, value]) => value > 0 ? (0, import_react36.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null)
- ) : (0, import_react36.createElement)("div", { className: "bt-us-stub" })))
+ column.parts.map(([key, value]) => value > 0 ? (0, import_react37.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null)
+ ) : (0, import_react37.createElement)("div", { className: "bt-us-stub" })))
),
- tip ? (0, import_react36.createElement)(
+ tip ? (0, import_react37.createElement)(
"div",
{ className: "bt-us-tip", style: tipSide },
- (0, import_react36.createElement)("div", { className: "bt-us-tip-title" }, tip.title),
- (0, import_react36.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_react36.createElement)(
+ (0, import_react37.createElement)("div", { className: "bt-us-tip-title" }, tip.title),
+ (0, import_react37.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_react37.createElement)(
"div",
{ key, className: "bt-us-tip-row" },
- (0, import_react36.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }),
- (0, import_react36.createElement)("span", null, tipOf.get(key)?.name),
- (0, import_react36.createElement)("b", null, compact(value))
+ (0, import_react37.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }),
+ (0, import_react37.createElement)("span", null, tipOf.get(key)?.name),
+ (0, import_react37.createElement)("b", null, compact(value))
))
) : null
),
- (0, import_react36.createElement)(
+ (0, import_react37.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_react36.createElement)(
+ columns.map((column, at) => (0, import_react37.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_react36.createElement)(
+ series.length > 1 || series[0]?.pick ? (0, import_react37.createElement)(
"div",
{ className: "bt-us-legend" },
- series.map((entry) => (0, import_react36.createElement)(entry.pick ? "button" : "span", {
+ series.map((entry) => (0, import_react37.createElement)(entry.pick ? "button" : "span", {
key: entry.key,
className: "bt-us-key",
...entry.pick ? { type: "button", onClick: () => onPick(entry.pick) } : {}
- }, (0, import_react36.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name))
+ }, (0, import_react37.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name))
) : null
);
}
function Row3({ kind, glyph, name, sub, sum, whole, fill: fill3, index, onClick }) {
const width = whole > 0 ? Math.max(sum.total > 0 ? 2 : 0, sum.total / whole * 100) : 0;
- return (0, import_react36.createElement)(
+ return (0, import_react37.createElement)(
onClick ? "button" : "div",
{
className: "bt-us-row",
@@ -15815,69 +16034,69 @@ function Row3({ kind, glyph, name, sub, sum, whole, fill: fill3, index, onClick
title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }),
...onClick ? { type: "button", onClick } : {}
},
- glyph ? (0, import_react36.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
- (0, import_react36.createElement)(
+ glyph ? (0, import_react37.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
+ (0, import_react37.createElement)(
"span",
{ className: "bt-us-row-body" },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"span",
{ className: "bt-us-row-line" },
- (0, import_react36.createElement)("span", { className: "bt-us-row-name" }, name),
- sub ? (0, import_react36.createElement)("span", { className: "bt-us-row-sub" }, sub) : null
+ (0, import_react37.createElement)("span", { className: "bt-us-row-name" }, name),
+ sub ? (0, import_react37.createElement)("span", { className: "bt-us-row-sub" }, sub) : null
),
- (0, import_react36.createElement)("span", { className: "bt-us-track" }, (0, import_react36.createElement)("span", { style: { width: `${width}%`, background: fill3 } }))
+ (0, import_react37.createElement)("span", { className: "bt-us-track" }, (0, import_react37.createElement)("span", { style: { width: `${width}%`, background: fill3 } }))
),
- (0, import_react36.createElement)("span", { className: "bt-us-row-num" }, (0, import_react36.createElement)("b", null, compact(sum.total)), (0, import_react36.createElement)("small", null, `${percent(sum.total, whole)}%`))
+ (0, import_react37.createElement)("span", { className: "bt-us-row-num" }, (0, import_react37.createElement)("b", null, compact(sum.total)), (0, import_react37.createElement)("small", null, `${percent(sum.total, whole)}%`))
);
}
function ListCard({ title, count, index, children }) {
- return (0, import_react36.createElement)(
+ return (0, import_react37.createElement)(
"section",
{ className: "bt-us-card", style: { "--i": index } },
- (0, import_react36.createElement)("div", { className: "bt-us-card-head" }, (0, import_react36.createElement)("h3", null, title, count === void 0 ? null : (0, import_react36.createElement)("span", null, count))),
- (0, import_react36.createElement)("div", { className: "bt-us-rows" }, children)
+ (0, import_react37.createElement)("div", { className: "bt-us-card-head" }, (0, import_react37.createElement)("h3", null, title, count === void 0 ? null : (0, import_react37.createElement)("span", null, count))),
+ (0, import_react37.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_react36.createElement)(
+ return (0, import_react37.createElement)(
"section",
{ className: "bt-us-profile" },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"span",
{ className: "bt-us-ring" },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"svg",
{ viewBox: "0 0 96 96", "aria-hidden": true },
- face.stops ? (0, import_react36.createElement)("defs", null, (0, import_react36.createElement)(
+ face.stops ? (0, import_react37.createElement)("defs", null, (0, import_react37.createElement)(
"linearGradient",
{ id: "bt-us-ring-paint", x1: 0, y1: 1, x2: 1, y2: 0 },
- face.stops.map(([offset, color]) => (0, import_react36.createElement)("stop", { key: offset, offset, stopColor: color }))
+ face.stops.map(([offset, color]) => (0, import_react37.createElement)("stop", { key: offset, offset, stopColor: color }))
)) : null,
- (0, import_react36.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }),
- arc > 0 ? (0, import_react36.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_react37.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }),
+ arc > 0 ? (0, import_react37.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_react36.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64))
+ (0, import_react37.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64))
),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-profile-copy" },
- (0, import_react36.createElement)("h3", null, face.name),
- face.sub ? (0, import_react36.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null,
- (0, import_react36.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_react37.createElement)("h3", null, face.name),
+ face.sub ? (0, import_react37.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null,
+ (0, import_react37.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_react36.useRef)(null);
+ const box = (0, import_react37.useRef)(null);
const todayMs = hourMs(today);
const thisMonth = monthOf(todayMs);
- const [right, setRight] = (0, import_react36.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today))));
- const [anchor, setAnchor] = (0, import_react36.useState)(null);
- const [hover, setHover] = (0, import_react36.useState)(null);
- (0, import_react36.useEffect)(() => {
+ const [right, setRight] = (0, import_react37.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today))));
+ const [anchor, setAnchor] = (0, import_react37.useState)(null);
+ const [hover, setHover] = (0, import_react37.useState)(null);
+ (0, import_react37.useEffect)(() => {
const onDown = (event) => {
if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose();
};
@@ -15903,17 +16122,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
const [from, to] = [anchor, key].sort();
onPick(from, to);
};
- const weekdays = (0, import_react36.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []);
+ const weekdays = (0, import_react37.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_react36.createElement)("span", { key: `b${at}` }));
+ const cells = Array.from({ length: lead }, (_, at) => (0, import_react37.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_react36.createElement)("button", {
+ cells.push((0, import_react37.createElement)("button", {
key,
type: "button",
className: "bt-us-day",
@@ -15929,15 +16148,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
"data-has": flag(active.has(key)),
onMouseEnter: () => setHover(key),
onClick: () => press(key)
- }, (0, import_react36.createElement)("b", null, day)));
+ }, (0, import_react37.createElement)("b", null, day)));
}
- const nav = side === "left" ? (0, import_react36.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_react36.createElement)(ChevronLeftIcon)) : (0, import_react36.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_react36.createElement)(ChevronRightIcon));
- return (0, import_react36.createElement)(
+ const nav = side === "left" ? (0, import_react37.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_react37.createElement)(ChevronLeftIcon)) : (0, import_react37.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_react37.createElement)(ChevronRightIcon));
+ return (0, import_react37.createElement)(
"div",
{ key: start, className: "bt-us-month" },
- (0, import_react36.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react36.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))),
- (0, import_react36.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react36.createElement)("span", { key: at }, name))),
- (0, import_react36.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells)
+ (0, import_react37.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react37.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))),
+ (0, import_react37.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react37.createElement)("span", { key: at }, name))),
+ (0, import_react37.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells)
);
};
const shortcuts = [
@@ -15946,30 +16165,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_react36.createElement)(
+ return (0, import_react37.createElement)(
"div",
{ ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-pick-quick" },
- shortcuts.map(([id, label, from, to]) => (0, import_react36.createElement)("button", {
+ shortcuts.map(([id, label, from, to]) => (0, import_react37.createElement)("button", {
key: id,
type: "button",
"aria-pressed": span?.from === from && span?.to === to,
onClick: () => onPick(from, to)
}, label))
),
- (0, import_react36.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-pick-foot", role: "status" },
- (0, import_react36.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")),
- low === null ? null : (0, import_react36.createElement)("span", null, (0, import_react36.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`)
+ (0, import_react37.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")),
+ low === null ? null : (0, import_react37.createElement)("span", null, (0, import_react37.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`)
)
);
}
function Heat({ view, keep, paint, index }) {
- const [hover, setHover] = (0, import_react36.useState)(null);
+ const [hover, setHover] = (0, import_react37.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);
@@ -15985,13 +16204,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_react36.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday)));
+ months.push((0, import_react37.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_react36.createElement)("span", {
+ cells.push((0, import_react37.createElement)("span", {
key: ms,
className: "bt-us-cell",
style: { "--w": week },
@@ -15999,7 +16218,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_react36.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
+ }, level > 0 ? (0, import_react37.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
}
}
const tipSide = hover ? {
@@ -16008,61 +16227,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_react36.createElement)(
+ return (0, import_react37.createElement)(
"section",
{ className: "bt-us-card bt-us-heat", style: { "--i": index } },
- (0, import_react36.createElement)("div", { className: "bt-us-card-head" }, (0, import_react36.createElement)("h3", null, t("Activity")), (0, import_react36.createElement)("span", { className: "bt-us-head-note" }, summary)),
- (0, import_react36.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)("div", { className: "bt-us-card-head" }, (0, import_react37.createElement)("h3", null, t("Activity")), (0, import_react37.createElement)("span", { className: "bt-us-head-note" }, summary)),
+ (0, import_react37.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months),
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-heat-plot", onMouseLeave: () => setHover(null) },
- (0, import_react36.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
- hover ? (0, import_react36.createElement)(
+ (0, import_react37.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
+ hover ? (0, import_react37.createElement)(
"div",
{ className: "bt-us-tip bt-us-heat-tip", style: tipSide },
- (0, import_react36.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)),
- (0, import_react36.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) }))
+ (0, import_react37.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)),
+ (0, import_react37.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) }))
) : null
),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-heat-scale", "aria-hidden": true },
t("Light"),
- levels.map((level) => (0, import_react36.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react36.createElement)("i", { style: { background: soften(paint, level) } }))),
+ levels.map((level) => (0, import_react37.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react37.createElement)("i", { style: { background: soften(paint, level) } }))),
t("Heavy")
)
);
}
function Skeleton2() {
- return (0, import_react36.createElement)(
+ return (0, import_react37.createElement)(
"div",
{ className: "bt-us", "aria-busy": true },
- (0, import_react36.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-sheet bt-us-skel-sheet" },
- (0, import_react36.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
- (0, import_react36.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
+ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-pair" },
- (0, import_react36.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }),
- (0, import_react36.createElement)("div", { className: "bt-us-skel", style: { height: 200 } })
+ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }),
+ (0, import_react37.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_react36.useState)(null);
- const [error, setError] = (0, import_react36.useState)("");
- const [rangeId, setRangeId] = (0, import_react36.useState)("7d");
- const [span, setSpan] = (0, import_react36.useState)(null);
- const [picking, setPicking] = (0, import_react36.useState)(false);
- const [by, setBy] = (0, import_react36.useState)("bot");
- const tabs = (0, import_react36.useRef)(null);
+ const [view, setView] = (0, import_react37.useState)(null);
+ const [error, setError] = (0, import_react37.useState)("");
+ const [rangeId, setRangeId] = (0, import_react37.useState)("7d");
+ const [span, setSpan] = (0, import_react37.useState)(null);
+ const [picking, setPicking] = (0, import_react37.useState)(false);
+ const [by, setBy] = (0, import_react37.useState)("bot");
+ const tabs = (0, import_react37.useRef)(null);
const focus = focusOf(focusId);
- (0, import_react36.useEffect)(() => {
+ (0, import_react37.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(() => {
@@ -16072,7 +16291,7 @@ function UsagePage({ roster, actions, focusId }) {
const value = await actions.usage();
await resized;
if (stopped) return;
- (0, import_react36.startTransition)(() => setView(value));
+ (0, import_react37.startTransition)(() => setView(value));
setError("");
timer = setTimeout(load, value.scanning ? 3e3 : 2e4);
} catch (failure) {
@@ -16089,7 +16308,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_react36.useMemo)(() => {
+ const shaped = (0, import_react37.useMemo)(() => {
if (!view) return null;
const faces2 = view.owners.map((owner) => ownerFace(owner, roster));
const lifetime = /* @__PURE__ */ new Map();
@@ -16098,7 +16317,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_react36.createElement)(ModelGlyph, { name, paint, size }) };
+ return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react37.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;
@@ -16146,23 +16365,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_react36.useRef)(null);
- (0, import_react36.useEffect)(() => {
+ const root = (0, import_react37.useRef)(null);
+ (0, import_react37.useEffect)(() => {
root.current?.closest(".bt-settings-scroll")?.scrollTo?.({ top: 0 });
}, [focusId]);
if (!view) {
- return error ? (0, import_react36.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react36.createElement)(Skeleton2);
+ return error ? (0, import_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react37.createElement)(Skeleton2);
}
if (view.rows.length === 0) {
- return (0, import_react36.createElement)(
+ return (0, import_react37.createElement)(
"div",
{ className: "bt-us" },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-empty" },
- (0, import_react36.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }),
- (0, import_react36.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")),
- (0, import_react36.createElement)("span", null, t("Token usage shows up here after a model is called."))
+ (0, import_react37.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }),
+ (0, import_react37.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")),
+ (0, import_react37.createElement)("span", null, t("Token usage shows up here after a model is called."))
)
);
}
@@ -16187,7 +16406,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_react36.createElement)(Row3, {
+ const ownerRow = ([at, sum], index, total = whole) => (0, import_react37.createElement)(Row3, {
key: `o${at}`,
kind: faces[at].kind,
glyph: faces[at].glyph(32),
@@ -16199,7 +16418,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_react36.createElement)(Row3, {
+ const modelRow = ([at, sum], index) => (0, import_react37.createElement)(Row3, {
key: `m${at}`,
kind: "model",
glyph: models[at].glyph(32),
@@ -16211,30 +16430,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_react36.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range."));
+ const nothing = (0, import_react37.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_react36.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
+ (0, import_react37.createElement)(
"div",
{ key: "sheet", className: "bt-us-sheet" },
- (0, import_react36.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
- (0, import_react36.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
+ (0, import_react37.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
+ (0, import_react37.createElement)(
"div",
{ key: "pair", className: "bt-us-pair" },
- ownerAt >= 0 ? (0, import_react36.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react36.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_react36.createElement)(
+ ownerAt >= 0 ? (0, import_react37.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react37.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_react37.createElement)(
ListCard,
{ title: t("What for"), index: 3 },
- kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react36.createElement)(Row3, { 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_react37.createElement)(Row3, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing
)
),
- (0, import_react36.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 })
+ (0, import_react37.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 })
)
];
} else {
@@ -16243,39 +16462,39 @@ function UsagePage({ roster, actions, focusId }) {
const rest = owners.filter(([at]) => !isBot(at));
const usedModels = ranked(all.models);
body = [
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ key: "sheet", className: "bt-us-sheet" },
- (0, import_react36.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
- (0, import_react36.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
+ (0, import_react37.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
+ (0, import_react37.createElement)(
"div",
{ key: "pair", className: "bt-us-pair" },
- (0, import_react36.createElement)(
+ (0, import_react37.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_react36.createElement)("div", { className: "bt-us-split", role: "separator" }) : null,
+ bots.length > 0 && rest.length > 0 ? (0, import_react37.createElement)("div", { className: "bt-us-split", role: "separator" }) : null,
rest.map((entry, index) => ownerRow(entry, bots.length + index))
),
- (0, import_react36.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing)
+ (0, import_react37.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing)
),
- (0, import_react36.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 })
+ (0, import_react37.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 })
)
];
}
- return (0, import_react36.createElement)(
+ return (0, import_react37.createElement)(
"div",
{ ref: root, className: "bt-us", key: focusId ?? "all" },
- (0, import_react36.createElement)(
+ (0, import_react37.createElement)(
"div",
{ className: "bt-us-top" },
- (0, import_react36.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }),
- view.scanning ? (0, import_react36.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null,
- picking ? (0, import_react36.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null
+ (0, import_react37.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }),
+ view.scanning ? (0, import_react37.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null,
+ picking ? (0, import_react37.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null
),
- error ? (0, import_react36.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
+ error ? (0, import_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
body
);
}
@@ -16432,7 +16651,7 @@ button.bt-us-row:hover{background:var(--bt-hover)}
`;
// src/client/connectors.js
-var import_react37 = require("react");
+var import_react38 = require("react");
var TOKEN_URL = "https://github.com/settings/personal-access-tokens/new";
var COMPOSIO_KEY_URL = "https://platform.composio.dev/";
var FEISHU_WEBHOOK_URL = "https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot";
@@ -16466,17 +16685,17 @@ var APPS = [
["youtube", "YouTube"]
];
var appName = (slug) => APPS.find(([id]) => id === slug)?.[1] ?? slug;
-var GitHubMark = () => (0, import_react37.createElement)(
+var GitHubMark = () => (0, import_react38.createElement)(
"svg",
{ width: 20, height: 20, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": true },
- (0, import_react37.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_react38.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 FeishuMark = () => (0, import_react37.createElement)(
+var FeishuMark = () => (0, import_react38.createElement)(
"svg",
{ width: 20, height: 20, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
- (0, import_react37.createElement)("path", { d: "M3.2 6.6c3 .1 6.4 1.5 8.6 3.9-1.9.4-3.5 1.4-4.7 2.9-1.9-.6-3.4-1.9-3.9-3.6-.2-.7-.2-1.5 0-3.2z" }),
- (0, import_react37.createElement)("path", { d: "M16.8 5.4c-2.9.2-5.6 1.5-7.4 3.4 1.7 1.2 3.1 2.8 3.9 4.9 2.2-1.1 3.8-3.2 4-5.6.1-.9-.1-1.9-.4-2.7z" }),
- (0, import_react37.createElement)("path", { d: "M8.6 12.1c.9.7 1.7 1.6 2.2 2.7-1.1.2-2.3 0-3.3-.6.2-.8.6-1.5 1.1-2.1z" })
+ (0, import_react38.createElement)("path", { d: "M3.2 6.6c3 .1 6.4 1.5 8.6 3.9-1.9.4-3.5 1.4-4.7 2.9-1.9-.6-3.4-1.9-3.9-3.6-.2-.7-.2-1.5 0-3.2z" }),
+ (0, import_react38.createElement)("path", { d: "M16.8 5.4c-2.9.2-5.6 1.5-7.4 3.4 1.7 1.2 3.1 2.8 3.9 4.9 2.2-1.1 3.8-3.2 4-5.6.1-.9-.1-1.9-.4-2.7z" }),
+ (0, import_react38.createElement)("path", { d: "M8.6 12.1c.9.7 1.7 1.6 2.2 2.7-1.1.2-2.3 0-3.3-.6.2-.8.6-1.5 1.1-2.1z" })
);
var MARKS = { github: GitHubMark, composio: ConnectorIcon, feishu: FeishuMark };
var ABOUT = {
@@ -16496,11 +16715,11 @@ function statusText2(connector) {
}
function CredentialField({ credential, value, onChange, onEnter, autoFocus }) {
const label = t(credential.label);
- if (credential.masked) return (0, import_react37.createElement)(MaskedInput, { value, onChange, onEnter, label, autoFocus });
- return (0, import_react37.createElement)(
+ if (credential.masked) return (0, import_react38.createElement)(MaskedInput, { value, onChange, onEnter, label, autoFocus });
+ return (0, import_react38.createElement)(
"label",
{ className: "bt-q-field" },
- (0, import_react37.createElement)("input", {
+ (0, import_react38.createElement)("input", {
className: "bt-input",
type: "text",
value,
@@ -16516,31 +16735,31 @@ function CredentialField({ credential, value, onChange, onEnter, autoFocus }) {
);
}
function AllowedSenders({ connector, actions, run, busy }) {
- const [openId, setOpenId] = (0, import_react37.useState)("");
+ const [openId, setOpenId] = (0, import_react38.useState)("");
const allowed = connector.allowed ?? [];
const refused = connector.inbound?.refusedSenders ?? [];
const allow = (id) => void run(actions.connectorAllow(connector.id, id));
- return (0, import_react37.createElement)(
+ return (0, import_react38.createElement)(
"div",
{ className: "bt-connector-allowed" },
- (0, import_react37.createElement)("span", { className: "bt-connector-person-title" }, t("Who may talk to the Bots")),
- allowed.length === 0 && refused.length === 0 ? (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("Nobody yet. A message from anyone else starts no turn.")) : null,
- allowed.map((entry) => (0, import_react37.createElement)(
+ (0, import_react38.createElement)("span", { className: "bt-connector-person-title" }, t("Who may talk to the Bots")),
+ allowed.length === 0 && refused.length === 0 ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("Nobody yet. A message from anyone else starts no turn.")) : null,
+ allowed.map((entry) => (0, import_react38.createElement)(
"div",
{ key: entry.openId, className: "bt-connector-person" },
- (0, import_react37.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId),
- (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorBlock(connector.id, entry.openId)) }, t("Block"))
+ (0, import_react38.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId),
+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorBlock(connector.id, entry.openId)) }, t("Block"))
)),
- refused.map((entry) => (0, import_react37.createElement)(
+ refused.map((entry) => (0, import_react38.createElement)(
"div",
{ key: entry.openId, className: "bt-connector-person" },
- (0, import_react37.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId),
- (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => allow(entry.openId) }, t("Allow"))
+ (0, import_react38.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId),
+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => allow(entry.openId) }, t("Allow"))
)),
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"div",
{ className: "bt-connector-form-row" },
- (0, import_react37.createElement)("input", {
+ (0, import_react38.createElement)("input", {
className: "bt-input",
type: "text",
value: openId,
@@ -16555,7 +16774,7 @@ function AllowedSenders({ connector, actions, run, busy }) {
}
}
}),
- (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || openId.trim() === "", onClick: () => {
+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || openId.trim() === "", onClick: () => {
allow(openId.trim());
setOpenId("");
} }, t("Allow"))
@@ -16565,20 +16784,20 @@ function AllowedSenders({ connector, actions, run, busy }) {
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_react37.createElement)(
+ return (0, import_react38.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_react37.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
+ label ? (0, import_react38.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
);
}
function AppPicker({ apps, onChange, search, statuses = {} }) {
- const [query, setQuery] = (0, import_react37.useState)("");
- const [found, setFound] = (0, import_react37.useState)(null);
- const [failed, setFailed] = (0, import_react37.useState)(null);
- const names = (0, import_react37.useRef)(new Map(APPS));
- const asked = (0, import_react37.useRef)(0);
- (0, import_react37.useEffect)(() => {
+ const [query, setQuery] = (0, import_react38.useState)("");
+ const [found, setFound] = (0, import_react38.useState)(null);
+ const [failed, setFailed] = (0, import_react38.useState)(null);
+ const names = (0, import_react38.useRef)(new Map(APPS));
+ const asked = (0, import_react38.useRef)(0);
+ (0, import_react38.useEffect)(() => {
const text2 = query.trim();
if (text2 === "" || search === void 0) {
setFound(null);
@@ -16605,13 +16824,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_react37.createElement)(
+ return (0, import_react38.createElement)(
"div",
{ className: "bt-connector-apps" },
- apps.length > 0 ? (0, import_react37.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react37.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null,
- (0, import_react37.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_react37.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null,
- (0, import_react37.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react37.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => {
+ apps.length > 0 ? (0, import_react38.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react38.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null,
+ (0, import_react38.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_react38.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null,
+ (0, import_react38.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react38.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)))
@@ -16619,22 +16838,22 @@ function AppPicker({ apps, onChange, search, statuses = {} }) {
}
function AppStatusList({ apps, statuses }) {
if (apps.length === 0 || Object.keys(statuses).length === 0) return null;
- return (0, import_react37.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react37.createElement)(
+ return (0, import_react38.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react38.createElement)(
"li",
{ key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" },
- (0, import_react37.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)),
- (0, import_react37.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "")
+ (0, import_react38.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)),
+ (0, import_react38.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "")
)));
}
function ConnectorRow({ connector, roster, actions }) {
- const [form, setForm] = (0, import_react37.useState)(false);
- const [bots, setBots] = (0, import_react37.useState)(false);
- const [token, setToken] = (0, import_react37.useState)("");
- const [apps, setApps] = (0, import_react37.useState)(() => connector.apps ?? []);
- const [picking, setPicking] = (0, import_react37.useState)(false);
- const [busy, setBusy] = (0, import_react37.useState)(false);
- const [error, setError] = (0, import_react37.useState)(null);
- const [message, setMessage] = (0, import_react37.useState)("");
+ const [form, setForm] = (0, import_react38.useState)(false);
+ const [bots, setBots] = (0, import_react38.useState)(false);
+ const [token, setToken] = (0, import_react38.useState)("");
+ const [apps, setApps] = (0, import_react38.useState)(() => connector.apps ?? []);
+ const [picking, setPicking] = (0, import_react38.useState)(false);
+ const [busy, setBusy] = (0, import_react38.useState)(false);
+ const [error, setError] = (0, import_react38.useState)(null);
+ const [message, setMessage] = (0, import_react38.useState)("");
const run = (task, after) => {
setBusy(true);
setError(null);
@@ -16646,15 +16865,15 @@ function ConnectorRow({ connector, roster, actions }) {
const sender = connector.kind === "sender";
const channel = connector.kind === "channel";
const modes = connector.modes ?? [];
- const [mode, setMode] = (0, import_react37.useState)(() => connector.mode ?? modes[0]?.id);
- const [values, setValues] = (0, import_react37.useState)({});
+ const [mode, setMode] = (0, import_react38.useState)(() => connector.mode ?? modes[0]?.id);
+ const [values, setValues] = (0, import_react38.useState)({});
const chosen = modes.find((entry) => entry.id === mode) ?? modes[0];
const field = (name) => values[name] ?? "";
const setField = (name, value) => setValues((current) => ({ ...current, [name]: value }));
const filled = chosen !== void 0 && chosen.credentials.every((credential) => field(credential.name).trim() !== "");
- const [statuses, setStatuses] = (0, import_react37.useState)({});
+ const [statuses, setStatuses] = (0, import_react38.useState)({});
const appsKey = (connector.apps ?? []).join(",");
- (0, import_react37.useEffect)(() => {
+ (0, import_react38.useEffect)(() => {
if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) {
setStatuses({});
return void 0;
@@ -16669,7 +16888,7 @@ function ConnectorRow({ connector, roster, actions }) {
live = false;
};
}, [gateway, connector.state, appsKey, picking]);
- const search = (0, import_react37.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]);
+ const search = (0, import_react38.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]);
const connect = () => {
if (busy) return;
if (channel) {
@@ -16703,52 +16922,52 @@ function ConnectorRow({ connector, roster, actions }) {
const canPost = sender || channel && connector.mode === "webhook";
const perBot = !sender && (!channel || connector.mode === "app");
const Mark = MARKS[connector.id];
- const row = (0, import_react37.useRef)(null);
+ const row = (0, import_react38.useRef)(null);
useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}:${mode}`);
- return (0, import_react37.createElement)(
+ return (0, import_react38.createElement)(
"div",
{ ref: row, className: "bt-connector", "data-state": connector.state },
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"div",
{ className: "bt-connector-head" },
- (0, import_react37.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react37.createElement)(Mark) : null),
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react38.createElement)(Mark) : null),
+ (0, import_react38.createElement)(
"span",
{ className: "bt-connector-text" },
- (0, import_react37.createElement)("span", { className: "bt-connector-name" }, connector.name),
- (0, import_react37.createElement)("span", { className: "bt-connector-status" }, statusText2(connector))
+ (0, import_react38.createElement)("span", { className: "bt-connector-name" }, connector.name),
+ (0, import_react38.createElement)("span", { className: "bt-connector-status" }, statusText2(connector))
),
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"span",
{ className: "bt-connector-actions" },
- connector.state === "error" ? (0, import_react37.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_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
+ connector.state === "error" ? (0, import_react38.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_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
setApps(connector.apps ?? []);
setPicking(true);
} }, t("Choose apps")) : null,
- on && perBot ? (0, import_react37.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_react37.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_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect"))
+ on && perBot ? (0, import_react38.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_react38.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_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect"))
)
),
- on && gateway && !picking ? (0, import_react37.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null,
- on && perBot ? (0, import_react37.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null,
- on && bots ? (0, import_react37.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null,
- on && channel && connector.mode === "app" ? (0, import_react37.createElement)(AllowedSenders, { connector, actions, run, busy }) : null,
- on && canPost ? (0, import_react37.createElement)(
+ on && gateway && !picking ? (0, import_react38.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null,
+ on && perBot ? (0, import_react38.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null,
+ on && bots ? (0, import_react38.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null,
+ on && channel && connector.mode === "app" ? (0, import_react38.createElement)(AllowedSenders, { connector, actions, run, busy }) : null,
+ on && canPost ? (0, import_react38.createElement)(
"div",
{ className: "bt-connector-form" },
- (0, import_react37.createElement)("label", { className: "bt-q-field" }, (0, import_react37.createElement)("input", { className: "bt-input", type: "text", value: message, "aria-label": t("Message to send to Feishu"), placeholder: t("Type a message for the Feishu group"), onChange: (event) => setMessage(event.target.value) })),
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)("label", { className: "bt-q-field" }, (0, import_react38.createElement)("input", { className: "bt-input", type: "text", value: message, "aria-label": t("Message to send to Feishu"), placeholder: t("Type a message for the Feishu group"), onChange: (event) => setMessage(event.target.value) })),
+ (0, import_react38.createElement)(
"div",
{ className: "bt-connector-form-row" },
- (0, import_react37.createElement)("span", { className: "bt-connector-link" }),
- (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || message.trim() === "" || actions.connectorSend === void 0, onClick: () => void run(actions.connectorSend(connector.id, message), () => setMessage("")) }, t("Send"))
+ (0, import_react38.createElement)("span", { className: "bt-connector-link" }),
+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || message.trim() === "" || actions.connectorSend === void 0, onClick: () => void run(actions.connectorSend(connector.id, message), () => setMessage("")) }, t("Send"))
)
) : null,
- !on && form ? (0, import_react37.createElement)(
+ !on && form ? (0, import_react38.createElement)(
"div",
{ className: "bt-connector-form" },
- channel ? (0, import_react37.createElement)("div", { className: "bt-states", role: "group", "aria-label": t("How to connect") }, modes.map((entry) => (0, import_react37.createElement)("button", {
+ channel ? (0, import_react38.createElement)("div", { className: "bt-states", role: "group", "aria-label": t("How to connect") }, modes.map((entry) => (0, import_react38.createElement)("button", {
key: entry.id,
type: "button",
className: "bt-state",
@@ -16759,67 +16978,67 @@ function ConnectorRow({ connector, roster, actions }) {
setError(null);
}
}, t(entry.label)))) : null,
- channel ? (chosen?.credentials ?? []).map((credential, index) => (0, import_react37.createElement)(CredentialField, {
+ channel ? (chosen?.credentials ?? []).map((credential, index) => (0, import_react38.createElement)(CredentialField, {
key: credential.name,
credential,
value: field(credential.name),
onChange: (value) => setField(credential.name, value),
onEnter: connect,
autoFocus: index === 0
- })) : (0, import_react37.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react37.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })),
- gateway ? (0, import_react37.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null,
- (0, import_react37.createElement)(
+ })) : (0, import_react38.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react38.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })),
+ gateway ? (0, import_react38.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null,
+ (0, import_react38.createElement)(
"div",
{ className: "bt-connector-form-row" },
- (0, import_react37.createElement)(
+ (0, import_react38.createElement)(
"a",
{ className: "bt-connector-link", href: channel ? chosen?.link ?? FEISHU_WEBHOOK_URL : gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" },
channel ? t(chosen?.linkLabel ?? "Get a webhook URL in Feishu") : gateway ? t("Get a Composio API key") : t("Create a token on GitHub")
),
- (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
setForm(false);
setToken("");
setValues({});
setError(null);
} }, t("Cancel")),
- (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || (channel ? !filled : token.trim() === ""), onClick: connect }, busy ? t("Connecting…") : t("Connect"))
+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || (channel ? !filled : token.trim() === ""), onClick: connect }, busy ? t("Connecting…") : t("Connect"))
)
) : null,
- on && gateway && picking ? (0, import_react37.createElement)(
+ on && gateway && picking ? (0, import_react38.createElement)(
"div",
{ className: "bt-connector-form" },
- (0, import_react37.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }),
- (0, import_react37.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_react37.createElement)(
+ (0, import_react38.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }),
+ (0, import_react38.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_react38.createElement)(
"div",
{ className: "bt-connector-form-row" },
- (0, import_react37.createElement)("span", { className: "bt-connector-link" }),
- (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
+ (0, import_react38.createElement)("span", { className: "bt-connector-link" }),
+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
setPicking(false);
setError(null);
} }, t("Cancel")),
- (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save"))
+ (0, import_react38.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_react37.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null
+ error ? (0, import_react38.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null
);
}
function ConnectorsPage({ roster, actions }) {
const list = roster?.connectors ?? [];
- return (0, import_react37.createElement)(
+ return (0, import_react38.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react37.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react37.createElement)("div", { className: "bt-set-row" }, (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react37.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions })))
+ (0, import_react38.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react38.createElement)("div", { className: "bt-set-row" }, (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react38.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions })))
);
}
// src/client/feedback.js
-var import_react38 = require("react");
-var FeedbackIcon = () => (0, import_react38.createElement)(
+var import_react39 = require("react");
+var FeedbackIcon = () => (0, import_react39.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_react38.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_react38.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" })
+ (0, import_react39.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_react39.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" })
);
function download(name, text2) {
const url = URL.createObjectURL(new Blob([text2], { type: "text/plain;charset=utf-8" }));
@@ -16842,16 +17061,16 @@ function entrySummary(entry) {
}
var SOURCES = { turn: "Model call", host: "FlyBot", api: "FlyBot", browser: "This window" };
function FeedbackPage({ actions, run, Section, Row: Row4 }) {
- const [what, setWhat] = (0, import_react38.useState)("");
- const [report, setReport] = (0, import_react38.useState)(null);
- const [preview, setPreview] = (0, import_react38.useState)(false);
- const [note, setNote] = (0, import_react38.useState)("");
+ const [what, setWhat] = (0, import_react39.useState)("");
+ const [report, setReport] = (0, import_react39.useState)(null);
+ const [preview, setPreview] = (0, import_react39.useState)(false);
+ const [note, setNote] = (0, import_react39.useState)("");
const fetchReport = () => actions.diagnostics({ client: clientFacts(), clientErrors: clientErrors() }).then((next) => {
setReport(next);
return next;
});
const load = () => fetchReport().then(() => void 0);
- (0, import_react38.useEffect)(() => {
+ (0, import_react39.useEffect)(() => {
void run(load());
}, []);
const copyReport = () => run(fetchReport().then(async (next) => {
@@ -16859,7 +17078,7 @@ function FeedbackPage({ actions, run, Section, Row: Row4 }) {
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_react38.useState)(null);
+ const [issueLink, setIssueLink] = (0, import_react39.useState)(null);
const openIssue = () => {
const win = window.open("", "_blank");
if (win !== null) win.opener = null;
@@ -16887,59 +17106,59 @@ function FeedbackPage({ actions, run, Section, Row: Row4 }) {
const total = (report?.total ?? 0) + browserErrors.length;
const shown = entries2.slice(0, 8);
return [
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
Section,
{ key: "report", title: t("Report a problem") },
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"div",
{ className: "bt-set-row bt-set-paste bt-fb-what" },
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
"label",
{ className: "bt-set-copy", htmlFor: "bt-fb-what" },
- (0, import_react38.createElement)("span", null, t("What happened?")),
- (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue."))
+ (0, import_react39.createElement)("span", null, t("What happened?")),
+ (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue."))
),
- (0, import_react38.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_react39.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_react38.createElement)(
+ (0, import_react39.createElement)(
Row4,
{ label: t("Diagnostics"), hint: t("Versions, system, models, and the latest errors. Saved keys, tokens, and your home folder are hidden.") },
- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")),
- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy"))
+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")),
+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy"))
),
- preview ? (0, import_react38.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react38.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null,
- (0, import_react38.createElement)(
+ preview ? (0, import_react39.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react39.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null,
+ (0, import_react39.createElement)(
Row4,
{ 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_react38.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
+ (0, import_react39.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
)
),
- note ? (0, import_react38.createElement)(
+ note ? (0, import_react39.createElement)(
"div",
{ key: "note", className: "bt-note bt-fb-note", role: "status" },
note,
- issueLink ? [" ", (0, import_react38.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null
+ issueLink ? [" ", (0, import_react39.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null
) : null,
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
Section,
{ key: "errors", title: report ? t("Recent errors · {count}", { count: total }) : t("Recent errors") },
- shown.length === 0 ? (0, import_react38.createElement)(Row4, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react38.createElement)(
+ shown.length === 0 ? (0, import_react39.createElement)(Row4, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react39.createElement)(
"div",
{ key: `${entry.time}-${index}`, className: "bt-set-row bt-fb-entry", title: entry.text },
- (0, import_react38.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
+ (0, import_react39.createElement)(
"div",
{ className: "bt-set-copy" },
- (0, import_react38.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)),
- (0, import_react38.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · "))
+ (0, import_react39.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)),
+ (0, import_react39.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_react38.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null
+ entry.code ? (0, import_react39.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null
)),
- (0, import_react38.createElement)(
+ (0, import_react39.createElement)(
Row4,
{ label: t("Error log"), hint: t("Kept on this computer across restarts, up to about 1 MB.") },
- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")),
- (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear"))
+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")),
+ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear"))
)
)
];
@@ -16947,8 +17166,8 @@ function FeedbackPage({ actions, run, Section, Row: Row4 }) {
var TOAST_MS = 6e3;
function ErrorToast({ useUi, actions }) {
const toast = useUi((state) => state.toast);
- const [leaving, setLeaving] = (0, import_react38.useState)(false);
- (0, import_react38.useEffect)(() => {
+ const [leaving, setLeaving] = (0, import_react39.useState)(false);
+ (0, import_react39.useEffect)(() => {
if (!toast) return void 0;
setLeaving(false);
const fade = setTimeout(() => setLeaving(true), TOAST_MS);
@@ -16960,15 +17179,15 @@ function ErrorToast({ useUi, actions }) {
}, [toast?.token]);
if (!toast) return null;
const info = toast.tone === "info";
- return (0, import_react38.createElement)(
+ return (0, import_react39.createElement)(
"div",
{ className: "bt-toast", role: info ? "status" : "alert", "data-tone": toast.tone, "data-leaving": leaving || void 0 },
- (0, import_react38.createElement)("span", { className: "bt-toast-text" }, toast.text),
- info ? null : (0, import_react38.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => {
+ (0, import_react39.createElement)("span", { className: "bt-toast-text" }, toast.text),
+ info ? null : (0, import_react39.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => {
actions.dismissToast(toast.token);
actions.openSettings("feedback");
} }, t("Details")),
- (0, import_react38.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react38.createElement)(CloseIcon))
+ (0, import_react39.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react39.createElement)(CloseIcon))
);
}
var FEEDBACK_CSS = `
@@ -16999,7 +17218,7 @@ var FEEDBACK_CSS = `
`;
// src/client/about.js
-var import_react39 = require("react");
+var import_react40 = 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…");
@@ -17034,9 +17253,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: Row4 }) {
const update = useUpdate((value) => value) ?? {};
- const [checking, setChecking] = (0, import_react39.useState)(false);
- const [stars, setStars] = (0, import_react39.useState)(null);
- (0, import_react39.useEffect)(() => {
+ const [checking, setChecking] = (0, import_react40.useState)(false);
+ const [stars, setStars] = (0, import_react40.useState)(null);
+ (0, import_react40.useEffect)(() => {
let live = true;
actions.repoStars().then((count) => {
if (live && Number.isInteger(count)) setStars(count);
@@ -17057,29 +17276,29 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row4 }) {
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_react39.createElement)(
+ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react40.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_react39.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_react39.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_react39.createElement)(
+ ) : (0, import_react40.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_react40.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_react40.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_react39.createElement)(
+ (0, import_react40.createElement)(
"div",
{ key: "hero", className: "bt-about-hero" },
- (0, import_react39.createElement)("span", { className: "bt-about-mark" }, (0, import_react39.createElement)(FlyBotMark, { size: 52 })),
- (0, import_react39.createElement)(
+ (0, import_react40.createElement)("span", { className: "bt-about-mark" }, (0, import_react40.createElement)(FlyBotMark, { size: 52 })),
+ (0, import_react40.createElement)(
"div",
{ className: "bt-about-copy" },
- (0, import_react39.createElement)("span", { className: "bt-about-name" }, t("FlyBot")),
- update.current ? (0, import_react39.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null
+ (0, import_react40.createElement)("span", { className: "bt-about-name" }, t("FlyBot")),
+ update.current ? (0, import_react40.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null
),
- repo ? (0, import_react39.createElement)(
+ repo ? (0, import_react40.createElement)(
"a",
{
className: "bt-star",
@@ -17089,23 +17308,23 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row4 }) {
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_react39.createElement)("span", { className: "bt-star-main" }, (0, import_react39.createElement)("span", { className: "bt-star-icon" }, (0, import_react39.createElement)(StarLineIcon)), "Star"),
- stars === null ? null : (0, import_react39.createElement)("span", { className: "bt-star-count" }, starCount(stars))
+ (0, import_react40.createElement)("span", { className: "bt-star-main" }, (0, import_react40.createElement)("span", { className: "bt-star-icon" }, (0, import_react40.createElement)(StarLineIcon)), "Star"),
+ stars === null ? null : (0, import_react40.createElement)("span", { className: "bt-star-count" }, starCount(stars))
) : null
),
- repo ? (0, import_react39.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_react39.createElement)(
+ repo ? (0, import_react40.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_react40.createElement)(
Section,
{ key: "updates", title: t("Updates") },
- (0, import_react39.createElement)(Row4, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react39.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null),
- dsh?.current ? (0, import_react39.createElement)(Row4, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null
+ (0, import_react40.createElement)(Row4, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react40.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null),
+ dsh?.current ? (0, import_react40.createElement)(Row4, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null
),
- (0, import_react39.createElement)(
+ (0, import_react40.createElement)(
Section,
{ key: "links", title: t("Links") },
- repo ? (0, import_react39.createElement)(Row4, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react39.createElement)("a", linkProps(repo), (0, import_react39.createElement)(GitHubMark), t("Open"))) : null,
- update.notesUrl ? (0, import_react39.createElement)(Row4, { label: t("Release notes") }, (0, import_react39.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
- (0, import_react39.createElement)(Row4, { label: t("Report a problem") }, (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open")))
+ repo ? (0, import_react40.createElement)(Row4, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react40.createElement)("a", linkProps(repo), (0, import_react40.createElement)(GitHubMark), t("Open"))) : null,
+ update.notesUrl ? (0, import_react40.createElement)(Row4, { label: t("Release notes") }, (0, import_react40.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
+ (0, import_react40.createElement)(Row4, { label: t("Report a problem") }, (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open")))
)
];
}
@@ -17134,19 +17353,19 @@ var ABOUT_CSS = `
`;
// src/client/settings.js
-var import_react40 = require("react");
+var import_react41 = require("react");
var capitalize2 = (word) => word[0].toUpperCase() + word.slice(1);
var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["projects", "Projects"], ["connectors", "Connectors"], ["capabilities", "Capabilities"], ["schedules", "Scheduled tasks"], ["workflows", "Workflows"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"], ["about", "About FlyBot"]];
var PAGE_ICONS = { ...SETTINGS_ICONS, projects: FolderIcon, connectors: ConnectorIcon, workflows: WorkflowIcon, capabilities: PlugIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon };
function SetSection({ title, children }) {
- return (0, import_react40.createElement)("section", { className: "bt-set-section" }, (0, import_react40.createElement)("h3", null, title), (0, import_react40.createElement)("div", { className: "bt-set-card" }, children));
+ return (0, import_react41.createElement)("section", { className: "bt-set-section" }, (0, import_react41.createElement)("h3", null, title), (0, import_react41.createElement)("div", { className: "bt-set-card" }, children));
}
function SetRow({ label, hint, children }) {
- return (0, import_react40.createElement)(
+ return (0, import_react41.createElement)(
"div",
{ className: "bt-set-row" },
- (0, import_react40.createElement)("div", { className: "bt-set-copy" }, (0, import_react40.createElement)("span", null, label), hint ? (0, import_react40.createElement)("span", { className: "bt-set-hint" }, hint) : null),
- children ? (0, import_react40.createElement)("div", { className: "bt-set-control" }, children) : null
+ (0, import_react41.createElement)("div", { className: "bt-set-copy" }, (0, import_react41.createElement)("span", null, label), hint ? (0, import_react41.createElement)("span", { className: "bt-set-hint" }, hint) : null),
+ children ? (0, import_react41.createElement)("div", { className: "bt-set-control" }, children) : null
);
}
var NOTIFY_KINDS = [
@@ -17158,35 +17377,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_react40.useState)(prefs.accent ?? "#4d6bfe");
- const timer = (0, import_react40.useRef)(0);
- (0, import_react40.useEffect)(() => () => clearTimeout(timer.current), []);
+ const [custom, setCustom] = (0, import_react41.useState)(prefs.accent ?? "#4d6bfe");
+ const timer = (0, import_react41.useRef)(0);
+ (0, import_react41.useEffect)(() => () => clearTimeout(timer.current), []);
const pickCustom = (value) => {
setCustom(value);
clearTimeout(timer.current);
timer.current = setTimeout(() => run(actions.setPrefs({ accent: value })), 350);
};
return [
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
SetSection,
{ key: "look", title: t("Appearance") },
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
SetRow,
{ label: t("Theme"), hint: t("Colors for the whole team, on every device.") },
- (0, import_react40.createElement)(
+ (0, import_react41.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_react40.createElement)("option", { key: id, value: id }, t(theme.label)))
+ Object.entries(themes).map(([id, theme]) => (0, import_react41.createElement)("option", { key: id, value: id }, t(theme.label)))
)
),
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
SetRow,
{ label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") },
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
"div",
{ className: "bt-swatches" },
- (0, import_react40.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_react40.createElement)("button", {
+ (0, import_react41.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_react41.createElement)("button", {
key: ink,
type: "button",
className: "bt-swatch",
@@ -17196,25 +17415,25 @@ function GeneralPage({ roster, actions, run }) {
style: { background: INKS[ink][0] },
onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] }))
})),
- (0, import_react40.createElement)(
+ (0, import_react41.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_react40.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) })
+ (0, import_react41.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_react40.createElement)(
+ (0, import_react41.createElement)(
SetSection,
{ key: "notify", title: t("Notifications") },
- (0, import_react40.createElement)(SwitchRow, {
+ (0, import_react41.createElement)(SwitchRow, {
label: t("In other chats"),
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_react40.createElement)(SwitchRow, {
+ NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react41.createElement)(SwitchRow, {
key: kind,
label: t(label),
hint: hint ? t(hint) : void 0,
@@ -17230,52 +17449,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_react40.createElement)(
+ return (0, import_react41.createElement)(
SetSection,
{ title: t("Main Bot") },
- (0, import_react40.createElement)(
+ (0, import_react41.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_react40.createElement)("select", {
+ (0, import_react41.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_react40.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react40.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
+ }, first ? null : (0, import_react41.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react41.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
),
- others.map((bot) => (0, import_react40.createElement)(
+ others.map((bot) => (0, import_react41.createElement)(
SetRow,
{ key: bot.id, label: bot.name, hint: t("Also a Main Bot.") },
- (0, import_react40.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_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role"))
+ (0, import_react41.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_react41.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_react40.createElement)(
+ candidates.length > 0 ? (0, import_react41.createElement)(
SetRow,
{ label: t("Add a Main Bot"), hint: t("Main Bots can create and change Bots, and run every group chat.") },
- (0, import_react40.createElement)("select", {
+ (0, import_react41.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_react40.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react40.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
+ }, (0, import_react41.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react41.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_react40.createElement)(
+ return (0, import_react41.createElement)(
SetSection,
{ title: t("Animation") },
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
SetRow,
{ label: t("Bot animation") },
- (0, import_react40.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react40.createElement)("button", {
+ (0, import_react41.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react41.createElement)("button", {
key: id,
type: "button",
"aria-pressed": motion === id,
@@ -17287,69 +17506,69 @@ function MotionSection({ roster, actions, run }) {
);
}
function BotsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react40.useState)(false);
+ const [creating, setCreating] = (0, import_react41.useState)(false);
return [
- (0, import_react40.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
- (0, import_react40.createElement)(MotionSection, { key: "motion", roster, actions, run }),
- creating ? (0, import_react40.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react40.createElement)(
+ (0, import_react41.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
+ (0, import_react41.createElement)(MotionSection, { key: "motion", roster, actions, run }),
+ creating ? (0, import_react41.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react41.createElement)(
"div",
{ key: "new", className: "bt-actions" },
- (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react40.createElement)(PlusIcon), t("New Bot"))
+ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react41.createElement)(PlusIcon), t("New Bot"))
),
- (0, import_react40.createElement)(
+ (0, import_react41.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_react40.createElement)(
+ roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react41.createElement)(
"button",
{ key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) },
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react40.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
- (0, import_react40.createElement)("span", { className: "bt-row-title" }, bot.name),
- bot.role ? (0, import_react40.createElement)("span", { className: "bt-tag" }, bot.role) : null,
- isMainOf(roster, bot.id) ? (0, import_react40.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
- bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react40.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
+ (0, import_react41.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
+ (0, import_react41.createElement)("span", { className: "bt-row-title" }, bot.name),
+ bot.role ? (0, import_react41.createElement)("span", { className: "bt-tag" }, bot.role) : null,
+ isMainOf(roster, bot.id) ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
+ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
),
- (0, import_react40.createElement)("span", { className: "bt-chevron" }, (0, import_react40.createElement)(ChevronRightIcon))
+ (0, import_react41.createElement)("span", { className: "bt-chevron" }, (0, import_react41.createElement)(ChevronRightIcon))
))
)
];
}
function GroupsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react40.useState)(false);
+ const [creating, setCreating] = (0, import_react41.useState)(false);
return [
- creating ? (0, import_react40.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react40.createElement)(
+ creating ? (0, import_react41.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react41.createElement)(
"div",
{ key: "new", className: "bt-actions" },
- (0, import_react40.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_react40.createElement)(PlusIcon), t("Create group chat"))
+ (0, import_react41.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_react41.createElement)(PlusIcon), t("Create group chat"))
),
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
SetSection,
{ key: "groups", title: t("Group chats · {count}", { count: roster.rooms.length }) },
- roster.rooms.length === 0 ? (0, import_react40.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_react41.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_react40.createElement)(
+ return (0, import_react41.createElement)(
"button",
{ key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) },
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react40.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
- (0, import_react40.createElement)("span", { className: "bt-row-title" }, room2.name),
- (0, import_react40.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })),
- admin ? (0, import_react40.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react40.createElement)(AdminStar), admin.name) : null,
- room2.hidden ? (0, import_react40.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
+ (0, import_react41.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
+ (0, import_react41.createElement)("span", { className: "bt-row-title" }, room2.name),
+ (0, import_react41.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })),
+ admin ? (0, import_react41.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react41.createElement)(AdminStar), admin.name) : null,
+ room2.hidden ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
),
- (0, import_react40.createElement)("span", { className: "bt-chevron" }, (0, import_react40.createElement)(ChevronRightIcon))
+ (0, import_react41.createElement)("span", { className: "bt-chevron" }, (0, import_react41.createElement)(ChevronRightIcon))
);
})
)
];
}
function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) {
- const [error, setError] = (0, import_react40.useState)("");
- const close = (0, import_react40.useCallback)(() => actions.closeSettings(), []);
+ const [error, setError] = (0, import_react41.useState)("");
+ const close = (0, import_react41.useCallback)(() => actions.closeSettings(), []);
useEscape(close);
const run = (promise) => Promise.resolve(promise).then(() => {
setError("");
@@ -17370,34 +17589,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_react40.useRef)({ key: pageKey, depth, move: "up" });
+ const last = (0, import_react41.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_react40.useState)(shown === "usage");
+ const [usageOpened, setUsageOpened] = (0, import_react41.useState)(shown === "usage");
if (shown === "usage" && !usageOpened) setUsageOpened(true);
- const errorLine = error ? (0, import_react40.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
- return (0, import_react40.createElement)(
+ const errorLine = error ? (0, import_react41.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
+ return (0, import_react41.createElement)(
"div",
{ className: "bt-settings-layer", "data-leaving": leaving || void 0 },
- (0, import_react40.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
+ (0, import_react41.createElement)(
"div",
{ className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("FlyBot settings"), "data-wide": shown === "usage" ? "" : void 0 },
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
"nav",
{ ref: nav.box, className: "bt-settings-nav", "aria-label": t("Settings pages") },
- (0, import_react40.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
- pages.map(([key, text2]) => (0, import_react40.createElement)("button", {
+ (0, import_react41.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
+ pages.map(([key, text2]) => (0, import_react41.createElement)("button", {
key,
type: "button",
className: "bt-settings-item",
"aria-current": shown === key ? "page" : void 0,
onClick: () => actions.openSettings(key)
- }, (0, import_react40.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text2))),
+ }, (0, import_react41.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text2))),
// 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_react40.createElement)("button", {
+ (0, import_react41.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"),
@@ -17406,44 +17625,44 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false
close();
openHarnessSettings();
}
- }, (0, import_react40.createElement)(SlidersIcon), t("Harness settings"), (0, import_react40.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
+ }, (0, import_react41.createElement)(SlidersIcon), t("Harness settings"), (0, import_react41.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
),
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
"div",
{ className: "bt-settings-main" },
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
"div",
{ className: "bt-settings-head" },
- back ? (0, import_react40.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react40.createElement)(ChevronLeftIcon)) : null,
- (0, import_react40.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label)
+ back ? (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react41.createElement)(ChevronLeftIcon)) : null,
+ (0, import_react41.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label)
),
- shown === "usage" ? null : (0, import_react40.createElement)(
+ shown === "usage" ? null : (0, import_react41.createElement)(
"div",
{ key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move },
errorLine,
- shown === "general" ? (0, import_react40.createElement)(GeneralPage, { roster, actions, run }) : null,
- bot ? (0, import_react40.createElement)("div", { className: "bt-settings-form" }, (0, import_react40.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
- shown === "bots" && !bot ? (0, import_react40.createElement)(BotsPage, { roster, actions, run }) : null,
- room2 ? (0, import_react40.createElement)("div", { className: "bt-settings-form" }, (0, import_react40.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
- shown === "groups" && !room2 ? (0, import_react40.createElement)(GroupsPage, { roster, actions, run }) : null,
- shown === "projects" ? (0, import_react40.createElement)(ProjectsPage, { roster, actions, run, call: call2 }) : null,
- shown === "connectors" ? (0, import_react40.createElement)(ConnectorsPage, { roster, actions }) : null,
- shown === "workflows" ? (0, import_react40.createElement)(WorkflowsPage, { call: call2, roster }) : null,
- pluginPage ? (0, import_react40.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null,
- shown === "capabilities" ? (0, import_react40.createElement)(CapabilitiesPage, { roster, actions, run }) : null,
- shown === "schedules" ? (0, import_react40.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
- shown === "secrets" ? (0, import_react40.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
- shown === "feedback" ? (0, import_react40.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
- shown === "about" ? (0, import_react40.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null
+ shown === "general" ? (0, import_react41.createElement)(GeneralPage, { roster, actions, run }) : null,
+ bot ? (0, import_react41.createElement)("div", { className: "bt-settings-form" }, (0, import_react41.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
+ shown === "bots" && !bot ? (0, import_react41.createElement)(BotsPage, { roster, actions, run }) : null,
+ room2 ? (0, import_react41.createElement)("div", { className: "bt-settings-form" }, (0, import_react41.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
+ shown === "groups" && !room2 ? (0, import_react41.createElement)(GroupsPage, { roster, actions, run }) : null,
+ shown === "projects" ? (0, import_react41.createElement)(ProjectsPage, { roster, actions, run, call: call2 }) : null,
+ shown === "connectors" ? (0, import_react41.createElement)(ConnectorsPage, { roster, actions }) : null,
+ shown === "workflows" ? (0, import_react41.createElement)(WorkflowsPage, { call: call2, roster }) : null,
+ pluginPage ? (0, import_react41.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null,
+ shown === "capabilities" ? (0, import_react41.createElement)(CapabilitiesPage, { roster, actions, run }) : null,
+ shown === "schedules" ? (0, import_react41.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
+ shown === "secrets" ? (0, import_react41.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
+ shown === "feedback" ? (0, import_react41.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
+ shown === "about" ? (0, import_react41.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null
),
- usageOpened ? (0, import_react40.createElement)(
+ usageOpened ? (0, import_react41.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_react40.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
+ (0, import_react41.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
) : null
),
- (0, import_react40.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react40.createElement)(CloseIcon))
+ (0, import_react41.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react41.createElement)(CloseIcon))
)
);
}
@@ -17454,35 +17673,35 @@ var projectMembers = (roster) => [
...(roster?.bots ?? []).filter((bot) => !bot.remote).map((bot) => ({ kind: "bot", id: bot.id, name: bot.name, projectId: bot.projectId ?? null })),
...(roster?.rooms ?? []).map((room2) => ({ kind: "group", id: room2.id, name: room2.name, projectId: room2.projectId ?? null }))
];
-function ProjectsList({ projects, roster, call: call4, reload }) {
+function ProjectsList({ projects, roster, call: call5, reload }) {
const members = projectMembers(roster);
const bind = async (choice, project) => {
const [kind, id] = String(choice ?? "").split(":");
if (!kind || !id) return;
- await call4("project-bind", { kind, id, projectId: project.id });
+ await call5("project-bind", { kind, id, projectId: project.id });
await reload?.();
};
const unbind = async (member) => {
- await call4("project-bind", { kind: member.kind, id: member.id, projectId: null });
+ await call5("project-bind", { kind: member.kind, id: member.id, projectId: null });
await reload?.();
};
const bound2 = members.filter((member) => member.projectId !== null && member.projectId !== void 0);
return [
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
SetSection,
{ key: "projects", title: t("Projects · {count}", { count: projects.length }) },
- projects.length === 0 ? (0, import_react40.createElement)("div", { className: "bt-set-row" }, (0, import_react40.createElement)("div", { className: "bt-set-copy" }, (0, import_react40.createElement)("span", null, t("No Project yet — create one.")))) : projects.map((project) => (0, import_react40.createElement)(
+ projects.length === 0 ? (0, import_react41.createElement)("div", { className: "bt-set-row" }, (0, import_react41.createElement)("div", { className: "bt-set-copy" }, (0, import_react41.createElement)("span", null, t("No Project yet — create one.")))) : projects.map((project) => (0, import_react41.createElement)(
SetRow,
{
key: project.id,
label: project.name,
hint: project.workDir === null || project.workDir === void 0 ? void 0 : project.workDir
},
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
"div",
{ className: "bt-project-row-controls" },
- (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openProjectBoard(project.id) }, t("Open board")),
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openProjectBoard(project.id) }, t("Open board")),
+ (0, import_react41.createElement)(
"select",
{
className: "bt-select",
@@ -17492,8 +17711,8 @@ function ProjectsList({ projects, roster, call: call4, reload }) {
void bind(event.target.value, project);
}
},
- (0, import_react40.createElement)("option", { value: "" }, t("Bind a Bot or group")),
- members.map((member) => (0, import_react40.createElement)(
+ (0, import_react41.createElement)("option", { value: "" }, t("Bind a Bot or group")),
+ members.map((member) => (0, import_react41.createElement)(
"option",
{ key: `${member.kind}:${member.id}`, value: `${member.kind}:${member.id}` },
`${member.kind === "group" ? t("Group") : t("Bot")} · ${member.name}`
@@ -17502,32 +17721,32 @@ function ProjectsList({ projects, roster, call: call4, reload }) {
)
))
),
- bound2.length === 0 ? null : (0, import_react40.createElement)(
+ bound2.length === 0 ? null : (0, import_react41.createElement)(
SetSection,
{ key: "bindings", title: t("Bindings") },
- bound2.map((member) => (0, import_react40.createElement)(
+ bound2.map((member) => (0, import_react41.createElement)(
SetRow,
{
key: `${member.kind}:${member.id}`,
label: member.name,
hint: member.kind === "group" ? t("Group") : t("Bot")
},
- (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
+ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
void unbind(member);
} }, t("Unbind"))
))
)
];
}
-function ProjectsPage({ roster, actions, run, call: call4 }) {
- const [state, setState] = (0, import_react40.useState)({ projects: [], error: null });
- const [name, setName] = (0, import_react40.useState)("");
- const [workDir, setWorkDir] = (0, import_react40.useState)(null);
- const reload = (0, import_react40.useCallback)(async () => {
- const listing = await call4("projects");
+function ProjectsPage({ roster, actions, run, call: call5 }) {
+ const [state, setState] = (0, import_react41.useState)({ projects: [], error: null });
+ const [name, setName] = (0, import_react41.useState)("");
+ const [workDir, setWorkDir] = (0, import_react41.useState)(null);
+ const reload = (0, import_react41.useCallback)(async () => {
+ const listing = await call5("projects");
setState({ projects: listing?.projects ?? [], error: null });
- }, [call4]);
- (0, import_react40.useEffect)(() => {
+ }, [call5]);
+ (0, import_react41.useEffect)(() => {
void reload().catch((error) => setState({ projects: [], error: error?.message ?? String(error) }));
}, [reload]);
const create = async () => {
@@ -17536,7 +17755,7 @@ function ProjectsPage({ roster, actions, run, call: call4 }) {
const body = { name: trimmed3 };
if (workDir !== null && workDir !== "") body.workDir = workDir;
try {
- await call4("project-create", body);
+ await call5("project-create", body);
setName("");
setWorkDir(null);
await reload();
@@ -17545,14 +17764,14 @@ function ProjectsPage({ roster, actions, run, call: call4 }) {
}
};
return [
- state.error === null ? null : (0, import_react40.createElement)("div", { key: "error", className: "bt-error", role: "alert" }, state.error),
- (0, import_react40.createElement)(
+ state.error === null ? null : (0, import_react41.createElement)("div", { key: "error", className: "bt-error", role: "alert" }, state.error),
+ (0, import_react41.createElement)(
SetSection,
{ key: "new", title: t("New Project") },
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
SetRow,
{ label: t("Name") },
- (0, import_react40.createElement)("input", {
+ (0, import_react41.createElement)("input", {
className: "bt-input",
"aria-label": t("Project name"),
value: name,
@@ -17560,35 +17779,35 @@ function ProjectsPage({ roster, actions, run, call: call4 }) {
onChange: (event) => setName(event.target.value)
})
),
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
SetRow,
{ label: t("Working folder"), hint: t("Optional. Where this Project's work happens.") },
- (0, import_react40.createElement)(FolderMenu, { value: workDir, actions, run, onPick: setWorkDir })
+ (0, import_react41.createElement)(FolderMenu, { value: workDir, actions, run, onPick: setWorkDir })
),
- (0, import_react40.createElement)(
+ (0, import_react41.createElement)(
"div",
{ className: "bt-actions" },
- (0, import_react40.createElement)("button", {
+ (0, import_react41.createElement)("button", {
type: "button",
className: "bt-primary bt-with-icon",
disabled: name.trim() === "",
onClick: () => {
void create();
}
- }, (0, import_react40.createElement)(PlusIcon), t("New Project"))
+ }, (0, import_react41.createElement)(PlusIcon), t("New Project"))
)
),
// The list is rendered here rather than as a child element: it holds no state of its own,
// and the page keeps one reload path for both halves.
- ...ProjectsList({ projects: state.projects, roster, call: call4, reload })
+ ...ProjectsList({ projects: state.projects, roster, call: call5, reload })
];
}
// src/client/context-menu.js
-var import_react41 = require("react");
+var import_react42 = require("react");
function ContextMenu({ menu, roster, actions, leaving = false }) {
useEscape(() => actions.closeMenu());
- (0, import_react41.useEffect)(() => {
+ (0, import_react42.useEffect)(() => {
const onDown = (event) => {
if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu();
};
@@ -17596,14 +17815,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
return () => window.removeEventListener("mousedown", onDown);
}, []);
const structure = useSidebarStructure();
- const ref = (0, import_react41.useRef)(null);
- const [place, setPlace] = (0, import_react41.useState)({ left: menu.x, top: menu.y });
- (0, import_react41.useLayoutEffect)(() => {
+ const ref = (0, import_react42.useRef)(null);
+ const [place, setPlace] = (0, import_react42.useState)({ left: menu.x, top: menu.y });
+ (0, import_react42.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_react41.createElement)(AgentMenu, { menu, roster, actions, leaving });
+ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react42.createElement)(AgentMenu, { menu, roster, actions, leaving });
const bot = roster.byId[menu.id];
const room2 = roster.roomsById[menu.id];
const entry = bot ?? room2;
@@ -17645,14 +17864,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
]
];
const shown = groups.map((group) => group.filter(Boolean)).filter((group) => group.length > 0);
- return (0, import_react41.createElement)(
+ return (0, import_react42.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_react41.createElement)(
- import_react41.Fragment,
+ shown.map((group, index) => (0, import_react42.createElement)(
+ import_react42.Fragment,
{ key: index },
- index > 0 ? (0, import_react41.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
- group.map(([label, Icon, run, danger]) => (0, import_react41.createElement)("button", {
+ index > 0 ? (0, import_react42.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
+ group.map(([label, Icon, run, danger]) => (0, import_react42.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
@@ -17661,14 +17880,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
actions.closeMenu();
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react41.createElement)(Icon), label))
+ }, (0, import_react42.createElement)(Icon), label))
))
);
}
function AgentMenu({ menu, roster, actions, leaving }) {
- const ref = (0, import_react41.useRef)(null);
- const [place, setPlace] = (0, import_react41.useState)({ left: menu.x, top: menu.y });
- (0, import_react41.useLayoutEffect)(() => {
+ const ref = (0, import_react42.useRef)(null);
+ const [place, setPlace] = (0, import_react42.useState)({ left: menu.x, top: menu.y });
+ (0, import_react42.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)) });
@@ -17690,14 +17909,14 @@ function AgentMenu({ menu, roster, actions, leaving }) {
[t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true]
]
];
- return (0, import_react41.createElement)(
+ return (0, import_react42.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_react41.createElement)(
- import_react41.Fragment,
+ groups.map((group, index) => (0, import_react42.createElement)(
+ import_react42.Fragment,
{ key: index },
- index > 0 ? (0, import_react41.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
- group.map(([label, Icon, run, danger]) => (0, import_react41.createElement)("button", {
+ index > 0 ? (0, import_react42.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
+ group.map(([label, Icon, run, danger]) => (0, import_react42.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
@@ -17706,7 +17925,7 @@ function AgentMenu({ menu, roster, actions, leaving }) {
actions.closeMenu();
void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error));
}
- }, (0, import_react41.createElement)(Icon), label))
+ }, (0, import_react42.createElement)(Icon), label))
))
);
}
@@ -17792,7 +18011,7 @@ function migrateStorage(storage = globalThis.localStorage) {
}
// src/client/details-drawer.js
-var import_react42 = require("react");
+var import_react43 = require("react");
var TAB_LABELS = { details: "Details", browser: "Browser" };
var hostOf = (url) => {
try {
@@ -17806,10 +18025,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_react42.useState)(null);
+ const [pending, setPending] = (0, import_react43.useState)(null);
const catalog = roster.permissions;
const saved = bot.permission ?? catalog?.defaultPreset;
- (0, import_react42.useEffect)(() => {
+ (0, import_react43.useEffect)(() => {
if (pending === saved) setPending(null);
}, [pending, saved]);
if (!catalog?.options?.length) return null;
@@ -17819,14 +18038,14 @@ function PermissionRow({ bot, roster, actions, run }) {
setPending(value);
void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null));
};
- return (0, import_react42.createElement)(
+ return (0, import_react43.createElement)(
"div",
{ className: "bt-drawer-permission" },
- (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
+ (0, import_react43.createElement)(
Segmented,
{ label: t("Permissions"), value: current },
- catalog.options.map((option) => (0, import_react42.createElement)("button", {
+ catalog.options.map((option) => (0, import_react43.createElement)("button", {
key: option.value,
type: "button",
"aria-pressed": option.value === current,
@@ -17840,42 +18059,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_react42.useEffect)(() => {
+ (0, import_react43.useEffect)(() => {
actions.browserEnsure?.(bot.id);
}, [bot.id]);
if (roster.browser !== true || !actions.browserShow) {
- return (0, import_react42.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app."));
+ return (0, import_react43.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app."));
}
if (state.ids.length === 0) {
- return (0, import_react42.createElement)(
+ return (0, import_react43.createElement)(
"div",
{ className: "bt-browser-empty-mark" },
- (0, import_react42.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
- (0, import_react42.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
- (0, import_react42.createElement)("div", { className: "bt-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_react42.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
+ (0, import_react43.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
+ (0, import_react43.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
+ (0, import_react43.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_react43.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
);
}
- return (0, import_react42.createElement)(
+ return (0, import_react43.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_react42.createElement)(
+ return (0, import_react43.createElement)(
"div",
{ key: tabId, className: "bt-bcard", "data-active": state.active === tabId || void 0, style: { animationDelay: `${index * 30}ms` } },
- (0, import_react42.createElement)(
+ (0, import_react43.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_react42.createElement)(
+ page.excerpt ? (0, import_react43.createElement)(
"span",
{ className: "bt-bcard-preview" },
- (0, import_react42.createElement)("span", { className: "bt-bcard-ptitle" }, label),
- (0, import_react42.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
- ) : (0, import_react42.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react42.createElement)(GlobeIcon), (0, import_react42.createElement)("span", null, label))
+ (0, import_react43.createElement)("span", { className: "bt-bcard-ptitle" }, label),
+ (0, import_react43.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
+ ) : (0, import_react43.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react43.createElement)(GlobeIcon), (0, import_react43.createElement)("span", null, label))
),
- (0, import_react42.createElement)("button", {
+ (0, import_react43.createElement)("button", {
type: "button",
className: "bt-bcard-x",
"aria-label": t("Close tab"),
@@ -17884,36 +18103,36 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow
void actions.browserCloseTab(bot.id, tabId);
}
}, "×"),
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(
"div",
{ className: "bt-bcard-row" },
- (0, import_react42.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
- (0, import_react42.createElement)("span", { className: "bt-bcard-title" }, label),
- page.url ? (0, import_react42.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null
+ (0, import_react43.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
+ (0, import_react43.createElement)("span", { className: "bt-bcard-title" }, label),
+ page.url ? (0, import_react43.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null
)
);
}),
- (0, import_react42.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react42.createElement)(PlusIcon), t("New tab"))
+ (0, import_react43.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react43.createElement)(PlusIcon), t("New tab"))
);
}
function About({ text: text2, title = "About" }) {
- const [open, setOpen] = (0, import_react42.useState)(false);
+ const [open, setOpen] = (0, import_react43.useState)(false);
const long = text2.split("\n").length > 5 || text2.length > 240;
- const body = (0, import_react42.useRef)(null);
+ const body = (0, import_react43.useRef)(null);
useMorph(body, open);
- return (0, import_react42.createElement)(
+ return (0, import_react43.createElement)(
"div",
null,
- (0, import_react42.createElement)("div", { className: "bt-section-title" }, t(title)),
- (0, import_react42.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2),
- long ? (0, import_react42.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null
+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t(title)),
+ (0, import_react43.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2),
+ long ? (0, import_react43.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_react42.useState)(room2.pendingApprovals ?? []);
- const [busy, setBusy] = (0, import_react42.useState)("");
+ const [items, setItems] = (0, import_react43.useState)(room2.pendingApprovals ?? []);
+ const [busy, setBusy] = (0, import_react43.useState)("");
const face = room2.origin === "face-to-face";
- (0, import_react42.useEffect)(() => {
+ (0, import_react43.useEffect)(() => {
const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length;
if (haveItems) setItems(room2.pendingApprovals);
if (room2.viewerRole !== "invited-owner" && !face) return void 0;
@@ -17939,28 +18158,28 @@ function SharedRoomApprovals({ room: room2 }) {
setBusy("");
}
};
- return (0, import_react42.createElement)(
+ return (0, import_react43.createElement)(
"div",
{ key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" },
- (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react42.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))),
- items.map((item) => (0, import_react42.createElement)(
+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react43.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))),
+ items.map((item) => (0, import_react43.createElement)(
"div",
{ key: item.id, className: "bt-set-row" },
- (0, import_react42.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")),
- (0, import_react42.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")),
- (0, import_react42.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject"))
+ (0, import_react43.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")),
+ (0, import_react43.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")),
+ (0, import_react43.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_react42.createElement)(
+ return (0, import_react43.createElement)(
"div",
null,
- (0, import_react42.createElement)(FaceLead, { room: room2, roster, pick: pick2 }),
- (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })),
- (0, import_react42.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react42.createElement)(FacePersonRow, {
+ (0, import_react43.createElement)(FaceLead, { room: room2, roster, pick: pick2 }),
+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })),
+ (0, import_react43.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react43.createElement)(FacePersonRow, {
key: person.key,
person,
roster,
@@ -17984,79 +18203,79 @@ function GroupOverview({ room: room2, roster, actions, usage, useWork, openSetti
};
const face = room2.origin === "face-to-face";
return [
- (0, import_react42.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
- invited && room2.sharedFrom && !face ? (0, import_react42.createElement)(
+ (0, import_react43.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
+ invited && room2.sharedFrom && !face ? (0, import_react43.createElement)(
"div",
{ key: "from", className: "bt-shared-from" },
- (0, import_react42.createElement)("div", { className: "bt-section-title" }, title),
- (0, import_react42.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_react42.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null,
- (0, import_react42.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot"))
- ) : face ? null : room2.notice ? (0, import_react42.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react42.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
- face ? null : (0, import_react42.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
- face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react42.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react42.createElement)(
+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, title),
+ (0, import_react43.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_react43.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null,
+ (0, import_react43.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot"))
+ ) : face ? null : room2.notice ? (0, import_react43.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react43.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
+ face ? null : (0, import_react43.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
+ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react43.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react43.createElement)(
"div",
{ key: "members" },
- (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })),
- (0, import_react42.createElement)("div", { className: "bt-members", role: "list" }, [
- ...humans.map((person) => (0, import_react42.createElement)(
+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })),
+ (0, import_react43.createElement)("div", { className: "bt-members", role: "list" }, [
+ ...humans.map((person) => (0, import_react43.createElement)(
"div",
{ key: person.id, role: "listitem" },
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react42.createElement)("span", { className: "bt-member-name" }, (0, import_react42.createElement)("span", null, person.displayName || person.id)),
- (0, import_react42.createElement)("span", { className: "bt-chip" }, t("Human")),
- person.role === "admin" ? (0, import_react42.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
+ (0, import_react43.createElement)("span", { className: "bt-member-name" }, (0, import_react43.createElement)("span", null, person.displayName || person.id)),
+ (0, import_react43.createElement)("span", { className: "bt-chip" }, t("Human")),
+ person.role === "admin" ? (0, import_react43.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
)
)),
- ...remotes.map((person) => (0, import_react42.createElement)(
+ ...remotes.map((person) => (0, import_react43.createElement)(
"div",
{ key: person.handle || person.id, role: "listitem" },
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(
"span",
{ className: "bt-member-name" },
- (0, import_react42.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
- (0, import_react42.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person))
+ (0, import_react43.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
+ (0, import_react43.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person))
),
- face ? null : (0, import_react42.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
+ face ? null : (0, import_react43.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
)
)),
...members.map((member) => {
const entry = entries2.find((item) => item.id === member.id);
- return (0, import_react42.createElement)(
+ return (0, import_react43.createElement)(
"div",
{ key: member.id, role: "listitem" },
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(
"button",
{ type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) },
- (0, import_react42.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
- (0, import_react42.createElement)("span", null, member.name)
+ (0, import_react43.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
+ (0, import_react43.createElement)("span", null, member.name)
),
- room2.admin === member.id ? (0, import_react42.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
+ room2.admin === member.id ? (0, import_react43.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
),
- entry ? (0, import_react42.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
+ entry ? (0, import_react43.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
);
})
])
),
- face ? null : (0, import_react42.createElement)(
+ face ? null : (0, import_react43.createElement)(
"div",
{ key: "mode" },
- (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
- (0, import_react42.createElement)("div", { className: "bt-kv" }, (0, import_react42.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone")))
+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
+ (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone")))
),
- face || !usage ? null : (0, import_react42.createElement)(
+ face || !usage ? null : (0, import_react43.createElement)(
"div",
{ key: "usage" },
- (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Usage")),
- (0, import_react42.createElement)("div", { className: "bt-kv" }, (0, import_react42.createElement)("span", null, room2.name), (0, import_react42.createElement)("span", null, usage))
+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Usage")),
+ (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, room2.name), (0, import_react43.createElement)("span", null, usage))
)
];
}
@@ -18064,15 +18283,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_react42.useState)("details");
+ const [tab, setTab] = (0, import_react43.useState)("details");
const room2 = roster.roomsById[sessionId];
- const [view, setView] = (0, import_react42.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
- const [editing, setEditing] = (0, import_react42.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
- const [draftName, setDraftName] = (0, import_react42.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
- const [copied, setCopied] = (0, import_react42.useState)(false);
- const [error, setError] = (0, import_react42.useState)("");
+ const [view, setView] = (0, import_react43.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
+ const [editing, setEditing] = (0, import_react43.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
+ const [draftName, setDraftName] = (0, import_react43.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
+ const [copied, setCopied] = (0, import_react43.useState)(false);
+ const [error, setError] = (0, import_react43.useState)("");
const glide = useGlide(tab);
- const last = (0, import_react42.useRef)({ view, tab, move: void 0 });
+ const last = (0, import_react43.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;
@@ -18107,37 +18326,37 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
actions.notify?.(t("Conversation copied as Markdown"));
});
};
- const errorLine = error ? (0, import_react42.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
+ const errorLine = error ? (0, import_react43.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
if ((bot || room2) && view === "settings" && !bot?.remote) {
return [
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(
"div",
{ key: "head", className: "bt-subhead", "data-move": move },
- (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react42.createElement)(ChevronLeftIcon)),
- (0, import_react42.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
- hasStrip ? null : (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react42.createElement)(CloseIcon))
+ (0, import_react43.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react43.createElement)(ChevronLeftIcon)),
+ (0, import_react43.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
+ hasStrip ? null : (0, import_react43.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react43.createElement)(CloseIcon))
),
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(
"div",
{ key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move },
errorLine,
- bot ? (0, import_react42.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react42.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
+ bot ? (0, import_react43.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react43.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
)
];
}
- const top = (0, import_react42.createElement)(
+ const top = (0, import_react43.createElement)(
"div",
{ className: "bt-drawer-top", "data-move": side },
- bot || room2 ? (0, import_react42.createElement)("button", {
+ bot || room2 ? (0, import_react43.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_react42.createElement)(GearIcon)) : null,
- bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react42.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_react42.createElement)(GlobeIcon)) : null,
- bot || room2 ? (0, import_react42.createElement)("button", {
+ }, (0, import_react43.createElement)(GearIcon)) : null,
+ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react43.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_react43.createElement)(GlobeIcon)) : null,
+ bot || room2 ? (0, import_react43.createElement)("button", {
type: "button",
className: "bt-round",
// #1347: the accessible name says what the click does, which is copy — "Share conversation"
@@ -18146,13 +18365,13 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
"aria-label": copied ? t("Copied conversation") : t("Copy conversation"),
title: copied ? t("Copied") : t("Copy conversation as Markdown"),
onClick: share
- }, copied ? (0, import_react42.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react42.createElement)(ShareIcon)) : null,
- hasStrip ? null : (0, import_react42.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react42.createElement)(CloseIcon))
+ }, copied ? (0, import_react43.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react43.createElement)(ShareIcon)) : null,
+ hasStrip ? null : (0, import_react43.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react43.createElement)(CloseIcon))
);
if (!bot && !room2) {
return [
- (0, import_react42.createElement)(import_react42.Fragment, { key: "top" }, top),
- (0, import_react42.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react42.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team.")))
+ (0, import_react43.createElement)(import_react43.Fragment, { key: "top" }, top),
+ (0, import_react43.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react43.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team.")))
];
}
const current = bot?.name ?? room2.name;
@@ -18166,12 +18385,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_react42.createElement)(import_react42.Fragment, { key: "top" }, top),
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(import_react43.Fragment, { key: "top" }, top),
+ (0, import_react43.createElement)(
"div",
{ key: "id", className: "bt-drawer-id", "data-move": side },
- bot ? (0, import_react42.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react42.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
- bot?.remote ? (0, import_react42.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react42.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react42.createElement)("input", {
+ bot ? (0, import_react43.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react43.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
+ bot?.remote ? (0, import_react43.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react43.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react43.createElement)("input", {
className: "bt-drawer-name-input",
autoFocus: true,
value: draftName,
@@ -18186,60 +18405,60 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
setEditing(false);
}
}
- }) : (0, import_react42.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => {
+ }) : (0, import_react43.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_react42.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react42.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null
+ subtitle ? (0, import_react43.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react43.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null
),
- bot?.remote ? null : (0, import_react42.createElement)(
+ bot?.remote ? null : (0, import_react43.createElement)(
"div",
{ key: "tabs", ref: glide.box, className: "bt-tabs", role: "tablist", "data-move": side },
- (0, import_react42.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
- tabs.map((key) => (0, import_react42.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key])))
+ (0, import_react43.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
+ tabs.map((key) => (0, import_react43.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key])))
),
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(
"div",
{ key: `body:${tab}`, className: "bt-drawer-body", "data-move": move },
errorLine,
- room2 && tab === "details" ? (0, import_react42.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
+ room2 && tab === "details" ? (0, import_react43.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
bot && bot.remote && tab === "details" ? [
- bot.remote.handle ? (0, import_react42.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null,
- (0, import_react42.createElement)(FlyBotDetails, { key: "social", bot, roster, actions })
+ bot.remote.handle ? (0, import_react43.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null,
+ (0, import_react43.createElement)(FlyBotDetails, { key: "social", bot, roster, actions })
] : null,
bot && !bot.remote && tab === "details" ? [
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(
"div",
{ key: "model", className: "bt-drawer-model" },
- (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Model")),
- (0, import_react42.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
- modelOf(bot, roster) ? null : (0, import_react42.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name }))
+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Model")),
+ (0, import_react43.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
+ modelOf(bot, roster) ? null : (0, import_react43.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name }))
),
- (0, import_react42.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
- (0, import_react42.createElement)(
+ (0, import_react43.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
+ (0, import_react43.createElement)(
"div",
{ key: "folder", className: "bt-drawer-model" },
- (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Working folder")),
- (0, import_react42.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) })
+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Working folder")),
+ (0, import_react43.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) })
),
- bot.instructions ? (0, import_react42.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react42.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
- roster.memory === false ? null : (0, import_react42.createElement)(MemoryRow, { key: "memory", bot, actions }),
- usage ? (0, import_react42.createElement)(
+ bot.instructions ? (0, import_react43.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react43.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
+ roster.memory === false ? null : (0, import_react43.createElement)(MemoryRow, { key: "memory", bot, actions }),
+ usage ? (0, import_react43.createElement)(
"div",
{ key: "usage" },
- (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Usage")),
- (0, import_react42.createElement)("div", { className: "bt-kv" }, (0, import_react42.createElement)("span", null, bot.name), (0, import_react42.createElement)("span", null, usage))
+ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Usage")),
+ (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, bot.name), (0, import_react43.createElement)("span", null, usage))
) : null,
- (0, import_react42.createElement)(ScheduleSummary, { key: "routines", bot, actions })
+ (0, import_react43.createElement)(ScheduleSummary, { key: "routines", bot, actions })
] : null,
- bot && !bot.remote && tab === "browser" ? (0, import_react42.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
+ bot && !bot.remote && tab === "browser" ? (0, import_react43.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
)
];
}
// src/client/browser-pane.js
-var import_react43 = require("react");
-var import_react_dom9 = require("react-dom");
+var import_react44 = require("react");
+var import_react_dom10 = require("react-dom");
var PANEL_LINGER_MS = 300;
var WIDTH_KEY = STORAGE.browserWidth;
var READ_FLASH_MS = 2400;
@@ -18248,10 +18467,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_react43.createElement)(
+ return (0, import_react44.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_react43.createElement)("path", { key: index, d }))
+ paths.map((d, index) => (0, import_react44.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"]);
@@ -18263,9 +18482,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_react43.useState)(null);
- if (!src || failed === src) return (0, import_react43.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react43.createElement)(GlobeIcon));
- return (0, import_react43.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
+ const [failed, setFailed] = (0, import_react44.useState)(null);
+ if (!src || failed === src) return (0, import_react44.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react44.createElement)(GlobeIcon));
+ return (0, import_react44.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
}
var savedWidth = () => {
try {
@@ -18324,11 +18543,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_react43.useRef)(null);
- const ind = (0, import_react43.useRef)(null);
- const spot = (0, import_react43.useRef)(null);
- const under = (0, import_react43.useRef)(null);
- (0, import_react43.useLayoutEffect)(() => {
+ const wrap = (0, import_react44.useRef)(null);
+ const ind = (0, import_react44.useRef)(null);
+ const spot = (0, import_react44.useRef)(null);
+ const under = (0, import_react44.useRef)(null);
+ (0, import_react44.useLayoutEffect)(() => {
const box = wrap.current;
const capsule = ind.current;
if (!box || !capsule) return void 0;
@@ -18357,7 +18576,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
trail.cancel();
};
});
- (0, import_react43.useEffect)(() => {
+ (0, import_react44.useEffect)(() => {
const box = wrap.current;
if (!box) return void 0;
const observer = new ResizeObserver(() => {
@@ -18371,13 +18590,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
observer.observe(box);
return () => observer.disconnect();
}, []);
- return (0, import_react43.createElement)(
+ return (0, import_react44.createElement)(
"div",
{ className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") },
- (0, import_react43.createElement)(
+ (0, import_react44.createElement)(
"div",
{ className: "bt-ptabs-in", ref: wrap },
- (0, import_react43.createElement)(
+ (0, import_react44.createElement)(
"button",
{
type: "button",
@@ -18387,13 +18606,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
title: bot.name,
onClick: () => onView("details")
},
- (0, import_react43.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
- view === "details" ? (0, import_react43.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null
+ (0, import_react44.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
+ view === "details" ? (0, import_react44.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_react43.createElement)(
+ return (0, import_react44.createElement)(
"button",
{
key: tabId,
@@ -18414,9 +18633,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
}
}
},
- (0, import_react43.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
- (0, import_react43.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
- (0, import_react43.createElement)("span", {
+ (0, import_react44.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
+ (0, import_react44.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
+ (0, import_react44.createElement)("span", {
className: "bt-ptab-x",
role: "button",
"aria-label": t("Close tab"),
@@ -18428,20 +18647,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
}, "×")
);
}),
- (0, import_react43.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true })
+ (0, import_react44.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_react43.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react43.createElement)(PlusTabIcon)),
- (0, import_react43.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_react43.createElement)(CloseIcon2))
+ (0, import_react44.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react44.createElement)(PlusTabIcon)),
+ (0, import_react44.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_react44.createElement)(CloseIcon2))
);
}
function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) {
const page = useBrowserPages((pages) => pages[tabId ?? ""]) ?? EMPTY_PAGE;
- const frame = (0, import_react43.useRef)(null);
- const input = (0, import_react43.useRef)(null);
- const [draft, setDraft] = (0, import_react43.useState)(null);
+ const frame = (0, import_react44.useRef)(null);
+ const input = (0, import_react44.useRef)(null);
+ const [draft, setDraft] = (0, import_react44.useState)(null);
const showGuest = landed && !leaving && Boolean(page.url) && !page.error;
- (0, import_react43.useLayoutEffect)(() => {
+ (0, import_react44.useLayoutEffect)(() => {
const element = frame.current;
if (!element || leaving || !tabId) {
void actions.browserShow(null);
@@ -18467,7 +18686,7 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
window.removeEventListener("resize", schedule);
};
}, [tabId, showGuest, leaving]);
- (0, import_react43.useEffect)(() => {
+ (0, import_react44.useEffect)(() => {
setDraft(null);
if (landed && !page.url) input.current?.focus();
}, [tabId, landed]);
@@ -18483,27 +18702,27 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
};
const editing = draft !== null;
const secure = /^https:/i.test(page.url);
- return (0, import_react43.createElement)(
+ return (0, import_react44.createElement)(
"div",
{ className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) },
- (0, import_react43.createElement)(
+ (0, import_react44.createElement)(
"div",
{ className: "bt-browser-bar" },
- (0, import_react43.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_react43.createElement)(BackIcon)),
- (0, import_react43.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_react43.createElement)(ForwardIcon)),
- (0, import_react43.createElement)("button", {
+ (0, import_react44.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_react44.createElement)(BackIcon)),
+ (0, import_react44.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_react44.createElement)(ForwardIcon)),
+ (0, import_react44.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_react43.createElement)(page.loading ? StopIcon : ReloadIcon)),
- (0, import_react43.createElement)(
+ }, (0, import_react44.createElement)(page.loading ? StopIcon : ReloadIcon)),
+ (0, import_react44.createElement)(
"form",
{ className: "bt-browser-address", onSubmit: go, "data-editing": editing || void 0 },
- (0, import_react43.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react43.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
- (0, import_react43.createElement)("input", {
+ (0, import_react44.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react44.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
+ (0, import_react44.createElement)("input", {
ref: input,
type: "text",
spellCheck: false,
@@ -18527,25 +18746,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
}
})
),
- page.loading ? (0, import_react43.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
+ page.loading ? (0, import_react44.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
),
- (0, import_react43.createElement)(
+ (0, import_react44.createElement)(
"div",
{ ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 },
- page.error ? (0, import_react43.createElement)(
+ page.error ? (0, import_react44.createElement)(
"div",
{ className: "bt-browser-empty" },
- (0, import_react43.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
- (0, import_react43.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
- page.url ? (0, import_react43.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => {
+ (0, import_react44.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
+ (0, import_react44.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
+ page.url ? (0, import_react44.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => {
void actions.browserNavigate(tabId, page.url);
} }, t("Try again")) : null
- ) : !page.url ? (0, import_react43.createElement)(
+ ) : !page.url ? (0, import_react44.createElement)(
"div",
{ className: "bt-browser-empty" },
- (0, import_react43.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }),
- (0, import_react43.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
- (0, import_react43.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_react44.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }),
+ (0, import_react44.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
+ (0, import_react44.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
)
);
@@ -18557,41 +18776,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_react43.useRef)(null);
- const fly = (0, import_react43.useRef)(null);
- const [landed, setLanded] = (0, import_react43.useState)(false);
- const [settling, setSettling] = (0, import_react43.useState)(false);
- const [width, setWidth] = (0, import_react43.useState)(savedWidth);
- const [viewport, setViewport] = (0, import_react43.useState)(() => window.innerWidth);
- const [dragging, setDragging] = (0, import_react43.useState)(false);
- const [resizing, setResizing] = (0, import_react43.useState)(false);
+ const root = (0, import_react44.useRef)(null);
+ const fly = (0, import_react44.useRef)(null);
+ const [landed, setLanded] = (0, import_react44.useState)(false);
+ const [settling, setSettling] = (0, import_react44.useState)(false);
+ const [width, setWidth] = (0, import_react44.useState)(savedWidth);
+ const [viewport, setViewport] = (0, import_react44.useState)(() => window.innerWidth);
+ const [dragging, setDragging] = (0, import_react44.useState)(false);
+ const [resizing, setResizing] = (0, import_react44.useState)(false);
const instant = dragging || resizing;
- const [reading, setReading] = (0, import_react43.useState)(false);
+ const [reading, setReading] = (0, import_react44.useState)(false);
const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport));
- (0, import_react43.useLayoutEffect)(() => {
+ (0, import_react44.useLayoutEffect)(() => {
if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH);
}, [view, width]);
- (0, import_react43.useLayoutEffect)(() => {
+ (0, import_react44.useLayoutEffect)(() => {
chatRoom.hold(instant);
}, [instant]);
- const [last, setLast] = (0, import_react43.useState)({ view, width: panelWidth });
+ const [last, setLast] = (0, import_react44.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_react43.useRef)(view);
- (0, import_react43.useLayoutEffect)(() => {
+ const shownView = (0, import_react44.useRef)(view);
+ (0, import_react44.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_react43.useEffect)(() => () => {
+ (0, import_react44.useEffect)(() => () => {
chatRoom.set(null);
chatRoom.hold(false);
}, []);
- (0, import_react43.useEffect)(() => {
+ (0, import_react44.useEffect)(() => {
let timer;
const onResize = () => {
setResizing(true);
@@ -18605,28 +18824,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
window.removeEventListener("resize", onResize);
};
}, []);
- (0, import_react43.useEffect)(() => {
+ (0, import_react44.useEffect)(() => {
if (!settling) return void 0;
const timer = setTimeout(() => setSettling(false), SPRING_SHAPE.duration + 40);
return () => clearTimeout(timer);
}, [settling, view]);
- (0, import_react43.useEffect)(() => {
+ (0, import_react44.useEffect)(() => {
const timer = setTimeout(() => setLanded(true), 360);
return () => clearTimeout(timer);
}, []);
- (0, import_react43.useEffect)(() => {
+ (0, import_react44.useEffect)(() => {
if (canBrowse) actions.browserEnsure(bot.id);
}, [bot?.id, canBrowse]);
- (0, import_react43.useEffect)(() => {
+ (0, import_react44.useEffect)(() => {
if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details");
}, [view, active, tabState.ids.length]);
- (0, import_react43.useEffect)(() => {
+ (0, import_react44.useEffect)(() => {
if (view !== "browser" || leaving) void actions.browserShow?.(null);
}, [view, leaving]);
- (0, import_react43.useEffect)(() => () => {
+ (0, import_react44.useEffect)(() => () => {
void actions.browserShow?.(null);
}, []);
- (0, import_react43.useEffect)(() => {
+ (0, import_react44.useEffect)(() => {
if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0;
setReading(true);
const timer = setTimeout(() => setReading(false), READ_FLASH_MS);
@@ -18663,7 +18882,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
return actions[name](...args);
};
}
- (0, import_react43.useLayoutEffect)(() => {
+ (0, import_react44.useLayoutEffect)(() => {
const move = fly.current;
if (!move) return;
fly.current = null;
@@ -18746,7 +18965,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_react43.createElement)(
+ return (0, import_react44.createElement)(
"aside",
{
ref: root,
@@ -18763,15 +18982,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
if (event.target === event.currentTarget) setLanded(true);
}
},
- leaving ? null : (0, import_react43.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }),
+ leaving ? null : (0, import_react44.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_dom9.createPortal)((0, import_react43.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
- hasStrip ? (0, import_react43.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
- view === "browser" && bot ? (0, import_react43.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react43.createElement)(
+ dragging ? (0, import_react_dom10.createPortal)((0, import_react44.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
+ hasStrip ? (0, import_react44.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
+ view === "browser" && bot ? (0, import_react44.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react44.createElement)(
"div",
{ key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" },
- (0, import_react43.createElement)(DetailsDrawer, {
+ (0, import_react44.createElement)(DetailsDrawer, {
sessionId: pane.id,
startRenaming: pane.renaming,
startTab: pane.tab,
@@ -18868,7 +19087,7 @@ var BROWSER_CSS = `
`;
// src/client/remote-bot-settings.js
-var import_react44 = require("react");
+var import_react45 = require("react");
var REMOTE_BOT_SETTINGS_CSS = `
.bt-remote-sheet-layer{position:fixed;inset:0;z-index:120;isolation:isolate;display:flex;align-items:flex-end;justify-content:center;padding:16px;pointer-events:none}
.bt-remote-sheet-layer[data-leaving]{pointer-events:none}
@@ -18892,8 +19111,8 @@ var REMOTE_BOT_SETTINGS_CSS = `
function RemoteBotSettingsSheet({ bot, roster, actions, leaving = false }) {
const close = () => actions.closeSettings();
useEscape(close);
- const dialog = (0, import_react44.useRef)(null);
- (0, import_react44.useEffect)(() => {
+ const dialog = (0, import_react45.useRef)(null);
+ (0, import_react45.useEffect)(() => {
const before = document.activeElement;
dialog.current?.focus?.({ preventScroll: true });
return () => {
@@ -18908,38 +19127,38 @@ 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_react44.createElement)(
+ return (0, import_react45.createElement)(
"div",
{ className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" },
- (0, import_react44.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
- (0, import_react44.createElement)(
+ (0, import_react45.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
+ (0, import_react45.createElement)(
"div",
{ className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown },
- (0, import_react44.createElement)(
+ (0, import_react45.createElement)(
"div",
{ className: "bt-remote-sheet-head" },
- (0, import_react44.createElement)(BotMark, { bot, size: 40, live: true }),
- (0, import_react44.createElement)(
+ (0, import_react45.createElement)(BotMark, { bot, size: 40, live: true }),
+ (0, import_react45.createElement)(
"div",
{ className: "bt-remote-sheet-who" },
- (0, import_react44.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name),
- (0, import_react44.createElement)("span", { className: "bt-remote-sheet-sub" }, who)
+ (0, import_react45.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name),
+ (0, import_react45.createElement)("span", { className: "bt-remote-sheet-sub" }, who)
),
- (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react44.createElement)(CloseIcon))
+ (0, import_react45.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react45.createElement)(CloseIcon))
),
- (0, import_react44.createElement)(
+ (0, import_react45.createElement)(
"div",
{ className: "bt-remote-sheet-body" },
- bot.remote.handle ? (0, import_react44.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null,
- (0, import_react44.createElement)(FlyBotDetails, { bot, roster, actions })
+ bot.remote.handle ? (0, import_react45.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null,
+ (0, import_react45.createElement)(FlyBotDetails, { bot, roster, actions })
)
)
);
}
// src/client/overlays.js
-var import_react45 = require("react");
-var import_react_dom10 = require("react-dom");
+var import_react46 = require("react");
+var import_react_dom11 = require("react-dom");
var DIALOG_EXIT_MS = 160;
var MENU_EXIT_MS3 = 120;
function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useWork, useRegistry, useBrowserPages, useBrowserTabs, useUpdate, actions }) {
@@ -18947,15 +19166,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_react45.useRef)(current);
- (0, import_react45.useEffect)(() => {
+ const previous = (0, import_react46.useRef)(current);
+ (0, import_react46.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_react45.useState)(open);
- (0, import_react45.useEffect)(() => {
+ const [kept, setKept] = (0, import_react46.useState)(open);
+ (0, import_react46.useEffect)(() => {
if (open) {
setKept(open);
return void 0;
@@ -18971,20 +19190,20 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity
const [menu, menuLeaving] = useLinger(ui2.menu, MENU_EXIT_MS3);
const [inbox, inboxLeaving] = useLinger(ui2.inbox ?? null, DIALOG_EXIT_MS);
const [board, boardLeaving] = useLinger(ui2.board ?? null, DIALOG_EXIT_MS);
- return (0, import_react45.createElement)(
- import_react45.Fragment,
+ return (0, import_react46.createElement)(
+ import_react46.Fragment,
null,
- pane ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null,
- ui2.newChat ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null,
- ui2.palette ? (0, import_react_dom10.createPortal)((0, import_react45.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_dom10.createPortal)((0, import_react45.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null,
- ui2.voice ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
- menu ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null,
- settings ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
- remoteBotSettings ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null,
- memory && roster.byId[memory] ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
- inbox ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null,
- board ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null
+ pane ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null,
+ ui2.newChat ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null,
+ ui2.palette ? (0, import_react_dom11.createPortal)((0, import_react46.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_dom11.createPortal)((0, import_react46.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null,
+ ui2.voice ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
+ menu ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null,
+ settings ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
+ remoteBotSettings ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null,
+ memory && roster.byId[memory] ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
+ inbox ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null,
+ board ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null
);
}
@@ -19871,7 +20090,7 @@ function watchShellLevel(doc) {
}
// src/client/project-board.js
-var import_react46 = require("react");
+var import_react47 = require("react");
var PROJECT_BOARD_CSS = `
/* #1750: the board was a short dialog in the page flow — no stylesheet defines 'bt-overlay', so the
bot tree and workflows stayed legible under its rows. It is a modal now, on the same tier as the
@@ -20044,14 +20263,14 @@ function projectBoardView({ project, rows, canManage, tasks, decisions, meetings
};
}
function ProjectBoardRows({ view, onOpen }) {
- if (view.empty) return (0, import_react46.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText));
- return view.groups.map((group) => (0, import_react46.createElement)(
+ if (view.empty) return (0, import_react47.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText));
+ return view.groups.map((group) => (0, import_react47.createElement)(
"section",
{ key: group.source, className: "bt-project-group", "data-source": group.source },
- (0, import_react46.createElement)("h4", null, t(group.label)),
+ (0, import_react47.createElement)("h4", null, t(group.label)),
group.rows.map((row) => {
const open = row.board === null ? void 0 : () => onOpen?.(row.board);
- return (0, import_react46.createElement)(
+ return (0, import_react47.createElement)(
"div",
{
key: row.key,
@@ -20066,17 +20285,17 @@ function ProjectBoardRows({ view, onOpen }) {
if (event.key === "Enter" || event.key === " ") open();
}
},
- (0, import_react46.createElement)("span", { className: "bt-project-column" }, t(row.column)),
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)("span", { className: "bt-project-column" }, t(row.column)),
+ (0, import_react47.createElement)(
"span",
{ className: "bt-project-title-wrap" },
- (0, import_react46.createElement)("span", { className: "bt-project-title" }, row.title ?? (row.assignee ? `@${row.assignee}` : "—")),
+ (0, import_react47.createElement)("span", { className: "bt-project-title" }, row.title ?? (row.assignee ? `@${row.assignee}` : "—")),
// The audit text the row already carries (#1730): shown inside the card detail, never a
// second read. A row without one renders exactly as before.
- row.audit === null ? null : (0, import_react46.createElement)("span", { className: "bt-project-audit" }, row.audit)
+ row.audit === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-audit" }, row.audit)
),
- row.priority === null ? null : (0, import_react46.createElement)("span", { className: "bt-project-priority" }, row.priority),
- row.runId === null ? null : (0, import_react46.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`)
+ row.priority === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-priority" }, row.priority),
+ row.runId === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`)
);
})
));
@@ -20090,14 +20309,14 @@ function waitedLabel(ms) {
return `${Math.floor(hours / 24)}d`;
}
function ProjectQueueGroups({ view, onOpen }) {
- return view.queues.map((entry) => (0, import_react46.createElement)(
+ return view.queues.map((entry) => (0, import_react47.createElement)(
"section",
{ key: `queue:${entry.group}`, className: "bt-project-group", "data-queue": entry.group },
- (0, import_react46.createElement)("h4", null, t(entry.label)),
+ (0, import_react47.createElement)("h4", null, t(entry.label)),
entry.items.map((row) => {
const open = row.board === null ? void 0 : () => onOpen?.(row.board);
- const note = entry.group === "waiting" ? (0, import_react46.createElement)("span", { className: "bt-project-wait" }, [row.role === null ? null : (0, import_react46.createElement)("span", { key: "role", className: "bt-project-role" }, row.role), waitedLabel(row.waitedMs) === null ? null : ` · ${waitedLabel(row.waitedMs)}`]) : entry.group === "inflight" ? (0, import_react46.createElement)("span", { className: "bt-project-state" }, row.holder === null ? t(row.state) : `${row.holder} · ${t(row.state)}`) : (0, import_react46.createElement)("span", { className: "bt-project-reason" }, row.reason);
- return (0, import_react46.createElement)(
+ const note = entry.group === "waiting" ? (0, import_react47.createElement)("span", { className: "bt-project-wait" }, [row.role === null ? null : (0, import_react47.createElement)("span", { key: "role", className: "bt-project-role" }, row.role), waitedLabel(row.waitedMs) === null ? null : ` · ${waitedLabel(row.waitedMs)}`]) : entry.group === "inflight" ? (0, import_react47.createElement)("span", { className: "bt-project-state" }, row.holder === null ? t(row.state) : `${row.holder} · ${t(row.state)}`) : (0, import_react47.createElement)("span", { className: "bt-project-reason" }, row.reason);
+ return (0, import_react47.createElement)(
"div",
{
key: row.key,
@@ -20111,10 +20330,10 @@ function ProjectQueueGroups({ view, onOpen }) {
if (event.key === "Enter" || event.key === " ") open();
}
},
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)(
"span",
{ className: "bt-project-title-wrap" },
- (0, import_react46.createElement)("span", { className: "bt-project-title" }, row.title ?? `#${row.id}`)
+ (0, import_react47.createElement)("span", { className: "bt-project-title" }, row.title ?? `#${row.id}`)
),
note
);
@@ -20124,77 +20343,77 @@ function ProjectQueueGroups({ view, onOpen }) {
var countText = (value) => value === null ? t("unknown") : String(value);
function ProjectBoardRecord({ record }) {
const { decisions, meetings } = record;
- const decisionRows = decisions.files.map((file) => (0, import_react46.createElement)(
+ const decisionRows = decisions.files.map((file) => (0, import_react47.createElement)(
"div",
{ key: file.key, className: "bt-project-decision", "data-path": file.path, "data-sha256": file.sha256 },
- (0, import_react46.createElement)("span", { className: "bt-project-date" }, file.date),
- (0, import_react46.createElement)("span", { className: "bt-project-title" }, file.title === "" ? file.name : file.title),
- (0, import_react46.createElement)("span", { className: "bt-project-hash", title: file.sha256 }, file.shortHash)
+ (0, import_react47.createElement)("span", { className: "bt-project-date" }, file.date),
+ (0, import_react47.createElement)("span", { className: "bt-project-title" }, file.title === "" ? file.name : file.title),
+ (0, import_react47.createElement)("span", { className: "bt-project-hash", title: file.sha256 }, file.shortHash)
));
- const decidedRows = meetings.decided.map((meeting) => (0, import_react46.createElement)(
+ const decidedRows = meetings.decided.map((meeting) => (0, import_react47.createElement)(
"div",
{ key: meeting.key, className: "bt-project-meeting", "data-status": "decided" },
- (0, import_react46.createElement)("span", { className: "bt-project-date" }, meeting.date),
- (0, import_react46.createElement)("span", { className: "bt-project-title" }, meeting.question),
- meeting.owner === null ? null : (0, import_react46.createElement)("span", { className: "bt-project-owner" }, `@${meeting.owner}`),
+ (0, import_react47.createElement)("span", { className: "bt-project-date" }, meeting.date),
+ (0, import_react47.createElement)("span", { className: "bt-project-title" }, meeting.question),
+ meeting.owner === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-owner" }, `@${meeting.owner}`),
// A decided meeting whose file is gone still reads as decided; the missing path says so
// instead of the row disappearing with the file.
- meeting.path === "" ? (0, import_react46.createElement)("span", { className: "bt-project-missing" }, "decision file gone") : (0, import_react46.createElement)("span", { className: "bt-project-path" }, meeting.path)
+ meeting.path === "" ? (0, import_react47.createElement)("span", { className: "bt-project-missing" }, "decision file gone") : (0, import_react47.createElement)("span", { className: "bt-project-path" }, meeting.path)
));
- const pendingRows = meetings.pending.map((meeting) => (0, import_react46.createElement)(
+ const pendingRows = meetings.pending.map((meeting) => (0, import_react47.createElement)(
"div",
{ key: meeting.key, className: "bt-project-meeting", "data-status": meeting.status },
- (0, import_react46.createElement)("span", { className: "bt-project-date" }, meeting.date),
- (0, import_react46.createElement)("span", { className: "bt-project-title" }, meeting.blockingQuestion === null ? meeting.question : meeting.blockingQuestion),
- (0, import_react46.createElement)("span", { className: "bt-project-status" }, meeting.status)
+ (0, import_react47.createElement)("span", { className: "bt-project-date" }, meeting.date),
+ (0, import_react47.createElement)("span", { className: "bt-project-title" }, meeting.blockingQuestion === null ? meeting.question : meeting.blockingQuestion),
+ (0, import_react47.createElement)("span", { className: "bt-project-status" }, meeting.status)
));
- return (0, import_react46.createElement)(
+ return (0, import_react47.createElement)(
"section",
{ className: "bt-project-record", "data-decisions": String(decisions.count), "data-meetings": String(meetings.count) },
- (0, import_react46.createElement)("h4", null, t("Decisions")),
- decisions.empty ? (0, import_react46.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(decisions.emptyText)) : decisionRows,
- (0, import_react46.createElement)("h4", null, t("Meetings")),
- meetings.empty ? (0, import_react46.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(meetings.emptyText)) : decidedRows,
- meetings.pending.length === 0 ? null : (0, import_react46.createElement)("h5", null, t("Not decided")),
+ (0, import_react47.createElement)("h4", null, t("Decisions")),
+ decisions.empty ? (0, import_react47.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(decisions.emptyText)) : decisionRows,
+ (0, import_react47.createElement)("h4", null, t("Meetings")),
+ meetings.empty ? (0, import_react47.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(meetings.emptyText)) : decidedRows,
+ meetings.pending.length === 0 ? null : (0, import_react47.createElement)("h5", null, t("Not decided")),
meetings.pending.length === 0 ? null : pendingRows
);
}
function ProjectBoardPane({ project, rows, canManage, tasks, decisions, meetings, queues, onOpen, onSettings, onMembers }) {
const view = projectBoardView({ project, rows, canManage, tasks, decisions, meetings, queues });
- return (0, import_react46.createElement)(
+ return (0, import_react47.createElement)(
"div",
{ className: "bt-project-board", "data-can-manage": view.canManage ? "true" : "false" },
- (0, import_react46.createElement)("h3", null, view.title),
+ (0, import_react47.createElement)("h3", null, view.title),
// Header counts (#1768): four numbers read from the host, an unreadable one shown as unknown.
- view.counts === null ? null : (0, import_react46.createElement)(
+ view.counts === null ? null : (0, import_react47.createElement)(
"div",
{ className: "bt-project-counts", "data-counts": "waiting,inflight,blocked,done" },
- (0, import_react46.createElement)("span", null, [t("Waiting"), ": ", (0, import_react46.createElement)("b", { key: "w" }, countText(view.counts.waiting))]),
- (0, import_react46.createElement)("span", null, [t("In flight"), ": ", (0, import_react46.createElement)("b", { key: "f" }, countText(view.counts.inflight))]),
- (0, import_react46.createElement)("span", null, [t("Blocked"), ": ", (0, import_react46.createElement)("b", { key: "b" }, countText(view.counts.blocked))]),
- (0, import_react46.createElement)("span", null, [t("Done"), ": ", (0, import_react46.createElement)("b", { key: "d" }, countText(view.counts.done))])
+ (0, import_react47.createElement)("span", null, [t("Waiting"), ": ", (0, import_react47.createElement)("b", { key: "w" }, countText(view.counts.waiting))]),
+ (0, import_react47.createElement)("span", null, [t("In flight"), ": ", (0, import_react47.createElement)("b", { key: "f" }, countText(view.counts.inflight))]),
+ (0, import_react47.createElement)("span", null, [t("Blocked"), ": ", (0, import_react47.createElement)("b", { key: "b" }, countText(view.counts.blocked))]),
+ (0, import_react47.createElement)("span", null, [t("Done"), ": ", (0, import_react47.createElement)("b", { key: "d" }, countText(view.counts.done))])
),
// #1749: both buttons open a panel and neither is hidden from a non-owner. The host refuses
// every write a non-owner attempts, so the refusal is an answer the reader sees, not a
// control that silently disappeared.
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)(
"div",
{ className: "bt-project-actions" },
- (0, import_react46.createElement)("button", { type: "button", "data-action": "project-settings", onClick: () => onSettings?.() }, t("Project settings")),
- (0, import_react46.createElement)("button", { type: "button", "data-action": "project-members", onClick: () => onMembers?.() }, t("Members"))
+ (0, import_react47.createElement)("button", { type: "button", "data-action": "project-settings", onClick: () => onSettings?.() }, t("Project settings")),
+ (0, import_react47.createElement)("button", { type: "button", "data-action": "project-members", onClick: () => onMembers?.() }, t("Members"))
),
- (0, import_react46.createElement)(ProjectBoardRows, { view, onOpen }),
- (0, import_react46.createElement)(ProjectQueueGroups, { view, onOpen }),
- (0, import_react46.createElement)(ProjectBoardRecord, { record: view.record })
+ (0, import_react47.createElement)(ProjectBoardRows, { view, onOpen }),
+ (0, import_react47.createElement)(ProjectQueueGroups, { view, onOpen }),
+ (0, import_react47.createElement)(ProjectBoardRecord, { record: view.record })
);
}
function ProjectSettingsPanel({ project, call: request, onClose, onSaved } = {}) {
const save = request ?? call;
- const [kind, setKind] = (0, import_react46.useState)("bot");
- const [memberId, setMemberId] = (0, import_react46.useState)("");
- const [note, setNote] = (0, import_react46.useState)("");
- const [assistantOff, setAssistantOff] = (0, import_react46.useState)(project?.assistant?.off === true);
- const [status, setStatus] = (0, import_react46.useState)("");
+ const [kind, setKind] = (0, import_react47.useState)("bot");
+ const [memberId, setMemberId] = (0, import_react47.useState)("");
+ const [note, setNote] = (0, import_react47.useState)("");
+ const [assistantOff, setAssistantOff] = (0, import_react47.useState)(project?.assistant?.off === true);
+ const [status, setStatus] = (0, import_react47.useState)("");
const id = text(project?.id);
const run = async (endpoint, payload, done) => {
try {
@@ -20207,57 +20426,57 @@ function ProjectSettingsPanel({ project, call: request, onClose, onSaved } = {})
return null;
}
};
- return (0, import_react46.createElement)(
+ return (0, import_react47.createElement)(
"div",
{ className: "bt-project-panel", "data-panel": "project-settings" },
- (0, import_react46.createElement)("h3", null, t("Project settings")),
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)("h3", null, t("Project settings")),
+ (0, import_react47.createElement)(
"div",
{ className: "bt-project-field" },
- (0, import_react46.createElement)("label", { htmlFor: "bt-project-member-id" }, t("Bind a member to this Project")),
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)("label", { htmlFor: "bt-project-member-id" }, t("Bind a member to this Project")),
+ (0, import_react47.createElement)(
"div",
{ className: "bt-project-row-actions" },
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)(
"select",
{ "aria-label": t("Member kind"), value: kind, onChange: (event) => setKind(event.target.value) },
- (0, import_react46.createElement)("option", { value: "bot" }, t("Bot")),
- (0, import_react46.createElement)("option", { value: "group" }, t("Group"))
+ (0, import_react47.createElement)("option", { value: "bot" }, t("Bot")),
+ (0, import_react47.createElement)("option", { value: "group" }, t("Group"))
),
- (0, import_react46.createElement)("input", { id: "bt-project-member-id", value: memberId, placeholder: t("Member id"), onChange: (event) => setMemberId(event.target.value) }),
- (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-bind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: id }, t("Member bound.")) }, t("Bind")),
- (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-unbind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: null }, t("Member unbound.")) }, t("Unbind"))
+ (0, import_react47.createElement)("input", { id: "bt-project-member-id", value: memberId, placeholder: t("Member id"), onChange: (event) => setMemberId(event.target.value) }),
+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-bind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: id }, t("Member bound.")) }, t("Bind")),
+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-unbind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: null }, t("Member unbound.")) }, t("Unbind"))
)
),
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)(
"div",
{ className: "bt-project-field" },
- (0, import_react46.createElement)("label", null, t("Project Assistant")),
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)("label", null, t("Project Assistant")),
+ (0, import_react47.createElement)(
"div",
{ className: "bt-project-row-actions" },
- (0, import_react46.createElement)("label", null, (0, import_react46.createElement)("input", { type: "checkbox", "data-action": "assistant-off", checked: assistantOff, onChange: (event) => setAssistantOff(event.target.checked) }), " ", t("Pause the daily digest")),
- (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-assistant-save", onClick: () => run("project-assistant-owner", { id, settings: { off: assistantOff === true } }, t("Assistant settings saved.")) }, t("Save"))
+ (0, import_react47.createElement)("label", null, (0, import_react47.createElement)("input", { type: "checkbox", "data-action": "assistant-off", checked: assistantOff, onChange: (event) => setAssistantOff(event.target.checked) }), " ", t("Pause the daily digest")),
+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-assistant-save", onClick: () => run("project-assistant-owner", { id, settings: { off: assistantOff === true } }, t("Assistant settings saved.")) }, t("Save"))
)
),
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)(
"div",
{ className: "bt-project-field" },
// The host knows two memory kinds; the free text the panel takes is recorded as a decision,
// never as a third kind the host would refuse (#1820).
- (0, import_react46.createElement)("label", { htmlFor: "bt-project-note" }, t("Add to the assistant memory")),
- (0, import_react46.createElement)("textarea", { id: "bt-project-note", value: note, onChange: (event) => setNote(event.target.value) }),
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)("label", { htmlFor: "bt-project-note" }, t("Add to the assistant memory")),
+ (0, import_react47.createElement)("textarea", { id: "bt-project-note", value: note, onChange: (event) => setNote(event.target.value) }),
+ (0, import_react47.createElement)(
"div",
{ className: "bt-project-row-actions" },
- (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-memory-save", onClick: () => run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")) }, t("Save memory"))
+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-memory-save", onClick: () => run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")) }, t("Save memory"))
)
),
- (0, import_react46.createElement)("div", { className: "bt-project-status-line", role: "status" }, status),
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)("div", { className: "bt-project-status-line", role: "status" }, status),
+ (0, import_react47.createElement)(
"div",
{ className: "bt-project-row-actions" },
- (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board"))
+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board"))
)
);
}
@@ -20274,32 +20493,32 @@ function ProjectMembersPanel({ project, rows, onClose } = {}) {
const name = text(row?.assignee);
if (name !== "" && !boardMembers.includes(name)) boardMembers.push(name);
}
- return (0, import_react46.createElement)(
+ return (0, import_react47.createElement)(
"div",
{ className: "bt-project-panel", "data-panel": "project-members" },
- (0, import_react46.createElement)("h3", null, t("Members")),
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)("h3", null, t("Members")),
+ (0, import_react47.createElement)(
"ul",
{ className: "bt-project-members" },
- owners.map((owner) => (0, import_react46.createElement)(
+ owners.map((owner) => (0, import_react47.createElement)(
"li",
{ key: `owner:${owner}`, className: "bt-project-member", "data-member-role": "owner", "data-member-id": owner },
- (0, import_react46.createElement)("span", null, `@${owner}`),
- (0, import_react46.createElement)("span", { className: "bt-project-member-kind" }, t("Owner"))
+ (0, import_react47.createElement)("span", null, `@${owner}`),
+ (0, import_react47.createElement)("span", { className: "bt-project-member-kind" }, t("Owner"))
)),
- boardMembers.map((name) => (0, import_react46.createElement)(
+ boardMembers.map((name) => (0, import_react47.createElement)(
"li",
{ key: `board:${name}`, className: "bt-project-member", "data-member-role": "board", "data-member-id": name },
- (0, import_react46.createElement)("span", null, `@${name}`),
- (0, import_react46.createElement)("span", { className: "bt-project-member-kind" }, t("On the board"))
+ (0, import_react47.createElement)("span", null, `@${name}`),
+ (0, import_react47.createElement)("span", { className: "bt-project-member-kind" }, t("On the board"))
))
),
- owners.length === 0 && boardMembers.length === 0 ? (0, import_react46.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null,
- (0, import_react46.createElement)("p", { className: "bt-project-note" }, t("Member management is not part of this slice; owners and the members their cards name are listed here.")),
- (0, import_react46.createElement)(
+ owners.length === 0 && boardMembers.length === 0 ? (0, import_react47.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null,
+ (0, import_react47.createElement)("p", { className: "bt-project-note" }, t("Member management is not part of this slice; owners and the members their cards name are listed here.")),
+ (0, import_react47.createElement)(
"div",
{ className: "bt-project-row-actions" },
- (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board"))
+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board"))
)
);
}
@@ -20307,15 +20526,15 @@ var PROJECT_BOARD_POLL_MS = 5e3;
var boardPollPaused = () => typeof document !== "undefined" && document.visibilityState === "hidden" || typeof window !== "undefined" && !window.location.hash.startsWith("#project");
function ProjectBoardOverlay({ actions, call: request } = {}) {
const read = request ?? call;
- const [state, setState] = (0, import_react46.useState)(null);
- const [panel, setPanel] = (0, import_react46.useState)(null);
- const [hash, setHash] = (0, import_react46.useState)(() => typeof window === "undefined" ? "" : window.location.hash);
- const close = (0, import_react46.useCallback)(() => {
+ const [state, setState] = (0, import_react47.useState)(null);
+ const [panel, setPanel] = (0, import_react47.useState)(null);
+ const [hash, setHash] = (0, import_react47.useState)(() => typeof window === "undefined" ? "" : window.location.hash);
+ const close = (0, import_react47.useCallback)(() => {
if (typeof window !== "undefined") window.location.hash = "";
setPanel(null);
setState(null);
}, []);
- (0, import_react46.useEffect)(() => {
+ (0, import_react47.useEffect)(() => {
if (typeof window === "undefined") return void 0;
const onKey = (event) => {
if (event.key !== "Escape") return;
@@ -20325,14 +20544,14 @@ function ProjectBoardOverlay({ actions, call: request } = {}) {
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [panel, close]);
- (0, import_react46.useEffect)(() => {
+ (0, import_react47.useEffect)(() => {
if (typeof window === "undefined") return void 0;
const sync = () => setHash(window.location.hash);
window.addEventListener("hashchange", sync);
sync();
return () => window.removeEventListener("hashchange", sync);
}, []);
- (0, import_react46.useEffect)(() => {
+ (0, import_react47.useEffect)(() => {
if (typeof window === "undefined") return void 0;
if (!hash.startsWith("#project")) {
setState(null);
@@ -20372,32 +20591,32 @@ function ProjectBoardOverlay({ actions, call: request } = {}) {
};
}, [hash]);
if (state === null) return null;
- return (0, import_react46.createElement)(
+ return (0, import_react47.createElement)(
"div",
{ className: "bt-project-overlay" },
- (0, import_react46.createElement)("div", { className: "bt-scrim", onClick: close }),
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)("div", { className: "bt-scrim", onClick: close }),
+ (0, import_react47.createElement)(
"div",
{ className: "bt-mem bt-project-dialog", role: "dialog", "aria-modal": true, "aria-label": t("Project board") },
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)(
"div",
{ className: "bt-mem-head" },
- (0, import_react46.createElement)("h2", {}, t("Project board")),
- (0, import_react46.createElement)("span", { className: "bt-mem-head-gap" }),
- (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-project-close", autoFocus: true, onClick: close }, t("Close"))
+ (0, import_react47.createElement)("h2", {}, t("Project board")),
+ (0, import_react47.createElement)("span", { className: "bt-mem-head-gap" }),
+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-project-close", autoFocus: true, onClick: close }, t("Close"))
),
- (0, import_react46.createElement)(
+ (0, import_react47.createElement)(
"div",
{ className: "bt-project-body" },
- state.project === null ? (0, import_react46.createElement)(
+ state.project === null ? (0, import_react47.createElement)(
"div",
{ className: "bt-project-empty" },
- (0, import_react46.createElement)("p", null, t("No Project yet — create one.")),
- (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
+ (0, import_react47.createElement)("p", null, t("No Project yet — create one.")),
+ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
close();
actions?.openSettings?.("projects");
} }, t("Open Projects in Settings"))
- ) : panel === "settings" ? (0, import_react46.createElement)(ProjectSettingsPanel, { project: state.project, call: read, onClose: () => setPanel(null) }) : panel === "members" ? (0, import_react46.createElement)(ProjectMembersPanel, { project: state.project, rows: state.rows, onClose: () => setPanel(null) }) : (0, import_react46.createElement)(ProjectBoardPane, {
+ ) : panel === "settings" ? (0, import_react47.createElement)(ProjectSettingsPanel, { project: state.project, call: read, onClose: () => setPanel(null) }) : panel === "members" ? (0, import_react47.createElement)(ProjectMembersPanel, { project: state.project, rows: state.rows, onClose: () => setPanel(null) }) : (0, import_react47.createElement)(ProjectBoardPane, {
project: state.project,
rows: state.rows,
canManage: state.canManage,
@@ -20489,7 +20708,7 @@ function writeMode(mode, storage = globalThis.localStorage) {
}
// src/client/user-message.js
-var import_react47 = require("react");
+var import_react48 = require("react");
var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
var canDrawUserMessages = () => typeof ui.projectUserText === "function";
function contentParts(content) {
@@ -20575,38 +20794,38 @@ function userTextParts(text2, bots, referenceLabels = []) {
return parts;
}
function UserText({ text: text2, bots, referenceLabels, skillNames, references, actions }) {
- const chip = (part) => [(0, import_react47.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`];
+ const chip = (part) => [(0, import_react48.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`];
return userTextParts(text2, bots, referenceLabels).map((part, index) => {
- if (!part.bot) return (0, import_react47.createElement)(import_react47.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
+ if (!part.bot) return (0, import_react48.createElement)(import_react48.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
const title = `@${part.bot.name}`;
- return typeof part.bot.id === "string" ? (0, import_react47.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react47.createElement)("span", { key: index, className: "bt-umention", title }, chip(part));
+ return typeof part.bot.id === "string" ? (0, import_react48.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react48.createElement)("span", { key: index, className: "bt-umention", title }, chip(part));
});
}
function Attachments({ attachments, renderMessageImages }) {
const compact2 = attachments.length > 1;
- return (0, import_react47.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => {
+ return (0, import_react48.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => {
if (block.type === "image") {
- return renderMessageImages ? (0, import_react47.createElement)(import_react47.Fragment, { key: index }, renderMessageImages({ images: [{ attachment: block.attachment }], align: "end", compact: compact2 })) : null;
+ return renderMessageImages ? (0, import_react48.createElement)(import_react48.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_react47.createElement)(
+ return (0, import_react48.createElement)(
"span",
{ key: index, className: "bt-ufile", title: file.name },
- ui.FileTypeIcon ? (0, import_react47.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null,
- (0, import_react47.createElement)("span", { className: "bt-ufile-text" }, (0, import_react47.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react47.createElement)("span", { className: "bt-ufile-meta" }, meta) : null)
+ ui.FileTypeIcon ? (0, import_react48.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null,
+ (0, import_react48.createElement)("span", { className: "bt-ufile-text" }, (0, import_react48.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react48.createElement)("span", { className: "bt-ufile-meta" }, meta) : null)
);
}));
}
function CopyButton({ text: text2 }) {
- const [copied, setCopied] = (0, import_react47.useState)(false);
+ const [copied, setCopied] = (0, import_react48.useState)(false);
const copy2 = () => {
void navigator.clipboard?.writeText(text2).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1e3);
});
};
- return (0, import_react47.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_react47.createElement)(CheckIcon) : (0, import_react47.createElement)(CopyIcon));
+ return (0, import_react48.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_react48.createElement)(CheckIcon) : (0, import_react48.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 }) {
@@ -20623,7 +20842,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_react47.createElement)(
+ return (0, import_react48.createElement)(
"div",
{
className: incoming ? "bt-umsg bt-umsg-other" : "bt-umsg",
@@ -20633,22 +20852,22 @@ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageIma
"data-author-kind": author?.authorKind,
"data-author-name": author?.name
},
- person ? (0, import_react47.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react47.createElement)(PersonMark, { name: author.name })) : null,
- faceMine ? (0, import_react47.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react47.createElement)(PersonMark, { name: myName || void 0 })) : null,
- (0, import_react47.createElement)(
+ person ? (0, import_react48.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react48.createElement)(PersonMark, { name: author.name })) : null,
+ faceMine ? (0, import_react48.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react48.createElement)(PersonMark, { name: myName || void 0 })) : null,
+ (0, import_react48.createElement)(
"div",
{ className: "bt-ustack" },
- faceMine && myName ? (0, import_react47.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react47.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react47.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null,
- attachments.length > 0 ? (0, import_react47.createElement)(Attachments, { attachments, renderMessageImages }) : null,
- text2 !== "" || rest.length > 0 ? (0, import_react47.createElement)(
+ faceMine && myName ? (0, import_react48.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react48.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react48.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null,
+ attachments.length > 0 ? (0, import_react48.createElement)(Attachments, { attachments, renderMessageImages }) : null,
+ text2 !== "" || rest.length > 0 ? (0, import_react48.createElement)(
"div",
{ className: "bt-ubody", title: clock3(data.time) },
- (0, import_react47.createElement)(UserText, { text: text2, bots: mentionableBots(roster, sessionId, { fromFriend: Boolean(incoming && author.ownerName) }), referenceLabels, skillNames: data.skillNames ?? [], references, actions }),
- rest.map((block, index) => ui.JsonBlock ? (0, import_react47.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null)
+ (0, import_react48.createElement)(UserText, { text: text2, bots: mentionableBots(roster, sessionId, { fromFriend: Boolean(incoming && author.ownerName) }), referenceLabels, skillNames: data.skillNames ?? [], references, actions }),
+ rest.map((block, index) => ui.JsonBlock ? (0, import_react48.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null)
) : null,
- referenceLabels.length > 0 ? (0, import_react47.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null
+ referenceLabels.length > 0 ? (0, import_react48.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null
),
- text2 === "" ? null : (0, import_react47.createElement)("div", { className: "bt-utools" }, (0, import_react47.createElement)(CopyButton, { text: text2 }))
+ text2 === "" ? null : (0, import_react48.createElement)("div", { className: "bt-utools" }, (0, import_react48.createElement)(CopyButton, { text: text2 }))
);
}
var USER_MESSAGE_CSS = `
@@ -20746,7 +20965,7 @@ function noticeApprovals({ work, roster, shownSession, openSession, since: since
}
// src/client/stats-dock.js
-var import_react48 = require("react");
+var import_react49 = require("react");
var STATS_DOCK = "conversation.composer.dock";
var STATS_ID = "stats";
var STATS_IDS = [STATS_ID, "activity"];
@@ -20762,17 +20981,17 @@ function pluralStats(t2) {
function statsDock(entriesOf, id = STATS_ID) {
const shell = () => (entriesOf(STATS_DOCK) ?? []).find((entry) => entry?.component !== StatsDock && entry?.options?.id === id);
function StatsDock(props) {
- const t2 = (0, import_react48.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]);
+ const t2 = (0, import_react49.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_react48.createElement)(entry.component, { ...props, t: t2 });
+ return (0, import_react49.createElement)(entry.component, { ...props, t: t2 });
}
return { component: StatsDock, inject: (...args) => shell()?.inject?.(...args) ?? {} };
}
var statsDocks = (entriesOf) => STATS_IDS.map((id) => ({ id, ...statsDock(entriesOf, id) }));
// src/client/index.js
-var import_react49 = require("react");
+var import_react50 = require("react");
var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout"];
function apply(ctx) {
const flybot = createClientApi2();
@@ -21671,6 +21890,7 @@ function apply(ctx) {
installCompiledStyles(MOBILE_CSS, "mobile"),
installCompiledStyles(PANE_MIN_WIDTH_CSS, "panes"),
installCompiledStyles(PROJECT_BOARD_CSS, "project-board"),
+ installCompiledStyles(PROJECTS_NAV_CSS, "projects-nav"),
typeof document !== "undefined" ? watchShellLevel(document) : null,
browsers ? installCompiledStyles(BROWSER_CSS, "browser") : null,
installTheme(),
@@ -21743,7 +21963,7 @@ function apply(ctx) {
];
ctx.effect(() => {
const style = installStyles(TURN_FAILURES_CSS, "turn-failures");
- const off = registry().register("flybot.session.dock", { id: "flybot-turn-failures", order: 20, component: (props) => (0, import_react49.createElement)(TurnFailures, { ...props, work }) });
+ const off = registry().register("flybot.session.dock", { id: "flybot-turn-failures", order: 20, component: (props) => (0, import_react50.createElement)(TurnFailures, { ...props, work }) });
return () => {
off();
style.remove();
diff --git a/core/src/client/i18n.js b/core/src/client/i18n.js
index b2d49c551..8c431e1aa 100644
--- a/core/src/client/i18n.js
+++ b/core/src/client/i18n.js
@@ -1329,6 +1329,16 @@ const ZH = {
'e.g. Website': '例如:官网',
"Optional. Where this Project's work happens.": '可选。这个项目的工作目录。',
'Projects · {count}': '项目 · {count}',
+ // #1828: a Project's roles, and the sidebar row that lists them.
+ Maintainer: '维护者',
+ Member: '成员',
+ 'No Projects yet': '还没有项目',
+ 'Reading Projects…': '正在读取项目…',
+ 'My role': '我的角色',
+ 'No role to switch here.': '这里没有可切换的角色。',
+ 'Role changed.': '角色已更改。',
+ 'Role unchanged: {reason}': '角色未更改:{reason}',
+ refused: '被拒绝',
'No Project yet — create one.': '还没有项目——新建一个。',
'Bind a Bot or group': '绑定 Bot 或群聊',
'Bind to {name}': '绑定到 {name}',
diff --git a/core/src/client/index.js b/core/src/client/index.js
index cd7590d74..8f48d3b1c 100644
--- a/core/src/client/index.js
+++ b/core/src/client/index.js
@@ -38,6 +38,7 @@ import { KANBAN_CSS } from './kanban.js'
import { MOBILE_CSS } from './mobile.js'
import { PANE_MIN_WIDTH_CSS, watchShellLevel } from './panes.js'
import { PROJECT_BOARD_CSS, ProjectBoardOverlay } from './project-board.js'
+import { PROJECTS_NAV_CSS } from './projects-nav.js'
import { STORAGE, migrateStorage } from './storage.js'
import { friendLineNews } from './friend-lines.js'
import { groupsFor, readMode, surfaceGroups, writeMode } from './surface.js'
@@ -893,6 +894,7 @@ export function apply(ctx) {
installCompiledStyles(MOBILE_CSS, 'mobile'),
installCompiledStyles(PANE_MIN_WIDTH_CSS, 'panes'),
installCompiledStyles(PROJECT_BOARD_CSS, 'project-board'),
+ installCompiledStyles(PROJECTS_NAV_CSS, 'projects-nav'),
typeof document !== 'undefined' ? watchShellLevel(document) : null,
browsers ? installCompiledStyles(BROWSER_CSS, 'browser') : null,
installTheme(),
diff --git a/core/src/client/projects-nav.js b/core/src/client/projects-nav.js
new file mode 100644
index 000000000..59f9901cc
--- /dev/null
+++ b/core/src/client/projects-nav.js
@@ -0,0 +1,191 @@
+// #1828: Projects as a first-level sidebar entry. The row expands to the Projects this caller can
+// see, each with the role they hold there; a row opens that board straight (`#project=`), and
+// the right-click menu offers only the roles the host says the caller may switch to, with the
+// current one marked. The policy itself lives in the host (`roleChoices`/`changeRole`), so this
+// module never decides who may switch what — it draws what the host answered and shows a refusal
+// as the host worded it.
+import { createElement as h, useCallback, useEffect, useRef, useState } from 'react'
+import { createPortal } from 'react-dom'
+import { call as callHost } from './api.js'
+import { t } from './i18n.js'
+
+const call = (endpoint, payload) => callHost(`flybot/${endpoint}`, payload)
+
+export const PROJECTS_NAV_CSS = `
+.bt-projects-row { display: flex; align-items: center; gap: 8px; }
+.bt-projects-chevron { margin-left: auto; display: inline-flex; transition: transform 120ms ease; }
+.bt-projects-row[aria-expanded="true"] .bt-projects-chevron { transform: rotate(90deg); }
+.bt-projects-list { display: flex; flex-direction: column; margin: 0 0 4px; padding-left: 30px; }
+.bt-project-entry { display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left; border: 0; background: none; color: inherit; font: inherit; padding: 4px 8px; border-radius: 6px; cursor: pointer; }
+.bt-project-entry:hover, .bt-project-entry:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; }
+.bt-project-entry[aria-current="true"] { background: var(--bt-hover, rgba(127,127,127,.14)); }
+.bt-project-entry-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.bt-project-entry-role { margin-left: auto; opacity: .62; font-size: .85em; white-space: nowrap; }
+.bt-projects-empty, .bt-projects-note { padding: 2px 8px 6px; opacity: .7; font-size: .85em; }
+.bt-project-menu { position: fixed; z-index: 60; min-width: 168px; padding: 4px; border-radius: 8px; background: var(--bt-menu-bg, #1f2126); border: 1px solid var(--bt-menu-border, rgba(255,255,255,.12)); box-shadow: 0 8px 24px rgba(0,0,0,.32); }
+.bt-project-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: none; color: inherit; font: inherit; text-align: left; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
+.bt-project-menu-item:hover, .bt-project-menu-item:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; }
+.bt-project-menu-item[aria-checked="true"]::before { content: "✓"; margin-right: 2px; }
+.bt-project-menu-item:not([aria-checked="true"])::before { content: " "; margin-right: 2px; }
+.bt-project-menu-item[disabled] { opacity: .6; cursor: default; }
+`
+
+const ROLE_LABELS = { owner: 'Owner', maintainer: 'Maintainer', member: 'Member' }
+
+export function roleText(role) {
+ if (role === null || role === undefined || role === '') return ''
+ return t(ROLE_LABELS[role] ?? String(role))
+}
+
+// `#project=` is the one entry point the board reads (`ProjectBoardOverlay`), so opening a row
+// writes the hash instead of mounting a second copy of the board.
+export function projectHash(id) {
+ return `#project=${encodeURIComponent(String(id ?? ''))}`
+}
+
+export function projectName(project) {
+ const name = typeof project?.name === 'string' ? project.name.trim() : ''
+ return name === '' ? `#${project?.id ?? ''}` : name
+}
+
+// The menu lists what the host said this caller may switch to, with the current rank marked. A
+// Project written before roles existed answers `role: null`, so the row reads without a rank
+// rather than guessing owner-only.
+export function projectMenuItems(project) {
+ const current = project?.role ?? null
+ const choices = Array.isArray(project?.choices) ? project.choices : []
+ const items = current === null ? [] : [{ role: current, label: roleText(current), current: true, pick: false }]
+ for (const role of choices) {
+ if (role === current) continue
+ items.push({ role, label: roleText(role), current: false, pick: true })
+ }
+ return items
+}
+
+// A refusal names a machine-readable reason (`not-owner`, `cannot-self-promote`, `last-owner`); the
+// reader gets the code back unchanged, as the brief asks, rather than a guess at its meaning.
+export function roleNote(answer) {
+ if (answer?.changed === true) return t('Role changed.')
+ const reason = typeof answer?.reason === 'string' && answer.reason !== '' ? answer.reason : t('refused')
+ return t('Role unchanged: {reason}', { reason })
+}
+
+// The expanded list on its own, so a test can draw it without a fetch or a click.
+export function ProjectList({ projects, error, note, onOpen, onRole }) {
+ const rows = projects === null
+ ? h('div', { className: 'bt-projects-note' }, t('Reading Projects…'))
+ : error !== ''
+ ? h('div', { className: 'bt-projects-note' }, error)
+ : projects.length === 0
+ ? h('div', { className: 'bt-projects-empty' }, t('No Projects yet'))
+ : projects.map(project => h('button', {
+ key: String(project.id),
+ type: 'button', className: 'bt-project-entry', 'data-project-id': String(project.id),
+ 'aria-current': project.current === true ? 'true' : undefined,
+ title: roleText(project.role) === '' ? projectName(project) : `${projectName(project)} · ${roleText(project.role)}`,
+ onClick: () => onOpen?.(project.id),
+ onContextMenu: (event) => { event.preventDefault(); onRole?.(project, event) },
+ },
+ h('span', { className: 'bt-project-entry-name' }, projectName(project)),
+ roleText(project.role) === '' ? null : h('span', { className: 'bt-project-entry-role' }, roleText(project.role))))
+ return h('div', { className: 'bt-projects-list', role: 'group', 'aria-label': t('Projects') },
+ rows,
+ note === '' || note === undefined ? null : h('div', { className: 'bt-projects-note', role: 'status' }, note))
+}
+
+function Chevron() {
+ return h('svg', { width: 12, height: 12, viewBox: '0 0 20 20', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
+ h('path', { d: 'M7.5 4.5L13 10l-5.5 5.5' }))
+}
+
+function ProjectsMark() {
+ 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('path', { d: 'M2.5 5.5h6l1.5 2h7.5v8a1 1 0 0 1-1 1h-13a1 1 0 0 1-1-1z' }),
+ h('path', { d: 'M2.5 5.5v-1a1 1 0 0 1 1-1h4l1.5 2' }))
+}
+
+// The menu is a portal at the cursor, closed by a click anywhere else, by Escape, and by picking.
+export function ProjectRoleMenu({ project, x, y, onPick, onClose }) {
+ const ref = useRef(null)
+ useEffect(() => {
+ const onDown = (event) => { if (!ref.current?.contains(event.target)) onClose() }
+ const onKey = (event) => { if (event.key === 'Escape') onClose() }
+ window.addEventListener('mousedown', onDown, true)
+ window.addEventListener('keydown', onKey, true)
+ return () => { window.removeEventListener('mousedown', onDown, true); window.removeEventListener('keydown', onKey, true) }
+ }, [onClose])
+ const items = projectMenuItems(project)
+ // A member has nothing to switch: the menu still opens and marks the rank they hold, so the
+ // right-click answers "where am I" instead of doing nothing.
+ return createPortal(h('div', {
+ ref, className: 'bt-project-menu', role: 'menu', 'aria-label': t('My role'),
+ style: { left: `${x}px`, top: `${y}px` }, 'data-project-id': String(project?.id ?? ''),
+ },
+ h('div', { className: 'bt-projects-note' }, projectName(project)),
+ items.length === 0
+ ? h('div', { className: 'bt-projects-note' }, t('No role to switch here.'))
+ : items.map(item => h('button', {
+ key: item.role, type: 'button', role: 'menuitemradio', className: 'bt-project-menu-item',
+ 'aria-checked': item.current ? 'true' : 'false', disabled: !item.pick,
+ onClick: () => { if (item.pick) onPick(item.role); else onClose() },
+ }, item.label))), document.body)
+}
+
+export function ProjectsNav({ wide = true, request, onOpen } = {}) {
+ const read = request ?? call
+ const open = useCallback((id) => {
+ if (onOpen) { onOpen(id); return }
+ if (typeof window !== 'undefined') window.location.hash = projectHash(id)
+ }, [onOpen])
+ const [expanded, setExpanded] = useState(false)
+ const [projects, setProjects] = useState(null)
+ const [error, setError] = useState('')
+ const [note, setNote] = useState('')
+ const [menu, setMenu] = useState(null)
+ const load = useCallback(async () => {
+ try {
+ const listing = await read('projects')
+ const list = Array.isArray(listing?.projects) ? listing.projects : []
+ setProjects(list)
+ setError('')
+ } catch (failure) {
+ setProjects([])
+ setError(failure?.message ?? String(failure))
+ }
+ }, [read])
+ useEffect(() => {
+ if (!expanded) return
+ void load()
+ }, [expanded, load])
+ const toggle = () => {
+ setNote('')
+ if (!wide) { void open(''); return }
+ setExpanded(value => !value)
+ }
+ const listed = expanded && wide
+ return h('div', { className: 'bt-projects' },
+ h('button', {
+ type: 'button', className: 'bt-row bt-projects-row', 'aria-expanded': listed ? 'true' : 'false',
+ 'aria-label': t('Projects'), title: wide ? undefined : t('Projects'), onClick: toggle,
+ },
+ h('span', { className: 'bt-projects-mark' }, h(ProjectsMark)),
+ wide ? h('span', { className: 'bt-projects-label' }, t('Projects')) : null,
+ wide ? h('span', { className: 'bt-projects-chevron', 'aria-hidden': true }, h(Chevron)) : null),
+ listed ? h(ProjectList, { projects, error, note, onOpen: open, onRole: (project, event) => setMenu({ project, x: event.clientX, y: event.clientY }) }) : null,
+ menu === null ? null : h(ProjectRoleMenu, {
+ project: menu.project, x: menu.x, y: menu.y,
+ onClose: () => setMenu(null),
+ onPick: async (role) => {
+ const project = menu.project
+ setMenu(null)
+ try {
+ const answer = await read('project-role', { id: project.id, role })
+ setNote(roleNote(answer))
+ if (answer?.changed === true) void load()
+ } catch (failure) {
+ // The host's sentence is the answer; quoting it beats a generic failure line.
+ setNote(failure?.message ?? String(failure))
+ }
+ },
+ }))
+}
diff --git a/core/src/client/sidebar.js b/core/src/client/sidebar.js
index bb94a1ffe..34bd2c8bc 100644
--- a/core/src/client/sidebar.js
+++ b/core/src/client/sidebar.js
@@ -16,6 +16,7 @@ import { visibleText } from '../shared/preview.js'
import { showsDshRow } from './dsh-offer.js'
import { InboxRow } from './inbox.js'
import { KanbanRow } from './kanban.js'
+import { ProjectsNav } from './projects-nav.js'
// -------------------------------------------------------------------------
// Sidebar
@@ -395,6 +396,7 @@ export function TeamSidebar(props) {
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(ProjectsNav, { wide }),
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/channel.js b/core/src/host/channel.js
index 71565bd24..c5a67ff27 100644
--- a/core/src/host/channel.js
+++ b/core/src/host/channel.js
@@ -675,6 +675,7 @@ export function install(rt) {
case 'project-bind':
case 'project-of':
case 'project-board':
+ case 'project-role':
case 'project-delete':
case 'project-memory':
case 'project-memory-write':
diff --git a/core/src/host/project-board.js b/core/src/host/project-board.js
index 6af97145a..8b830395f 100644
--- a/core/src/host/project-board.js
+++ b/core/src/host/project-board.js
@@ -21,6 +21,74 @@ import { join } from 'node:path'
import { columnOf } from '../shared/task-board.js'
import { DECISIONS_DIR, listProjectState, sha256 } from './project-state.js'
+// #1828: a Project's roles. The record stores only `roles: { maintainers, members }`; the owner
+// is read from `ownerUserIds`, never stored twice, so the two cannot disagree. A record written
+// before this slice carries no `roles` and derives to owner-only — no migration pass.
+export const PROJECT_ROLE_RANKS = { member: 1, maintainer: 2, owner: 3 }
+const roleList = value => (Array.isArray(value) ? [...new Set(value.map(id => text(id)).filter(id => id !== ''))] : [])
+export function rolesOf(project) {
+ const owner = roleList(project?.ownerUserIds)
+ const primary = text(project?.ownerUserId)
+ if (primary !== '' && !owner.includes(primary)) owner.unshift(primary)
+ const stored = project?.roles !== null && typeof project?.roles === 'object' ? project.roles : {}
+ const maintainers = roleList(stored.maintainers).filter(id => !owner.includes(id))
+ const members = roleList(stored.members).filter(id => !owner.includes(id) && !maintainers.includes(id))
+ return { owner, maintainers, members }
+}
+export function roleOf(project, memberId) {
+ const id = text(memberId)
+ if (id === '') return null
+ const roles = rolesOf(project)
+ if (roles.owner.includes(id)) return 'owner'
+ if (roles.maintainers.includes(id)) return 'maintainer'
+ if (roles.members.includes(id)) return 'member'
+ return null
+}
+// The roles a caller may switch a target to: the current one is not repeated, and the Owner rank
+// is never an assignment (transfer is a separate slice). A Member sees nothing, a Maintainer
+// sees only Member, and an Owner acting on anyone else sees the two assignable ranks.
+export function roleChoices(project, actorId, targetId = actorId) {
+ const actor = roleOf(project, actorId)
+ const self = text(targetId) === text(actorId)
+ const target = self ? actor : roleOf(project, targetId)
+ const choices = []
+ // The Owner of a Project never drops its own rank here: the menu shows only the current item.
+ if (self && actor === 'owner') return choices
+ for (const role of ['maintainer', 'member']) {
+ if (role === target) continue
+ if (actor === 'owner') choices.push(role)
+ else if (self && actor !== null && PROJECT_ROLE_RANKS[role] < PROJECT_ROLE_RANKS[actor]) choices.push(role)
+ }
+ return choices
+}
+// The policy Chief's matrix (2026-10-30) fixes, in one place the endpoint and the menu both read:
+// only an Owner re-roles someone else, nobody promotes themselves, and a Project always keeps its
+// Owner. Returns the next stored roles on success and a machine-readable reason on refusal; the
+// caller's record is untouched either way.
+export function changeRole(project, { actorId, targetId, role } = {}) {
+ const actor = roleOf(project, actorId)
+ const self = text(targetId) === '' || text(targetId) === text(actorId)
+ const target = self ? text(actorId) : text(targetId)
+ const wanted = text(role)
+ const current = rolesOf(project)
+ if (PROJECT_ROLE_RANKS[wanted] === undefined) return { ok: false, reason: 'unknown-role' }
+ if (wanted === 'owner') return { ok: false, reason: 'owner-not-assignable' }
+ if (actor === null) return { ok: false, reason: 'not-owner' }
+ if (self) {
+ if (actor === 'owner') return { ok: false, reason: 'last-owner' }
+ if (PROJECT_ROLE_RANKS[wanted] > PROJECT_ROLE_RANKS[actor]) return { ok: false, reason: 'cannot-self-promote' }
+ } else if (actor !== 'owner') {
+ return { ok: false, reason: 'not-owner' }
+ } else if (current.owner.includes(target)) {
+ return { ok: false, reason: 'last-owner' }
+ }
+ const maintainers = current.maintainers.filter(id => id !== target)
+ const members = current.members.filter(id => id !== target)
+ if (wanted === 'maintainer') maintainers.push(target)
+ else members.push(target)
+ return { ok: true, roles: { maintainers, members } }
+}
+
const text = value => (typeof value === 'string' ? value.trim() : '')
// The queue, the leases and the blocked cards are host truth the board never copied (#1768):
@@ -364,6 +432,13 @@ export function install(rt) {
waiting.sort((a, b) => (a.since ?? Infinity) - (b.since ?? Infinity) || a.id.localeCompare(b.id))
return { waiting, inflight, blocked, counts: { waiting: waiting.length, inflight: inflight.length, blocked: blocked.length, done } }
}
+ // #1828: the endpoint the host calls for a role switch. It answers the policy's own decision;
+ // the store is what persists the two assignable lists.
+ rt.projectRole = (project, { actorId, targetId, role } = {}) => changeRole(project, { actorId, targetId, role })
+ // The listing's own read (#1828): the caller's rank and the ranks they may switch to. Both are
+ // the functions the endpoint uses, so the sidebar menu offers exactly what the host accepts.
+ rt.projectRoleOf = (project, memberId) => roleOf(project, memberId)
+ rt.projectRoleChoices = (project, memberId) => roleChoices(project, memberId)
rt.projectBoard = projectBoard
// The three groups are their own read over the same pass, for a caller that wants only the queue.
rt.projectBoardQueues = async (projectId, actor) => {
diff --git a/core/src/host/store.js b/core/src/host/store.js
index 6127b1d64..02dbb08e7 100644
--- a/core/src/host/store.js
+++ b/core/src/host/store.js
@@ -570,9 +570,42 @@ export function install(rt) {
throw projectRefusal('Only the owner of this Project may change it', 'bot/forbidden')
}
+// #1828: only the two assignable ranks are stored; the Owner stays derived from `ownerUserIds`.
+ const setProjectRoles = (project, roles) => {
+ const list = value => (Array.isArray(value) ? [...new Set(value.map(id => projectText(id, 120)).filter(id => id !== ''))] : [])
+ project.roles = { maintainers: list(roles?.maintainers), members: list(roles?.members) }
+ project.updatedAt = Math.max(project.updatedAt, Date.now())
+ return project.roles
+ }
const projectEndpoint = async (endpoint, body = {}, actor = null) => {
switch (endpoint) {
- case 'projects': return { projects: listProjects() }
+ // #1828: the sidebar reads one list, and each entry carries the caller's own role and the
+ // roles they may switch to — both from the same policy the endpoint applies, so the menu and
+ // the refusal cannot disagree. A host without the helpers (an older plugin instance) answers
+ // neither field and the client draws the list without a role.
+ case 'projects': {
+ const actorId = projectActorId(actor)
+ return {
+ projects: listProjects().map(project => ({
+ ...project,
+ role: rt.projectRoleOf === undefined ? null : rt.projectRoleOf(project, actorId),
+ choices: rt.projectRoleChoices === undefined ? [] : rt.projectRoleChoices(project, actorId),
+ })),
+ }
+ }
+ // #1828: the role switch. The policy lives in project-board.js (`changeRole`), so the endpoint
+ // and the menu cannot disagree; a refusal answers the reason and writes nothing.
+ case 'project-role': {
+ const project = readProject(projectText(body.id, 200))
+ if (project === null) throw projectRefusal('Unknown Project', 'bot/not-found')
+ const result = rt.projectRole === undefined
+ ? { ok: false, reason: 'not-owner' }
+ : rt.projectRole(project, { actor, actorId: projectActorId(actor), targetId: body.memberId, role: body.role })
+ if (result.ok !== true) return { project, changed: false, reason: result.reason }
+ const roles = setProjectRoles(project, result.roles)
+ await save()
+ return { project, changed: true, roles }
+ }
case 'project-create': {
// The caller is stamped as the owner; a member id from the body cannot mint a
// Project someone else manages.
@@ -956,7 +989,7 @@ export function install(rt) {
chatOwners, groupOwners, pastOwners,
groupSessionsOf, dropGroupSession, findBot, findRooms, isAdminOf,
initialOwnerId, syncDesignatedChief,
- projectEndpoint, createProject, readProject, listProjects, bindProject, projectOfMember,
+ projectEndpoint, createProject, readProject, listProjects, bindProject, projectOfMember, setProjectRoles,
projectMemory, projectMemoryOf, projectMemoryFor, appendProjectMemory, setProjectDigest, deleteProject,
})
}
diff --git a/core/test/project-roles.test.js b/core/test/project-roles.test.js
new file mode 100644
index 000000000..470d682ec
--- /dev/null
+++ b/core/test/project-roles.test.js
@@ -0,0 +1,47 @@
+// #1828: a Project's roles. The record stores only maintainers and members; the Owner is derived
+// from `ownerUserIds`, so a Project written before this slice is owner-only without a migration.
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { changeRole, roleChoices, roleOf, rolesOf } from '../src/host/project-board.js'
+
+const project = (extra = {}) => ({ id: 'project-1', name: 'P', ownerUserIds: ['owner'], ...extra })
+
+test('a Project written before roles derives to owner-only (#1828)', () => {
+ const old = project()
+ assert.deepEqual(rolesOf(old), { owner: ['owner'], maintainers: [], members: [] })
+ assert.equal(roleOf(old, 'owner'), 'owner')
+ assert.equal(roleOf(old, 'someone'), null)
+ assert.deepEqual(roleChoices(old, 'owner'), [])
+})
+
+test('a member cannot promote themselves and the roles stay put (#1828)', () => {
+ const p = project({ roles: { maintainers: [], members: ['m'] } })
+ const before = rolesOf(p)
+ assert.deepEqual(changeRole(p, { actorId: 'm', targetId: 'm', role: 'maintainer' }), { ok: false, reason: 'cannot-self-promote' })
+ assert.deepEqual(changeRole(p, { actorId: 'm', targetId: 'm', role: 'owner' }), { ok: false, reason: 'owner-not-assignable' })
+ assert.deepEqual(rolesOf(p), before)
+ assert.deepEqual(roleChoices(p, 'm'), [])
+})
+
+test('a member has no switchable role and cannot promote themselves (#1828)', () => {
+ const p = project({ roles: { maintainers: [], members: ['b'] } })
+ assert.deepEqual(roleChoices(p, 'b'), [])
+ assert.deepEqual(changeRole(p, { actorId: 'b', targetId: 'b', role: 'maintainer' }), { ok: false, reason: 'cannot-self-promote' })
+ assert.deepEqual(changeRole(p, { actorId: 'b', targetId: 'c', role: 'member' }), { ok: false, reason: 'not-owner' })
+ assert.deepEqual(rolesOf(p), { owner: ['owner'], maintainers: [], members: ['b'] })
+})
+
+test('an owner demotes another member to Member (#1828)', () => {
+ const p = project({ roles: { maintainers: ['m'], members: ['n'] } })
+ const done = changeRole(p, { actorId: 'owner', targetId: 'm', role: 'member' })
+ assert.deepEqual(done, { ok: true, roles: { maintainers: [], members: ['n', 'm'] } })
+ assert.deepEqual(roleChoices(p, 'owner', 'm'), ['member'], 'the current role is not offered')
+ assert.deepEqual(roleChoices(p, 'owner', 'n'), ['maintainer'])
+})
+
+test('the last owner cannot be demoted, by themselves or by anyone else (#1828)', () => {
+ const p = project()
+ assert.deepEqual(changeRole(p, { actorId: 'owner', targetId: 'owner', role: 'member' }), { ok: false, reason: 'last-owner' })
+ assert.deepEqual(changeRole(p, { actorId: 'owner', targetId: 'owner', role: 'owner' }), { ok: false, reason: 'owner-not-assignable' })
+ assert.deepEqual(rolesOf(p), { owner: ['owner'], maintainers: [], members: [] })
+})
diff --git a/core/test/projects-nav.test.js b/core/test/projects-nav.test.js
new file mode 100644
index 000000000..53397e5b8
--- /dev/null
+++ b/core/test/projects-nav.test.js
@@ -0,0 +1,118 @@
+// #1828: Projects in the sidebar. The list is the host's answer — a Project's role and the ranks
+// the caller may switch to both come from the endpoint's own policy — so these tests draw what the
+// host said and never guess a rank.
+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/projects-nav.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: () => 0, clearTimeout: () => {}, Date, Promise, console,
+ window: { addEventListener: () => {}, removeEventListener: () => {}, location: { hash: '' } },
+ document: { body: {} },
+ require: (name) => {
+ const platform = {
+ react: {
+ createElement: (type, props, ...children) => ({ type, props: props ?? {}, children: children.flat(Infinity) }),
+ useState: initial => [typeof initial === 'function' ? initial() : initial, () => {}],
+ useEffect: () => {}, useLayoutEffect: () => {}, useRef: current => ({ current }), useCallback: fn => fn, useMemo: fn => fn(), Fragment: 'fragment',
+ },
+ 'react-dom': { createPortal: element => element },
+ }
+ if (!(name in platform)) throw new Error(`no platform module ${name}`)
+ return platform[name]
+ },
+})
+const { ProjectList, ProjectRoleMenu, ProjectsNav, projectHash, projectMenuItems, projectName, roleNote, roleText } = module.exports
+
+const textOf = node => (node?.children ?? []).flat(Infinity).filter(child => typeof child === 'string').join('')
+const buttons = node => (node?.children ?? []).flat(Infinity).filter(child => child && child.type === 'button')
+const find = (node, className) => {
+ if (node === null || typeof node !== 'object') return null
+ if (String(node.props?.className ?? '').split(' ').includes(className)) return node
+ for (const child of node.children ?? []) {
+ const hit = find(child, className)
+ if (hit) return hit
+ }
+ return null
+}
+const project = (extra = {}) => ({ id: 'p1', name: 'Alpha', role: 'member', choices: [], ...extra })
+// Values built inside the vm carry that realm's prototypes; compare the data, not the origin.
+const plain = value => JSON.parse(JSON.stringify(value))
+
+test('the list draws each Project with the role the host answered (#1828)', () => {
+ const view = ProjectList({ projects: [project(), project({ id: 'p2', name: '', role: 'owner' })], error: '', note: '' })
+ const rows = buttons(view)
+ assert.equal(rows.length, 2)
+ assert.deepEqual(rows.map(row => row.props['data-project-id']), ['p1', 'p2'])
+ const roles = rows.map(row => (find(row, 'bt-project-entry-role') ?? {}).children?.join(''))
+ assert.deepEqual(roles, ['Member', 'Owner'])
+ // A Project with no name reads by id, never blank.
+ assert.equal(projectName({ id: 'x9' }), '#x9')
+})
+
+test('an empty list and an unreadable one say so instead of drawing nothing (#1828)', () => {
+ assert.equal(textOf(find(ProjectList({ projects: [], error: '', note: '' }), 'bt-projects-empty')), 'No Projects yet')
+ assert.equal(textOf(find(ProjectList({ projects: null, error: '', note: '' }), 'bt-projects-note')), 'Reading Projects…')
+ assert.equal(textOf(find(ProjectList({ projects: [], error: 'No permission', note: '' }), 'bt-projects-note')), 'No permission')
+})
+
+test('a row opens its board by id, and the hash is the board entry point (#1828)', () => {
+ const opened = []
+ const view = ProjectList({ projects: [project()], error: '', note: '', onOpen: id => opened.push(id) })
+ buttons(view)[0].props.onClick({})
+ assert.deepEqual(opened, ['p1'])
+ assert.equal(projectHash('p 1'), '#project=p%201')
+})
+
+test('the menu lists only switchable roles and marks the current one (#1828)', () => {
+ // A Member sees only the rank they hold: nothing to pick.
+ const member = plain(projectMenuItems(project()))
+ assert.deepEqual(member, [{ role: 'member', label: 'Member', current: true, pick: false }])
+ // An Owner cannot demote themselves, so the menu is the current item alone.
+ assert.deepEqual(plain(projectMenuItems(project({ role: 'owner', choices: [] }))), [{ role: 'owner', label: 'Owner', current: true, pick: false }])
+ // A Maintainer may drop to Member, and the current rank is not repeated.
+ assert.deepEqual(plain(projectMenuItems(project({ role: 'maintainer', choices: ['member'] }))), [
+ { role: 'maintainer', label: 'Maintainer', current: true, pick: false },
+ { role: 'member', label: 'Member', current: false, pick: true },
+ ])
+ // A record written before roles existed carries none, and the menu does not invent owner-only.
+ assert.deepEqual(plain(projectMenuItems({ id: 'old', role: null, choices: [] })), [])
+ assert.equal(roleText(null), '')
+})
+
+test('the menu draws the current rank checked and disabled, and picking calls back (#1828)', () => {
+ const picked = []
+ const view = ProjectRoleMenu({
+ project: project({ id: 'p3', role: 'maintainer', choices: ['member'], name: 'Beta' }),
+ x: 10, y: 20, onPick: role => picked.push(role), onClose: () => {},
+ })
+ const items = (view.children ?? []).flat(Infinity).filter(child => child && child.type === 'button')
+ assert.equal(items.length, 2)
+ assert.equal(items[0].props['aria-checked'], 'true')
+ assert.equal(items[0].props.disabled, true)
+ assert.equal(items[1].props['aria-checked'], 'false')
+ items[1].props.onClick()
+ assert.deepEqual(picked, ['member'])
+})
+
+test('a refusal is quoted with the host reason, and a switch says so (#1828)', () => {
+ assert.equal(roleNote({ changed: false, reason: 'last-owner' }), 'Role unchanged: last-owner')
+ assert.equal(roleNote({ changed: false, reason: 'cannot-self-promote' }), 'Role unchanged: cannot-self-promote')
+ assert.equal(roleNote({ changed: true }), 'Role changed.')
+})
+
+test('the sidebar row carries the expand arrow and reports its state (#1828)', () => {
+ const row = ProjectsNav({ wide: true })
+ const button = buttons(row)[0]
+ assert.equal(button.props['aria-expanded'], 'false')
+ assert.ok(find(row, 'bt-projects-chevron'), 'the expand arrow is drawn')
+ assert.equal(textOf(find(row, 'bt-projects-label')), 'Projects')
+})
diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md
index 0d459cd54..d14dd2c69 100644
--- a/docs/subsystems/core-client.md
+++ b/docs/subsystems/core-client.md
@@ -292,7 +292,17 @@ The pane is registered on the existing `shell.overlay` surface and opens at `#pr
re-reads the hash on `hashchange`, so an entry that writes it while the overlay is mounted still opens
the board. The entry is the **Project board** button in `KanbanDialog`'s header: it is drawn only when the
selected Bot or group carries a `projectId`, writes `#project=` and closes the Tasks dialog, leaving
-the overlay to take over. It is not a second board and not a rail row.
+the overlay to take over. It is not a second board.
+
+Since #1828 the sidebar draws the entry: one **Projects** row below Tasks, with an expand arrow that
+lists every Project the `projects` listing answered together with the caller's role there. A row
+opens that board by writing `#project=` (the same entry point the overlay reads), the rail's
+version opens the first board, and an install with no Project reads *No Projects yet*. A
+right-click on a row opens the **My role** menu: it lists the `choices` the listing answered and
+marks the current rank (a Member has only the current one, and an Owner never demotes themselves),
+calls `project-role`, and shows a refusal's reason unchanged (`Role unchanged: last-owner`). The
+policy stays in the host; this module (`src/client/projects-nav.js`) only draws the answer. Tests:
+`core/test/projects-nav.test.js`.
A row knows the board its task lives on without re-deciding membership: the mapping follows the host's own
`source` (`group` → the task's `origin.roomId`, `bot` → its `ownerBotId`, a `workflow` row is one a run
diff --git a/docs/subsystems/core-host.md b/docs/subsystems/core-host.md
index 0b7ede25a..b027abdf7 100644
--- a/docs/subsystems/core-host.md
+++ b/docs/subsystems/core-host.md
@@ -620,6 +620,20 @@ refused there; a board with nothing to paint is still `[]`, not an error.
`project-board` is listed in `authz.js`'s `TASK_ENDPOINTS`, so a signed-in deployment does not
refuse it as an unknown endpoint.
+Projects carry three roles since #1828, and only two of them are stored. `roles: { maintainers,
+members }` on the record holds the two assignable ranks; the Owner is derived every time from
+`ownerUserIds` (plus `ownerUserId`), so a record written before this slice reads owner-only with no
+migration and the stored lists can never disagree with the owner. `rolesOf` / `roleOf` /
+`roleChoices` / `changeRole` in `project-board.js` are the whole policy: nobody promotes themselves,
+only an Owner re-roles someone else, the Owner rank is never assignable here, and the last owner
+cannot be demoted. `changeRole` answers `{ ok: true, roles }` or a machine-readable reason
+(`not-owner`, `cannot-self-promote`, `last-owner`, `unknown-role`, `owner-not-assignable`) and the
+record is untouched on a refusal. The `project-role` endpoint runs that function, persists only
+`maintainers`/`members` (`setProjectRoles`), and answers `{ project, changed, reason }`; the
+`projects` listing now also carries each entry's `role` and `choices` for the caller, both read
+from the same functions, so the sidebar menu cannot offer a switch the host would refuse. Tests:
+`core/test/project-roles.test.js`.
+
### Role queues, isolated workers and lease recovery (#1729)
Stage 3.2 of the task pool. One queue per role, one worker per claim, one owner for an expired lease; all of it reads the cards already on the board, so there is no second store.
diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch
index 70aa7019a..d0d86173a 100644
--- a/patches/ds-bot/0001-core-fixes.patch
+++ b/patches/ds-bot/0001-core-fixes.patch
@@ -639,7 +639,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
// Shared
'Main Bot': '主 Bot',
Working: '工作中',
-@@ -12,6 +12,67 @@ const ZH = {
+@@ -12,6 +12,67 @@
'Unread activity': '有未读',
Search: '搜索',
'New chat': '新对话',
@@ -707,7 +707,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'New…': '新建',
'Animation': '动画',
'Bot animation': 'Bot 动画',
-@@ -26,6 +26,21 @@ const ZH = {
+@@ -26,6 +26,21 @@
Duplicate: '复制一份',
Hidden: '已隐藏',
Admin: '管理员',
@@ -729,7 +729,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Add: '添加',
Close: '关闭',
Copy: '复制',
-@@ -41,22 +41,73 @@ const ZH = {
+@@ -41,22 +41,73 @@
' and ': '、',
and: '和',
'{count} members': '{count} 位成员',
@@ -806,7 +806,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Connectors: '连接器',
'Search connectors': '搜索连接器',
'No connectors': '暂无连接器',
-@@ -93,6 +93,59 @@ const ZH = {
+@@ -93,6 +93,59 @@
'Connecting…': '正在连接…',
'Could not connect: {error}': '连接失败:{error}',
'Connected as {account}': '已连接:{account}',
@@ -866,7 +866,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Connected: '已连接',
Disconnect: '断开',
Connect: '连接',
-@@ -105,17 +105,64 @@ const ZH = {
+@@ -105,17 +105,64 @@
'GitHub addresses must start with https://': 'GitHub 地址必须以 https:// 开头',
'This DSH cannot load connectors': '这个 DSH 不能加载连接器',
'No such connector': '没有这个连接器',
@@ -935,7 +935,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
// Header
'Message {name}': '给 {name} 发消息',
-@@ -128,8 +128,6 @@ const ZH = {
+@@ -128,8 +128,6 @@
New: '新',
// New chat
@@ -944,7 +944,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Add to group chat': '加入群聊',
'Message Bot': '给 Bot 发消息',
'To:': '发给:',
-@@ -154,9 +154,24 @@ const ZH = {
+@@ -154,9 +154,24 @@
Everyone: '所有人',
'Every member answers in turn, admin first.': '每位成员轮流回答,管理员先说。',
'Members · {count}': '成员 · {count}',
@@ -969,7 +969,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Remove {name} from the group': '把 {name} 移出群聊',
'Remove from group': '移出群聊',
'Add member': '添加成员',
-@@ -172,6 +172,16 @@ const ZH = {
+@@ -172,6 +172,16 @@
'Close details': '关闭详情',
'Copied conversation': '已复制对话',
'Share conversation': '分享对话',
@@ -986,7 +986,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Copy conversation as Markdown': '把对话复制为 Markdown',
'This conversation is not part of your Bot team.': '这个对话不属于你的 Bot 团队。',
'Admin {name}': '管理员 {name}',
-@@ -215,6 +215,25 @@ const ZH = {
+@@ -215,6 +215,25 @@
'This model is not available now. Pick another one.': '这个模型现在不可用,请换一个。',
'Every message of this Bot runs on it.': '这个 Bot 的每条消息都用它。',
'Applies from the next message.': '从下一条消息开始生效。',
@@ -1012,7 +1012,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Profile: '资料',
Label: '标签',
'Optional. Shown under the name.': '可选,显示在名字下方。',
-@@ -238,6 +238,7 @@ const ZH = {
+@@ -238,6 +238,7 @@
'Kept above the other chats.': '排在其他对话上面。',
'Search still finds it.': '仍然可以搜索到。',
Manage: '管理',
@@ -1020,7 +1020,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
// Group chat settings
'Leave it empty to name the group after its members.': '留空就按成员命名。',
-@@ -274,6 +274,8 @@ const ZH = {
+@@ -274,6 +274,8 @@
'Harness default': 'Harness 默认',
Saving: '保存中',
'Manage models': '管理模型',
@@ -1029,7 +1029,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
// Shapes
Whale: '鲸鱼', Star: '星星', Moon: '月亮', Wave: '声波', Squircle: '圆角方', Bull: '牛角', Blossom: '花朵',
-@@ -312,9 +312,10 @@ const ZH = {
+@@ -312,9 +312,10 @@
'No group chats yet.': '还没有群聊。',
'A group chat lets several Bots work on one thing with you.': '在群聊里,几个 Bot 和你一起做一件事。',
'Back to group chats': '返回群聊列表',
@@ -1042,7 +1042,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Harness settings': 'Harness 设置',
'Back to Bots': '返回 Bot 列表',
'Close settings': '关闭设置',
-@@ -324,7 +324,6 @@ const ZH = {
+@@ -324,7 +324,6 @@
Pin: '置顶',
Pinned: '已置顶',
'Mark as Unread': '标为未读',
@@ -1050,7 +1050,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Group settings': '群设置',
'Rename group': '重命名群聊',
'Copy conversation ID': '复制对话 ID',
-@@ -333,6 +333,8 @@ const ZH = {
+@@ -333,6 +333,8 @@
// The DSH Agent's own row and Session list. "DSH Agent" is a product name and
// stays in English.
'Add DSH Agent': '添加 DSH Agent',
@@ -1059,7 +1059,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Remove DSH Agent': '移除 DSH Agent',
'New session': '新会话',
Archive: '归档',
-@@ -344,13 +344,13 @@ const ZH = {
+@@ -344,13 +344,13 @@
'{n} d': '{n} 天',
// Plugin updates
@@ -1079,7 +1079,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,78 @@ const ZH = {
+@@ -360,9 +360,78 @@
'DSH update': 'DSH 更新',
'DSH {version}': 'DSH {version}',
'Restart DSH to start using DSH {version}.': '重启 DSH 后开始使用 DSH {version}。',
@@ -1159,7 +1159,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
// Command palette
Messages: '消息',
Commands: '命令',
-@@ -390,10 +390,13 @@ const ZH = {
+@@ -390,10 +390,13 @@
Reply: '回复',
'More message actions': '更多操作',
More: '更多',
@@ -1173,7 +1173,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Earlier messages': '更早的消息',
'Loading earlier messages…': '正在加载更早的消息…',
'Couldn’t load earlier messages · Retry': '更早的消息没加载出来 · 重试',
-@@ -403,6 +403,9 @@ const ZH = {
+@@ -403,6 +403,9 @@
'Replies from': '群聊回复:',
'Reply from': '收到回复:',
'Message from': '收到消息:',
@@ -1183,7 +1183,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Stopped by': '叫停者:',
'{count} messages with': '{count} 条消息,来自',
'Routine started': '例行任务开始',
-@@ -412,6 +412,8 @@ const ZH = {
+@@ -412,6 +412,8 @@
'Working out a plan': '理一下思路',
'Sorting out the details': '梳理细节',
'On it': '在做了',
@@ -1192,7 +1192,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Busy with it': '正在处理',
'Moving ahead': '推进中',
'Taking care of it': '处理中',
-@@ -439,7 +439,6 @@ const ZH = {
+@@ -439,7 +439,6 @@
'Looking back through the chat': '翻看聊天记录',
'{name} looked up its own chat': '{name} 查看了自己的对话',
'{name} looked up its own chat for “{text}”': '{name} 在自己的对话里查找「{text}」',
@@ -1200,7 +1200,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
"{name} didn't answer in time": '{name} 没有及时回应',
'{name} paused for your approval': '{name} 停下来等你批准',
'{name} started on it': '{name} 开始做了',
-@@ -450,12 +450,31 @@ const ZH = {
+@@ -450,12 +450,31 @@
'Asked to make the tool call it wrote as text': '工具调用被写成了文字,已让它重新调用',
'A Bot': '一个 Bot',
'{name} is waiting for your approval': '{name} 等你批准',
@@ -1233,7 +1233,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
"Couldn't message {name}": '没能给 {name} 发消息',
Messaged: '已发消息给',
"Couldn't stop {name}": '没能叫停 {name}',
-@@ -478,6 +478,7 @@ const ZH = {
+@@ -478,6 +478,7 @@
Selected: '已选',
Skip: '跳过',
Submit: '提交',
@@ -1241,7 +1241,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Choose all that apply': '可多选',
'Dismiss question': '关闭问题',
Dismiss: '关闭',
-@@ -485,7 +485,7 @@ const ZH = {
+@@ -485,7 +485,7 @@
'Your answer': '你的回答',
// Secrets
@@ -1250,7 +1250,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'No Bot': '没有 Bot',
'Paste or type the key': '粘贴或输入密钥',
'Only {name}': '只给 {name}',
-@@ -501,7 +501,6 @@ const ZH = {
+@@ -501,7 +501,6 @@
'At least {count} characters.': '至少 {count} 个字符。',
'Replace value': '替换值',
'Saving…': '保存中…',
@@ -1258,7 +1258,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Allowing…': '允许中…',
Allow: '允许',
'{bot} asked for {name}': '{bot} 请求了 {name}',
-@@ -528,7 +528,7 @@ const ZH = {
+@@ -528,7 +528,7 @@
'NAME, e.g. GITHUB_TOKEN': '名字,例如 GITHUB_TOKEN',
'What it is for': '用途',
Value: '值',
@@ -1267,7 +1267,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Save key': '保存密钥',
'Keys your Bots asked for. Bots never see the values: they use them as ': '你的 Bot 请求过的密钥。Bot 永远看不到值:它们在 shell 里通过 ',
' in their shell, and every value is replaced by ': ' 使用,而且发给模型之前,每个值都会被替换成 ',
-@@ -568,7 +568,6 @@ const ZH = {
+@@ -568,7 +568,6 @@
Remove: '删除',
'Ask or update': '询问或更新',
"Ask about or update {name}'s memory": '询问或更新 {name} 的记忆',
@@ -1275,7 +1275,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Added: {text}': '已添加:{text}',
'Added to team memory: {text}': '已加入团队记忆:{text}',
'Updated: {text}': '已更新:{text}',
-@@ -697,11 +697,11 @@ const ZH = {
+@@ -697,11 +697,11 @@
'Compared with the {count} days before': '与再往前 {count} 天相比',
'{share}% of all usage from {span}': '占 {span} 全部用量的 {share}%',
// About
@@ -1290,7 +1290,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Updates: '更新',
'Check for updates': '检查更新',
'Checking…': '正在检查…',
-@@ -711,6 +711,13 @@ const ZH = {
+@@ -711,6 +711,13 @@
'Update checks are off.': '已关闭更新检查。',
'Installed from a local folder; update it there.': '从本地文件夹安装,请在那里更新。',
'Updates are not offered for this install.': '这种安装方式不提供更新。',
@@ -1304,7 +1304,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
Links: '链接',
'Source code': '源代码',
'Release notes': '发布说明',
-@@ -726,7 +726,7 @@ const ZH = {
+@@ -726,7 +726,7 @@
Show: '查看',
Hide: '收起',
'GitHub issue': 'GitHub issue',
@@ -1313,7 +1313,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Report on GitHub': '在 GitHub 上报告',
'Copied the diagnostics.': '已复制诊断信息。',
'Could not copy. Select the text below and copy it.': '无法复制。请选中下面的文字手动复制。',
-@@ -738,7 +738,7 @@ const ZH = {
+@@ -738,7 +738,7 @@
'Recent errors': '最近的错误',
'Recent errors · {count}': '最近的错误 · {count}',
'No errors recorded.': '没有错误记录。',
@@ -1322,7 +1322,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Model call': '模型调用',
'This window': '这个窗口',
'Error log': '错误日志',
-@@ -771,12 +771,21 @@ const ZH = {
+@@ -771,12 +771,21 @@
'Add at least one more Bot': '再至少添加一个 Bot',
Members: '成员',
// Connectors and scheduled tasks
@@ -1344,7 +1344,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Once, {time}': '一次,{time}',
'Every day at {time}': '每天 {time}',
'Every {days} at {time}': '每{days} {time}',
-@@ -784,7 +784,21 @@ const ZH = {
+@@ -784,7 +784,21 @@
'Every weekend at {time}': '每个周末 {time}',
'On the schedule {expression}': '按计划 {expression}',
'Next: {time}': '下次 {time}',
@@ -1366,7 +1366,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'in {name}': '在 {name}',
'Click again to delete': '再点一次删除',
'Delete task': '删除任务',
-@@ -794,6 +794,7 @@ const ZH = {
+@@ -794,6 +794,7 @@
minutes: '分钟',
hours: '小时',
days: '天',
@@ -1374,7 +1374,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Morning briefing': '早间简报',
'What to do': '要做什么',
'Sum up what changed since yesterday and what needs me today.': '总结一下昨天以来的变化,以及今天需要我处理的事。',
-@@ -806,9 +806,20 @@ const ZH = {
+@@ -806,9 +806,20 @@
Unit: '单位',
'Create task': '创建任务',
'Turn on Automation tasks in DSH': '在 DSH 里打开「自动化任务」',
@@ -1397,7 +1397,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'No scheduled tasks yet.': '还没有定时任务。',
'No scheduled tasks yet. Add one': '还没有定时任务,去添加',
'Automation tasks are off in DSH': 'DSH 的「自动化任务」没有打开',
-@@ -819,10 +819,19 @@ const ZH = {
+@@ -819,10 +819,19 @@
'Pick a date and time': '选一个日期和时间',
'Pick a time in the future': '选一个将来的时间',
'Pick a time of day': '选一个时间',
@@ -1417,7 +1417,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Working folder': '工作文件夹',
'Working folder: {folder}': '工作文件夹:{folder}',
Folder: '文件夹',
-@@ -838,6 +838,50 @@ const ZH = {
+@@ -838,6 +838,60 @@
'Up one level': '上一级',
'No folders inside.': '里面没有文件夹。',
'Some folders are not shown.': '还有一些文件夹没有显示。',
@@ -1439,6 +1439,16 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
+ 'e.g. Website': '例如:官网',
+ "Optional. Where this Project's work happens.": '可选。这个项目的工作目录。',
+ 'Projects · {count}': '项目 · {count}',
++ // #1828: a Project's roles, and the sidebar row that lists them.
++ Maintainer: '维护者',
++ Member: '成员',
++ 'No Projects yet': '还没有项目',
++ 'Reading Projects…': '正在读取项目…',
++ 'My role': '我的角色',
++ 'No role to switch here.': '这里没有可切换的角色。',
++ 'Role changed.': '角色已更改。',
++ 'Role unchanged: {reason}': '角色未更改:{reason}',
++ refused: '被拒绝',
+ 'No Project yet — create one.': '还没有项目——新建一个。',
+ 'Bind a Bot or group': '绑定 Bot 或群聊',
+ 'Bind to {name}': '绑定到 {name}',
@@ -1468,7 +1478,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
}
// Host errors stay English, since Bots read them too; they are translated where the
-@@ -853,8 +853,11 @@ const HOST_ERRORS = [
+@@ -853,8 +853,11 @@
[/^Only the user, a Main Bot, or the admin of "(.+)" can change it$/, '只有你、主 Bot 或「$1」的管理员可以修改'],
[/^A key must have at least (\d+) characters$/, '密钥至少 $1 个字符'],
[/^A key can have at most (\d+) characters$/, '密钥最多 $1 个字符'],
@@ -1480,7 +1490,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
[/^HTTP (\d+)$/, '请求失败(HTTP $1)'],
[/^GitHub answered (\d+)$/, 'GitHub 返回了 $1'],
[/^Keep the name under (\d+) characters$/, '名字不能超过 $1 个字'],
-@@ -872,6 +872,13 @@ const HOST_ERRORS = [
+@@ -872,6 +872,13 @@
[/^No entry in [\s\S]* matches "([\s\S]*)"\.[\s\S]*$/, '找不到「$1」这条记忆'],
[/^"([\s\S]*)" matches (\d+) entries in [\s\S]*$/, '「$1」对应了 $2 条记忆,请说得更具体'],
[/^Memory cannot change now: [\s\S]*$/, '现在不能修改记忆:另一个 dsh 正在使用这个 Bot 团队'],
@@ -1494,7 +1504,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
]
const HOST_ZH = {
'Request failed': '请求失败',
-@@ -883,6 +883,20 @@ const HOST_ZH = {
+@@ -883,6 +883,20 @@
'Unknown Bot': '找不到这个 Bot',
'Unknown conversation': '找不到这个对话',
'A group chat needs at least two Bots': '群聊至少要两个 Bot',
@@ -1515,7 +1525,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'The admin must be a member of the group': '管理员必须是群成员',
'Unknown group chat': '找不到这个群聊',
'Hand the admin role to another member instead of leaving the group without one': '请把管理员交给其他成员,不要让群聊没有管理员',
-@@ -905,6 +905,8 @@ const HOST_ZH = {
+@@ -905,6 +905,8 @@
'Write the entry to save.': '请写下要保存的内容。',
'This DeepSeek Harness has no folder chooser': '这个 DeepSeek Harness 没有文件夹选择窗口',
'This DeepSeek Harness cannot list folders': '这个 DeepSeek Harness 不能列出文件夹',
diff --git a/patches/ds-bot/0002-flybot-api-host.patch b/patches/ds-bot/0002-flybot-api-host.patch
index 1ab643184..fbc9ab1f9 100644
--- a/patches/ds-bot/0002-flybot-api-host.patch
+++ b/patches/ds-bot/0002-flybot-api-host.patch
@@ -715,7 +715,7 @@ diff --git a/src/host/store.js b/src/host/store.js
const EMPTY = JSON.stringify(state)
// The other dsh host that holds state.lock while this one only reads (see home-lock.js);
-@@ -50,12 +50,88 @@ export function install(rt) {
+@@ -50,12 +50,88 @@
}
}
const readOnly = () => {
@@ -805,7 +805,7 @@ diff --git a/src/host/store.js b/src/host/store.js
const readFileState = async () => {
let contents
try {
-@@ -87,10 +87,32 @@ export function install(rt) {
+@@ -87,10 +87,32 @@
return undefined
}
if (parsed?.version === 1) {
@@ -821,10 +821,9 @@ diff --git a/src/host/store.js b/src/host/store.js
+ warn('ds-bot: unreadable state %s: %s', statePath, backupProblem)
+ return undefined
+ }
- stuck = undefined
- return parsed
- }
-- stuck = 'saved by a newer DS Bot; update DS Bot'
++ stuck = undefined
++ return parsed
++ }
+ if (parsed?.version === STATE_VERSION) {
+ const problem = ownershipProblem(parsed)
+ if (problem !== undefined) {
@@ -832,14 +831,15 @@ diff --git a/src/host/store.js b/src/host/store.js
+ warn('ds-bot: state %s was refused: %s', statePath, problem)
+ return undefined
+ }
-+ stuck = undefined
-+ return parsed
-+ }
+ stuck = undefined
+ return parsed
+ }
+- stuck = 'saved by a newer DS Bot; update DS Bot'
+ stuck = 'saved by a newer FlyBot; update FlyBot'
return undefined
}
-@@ -104,6 +104,20 @@ export function install(rt) {
+@@ -104,6 +104,20 @@
// Teams saved before the DSH Agent existed carry no `agents`; corrupt values reset.
if (state.agents === null || typeof state.agents !== 'object' || Array.isArray(state.agents)) state.agents = {}
let migrated = !hadCapabilities
@@ -860,7 +860,7 @@ diff --git a/src/host/store.js b/src/host/store.js
if (state.capabilities === null || typeof state.capabilities !== 'object' || Array.isArray(state.capabilities)) {
state.capabilities = { items: {} }
migrated = true
-@@ -112,6 +112,11 @@ export function install(rt) {
+@@ -112,6 +112,11 @@
migrated = true
}
for (const bot of Object.values(state.bots)) {
@@ -872,7 +872,7 @@ diff --git a/src/host/store.js b/src/host/store.js
if (!Array.isArray(bot.capabilities)) {
bot.capabilities = []
migrated = true
-@@ -120,6 +120,11 @@ export function install(rt) {
+@@ -120,6 +120,11 @@
bot.color = currentColor(bot.color)
migrated = true
}
@@ -884,7 +884,7 @@ diff --git a/src/host/store.js b/src/host/store.js
// LEGACY: Bots saved with a lab ran on that lab's default model; the model they
// last ran on becomes their own, so dropping the lab changes nothing for them.
if ('lab' in bot) {
-@@ -161,12 +161,390 @@ export function install(rt) {
+@@ -161,12 +161,423 @@
migrated = true
}
// Unnamed groups from before names followed their members may still list old names.
@@ -1147,9 +1147,42 @@ diff --git a/src/host/store.js b/src/host/store.js
+ throw projectRefusal('Only the owner of this Project may change it', 'bot/forbidden')
+ }
+
++// #1828: only the two assignable ranks are stored; the Owner stays derived from `ownerUserIds`.
++ const setProjectRoles = (project, roles) => {
++ const list = value => (Array.isArray(value) ? [...new Set(value.map(id => projectText(id, 120)).filter(id => id !== ''))] : [])
++ project.roles = { maintainers: list(roles?.maintainers), members: list(roles?.members) }
++ project.updatedAt = Math.max(project.updatedAt, Date.now())
++ return project.roles
++ }
+ const projectEndpoint = async (endpoint, body = {}, actor = null) => {
+ switch (endpoint) {
-+ case 'projects': return { projects: listProjects() }
++ // #1828: the sidebar reads one list, and each entry carries the caller's own role and the
++ // roles they may switch to — both from the same policy the endpoint applies, so the menu and
++ // the refusal cannot disagree. A host without the helpers (an older plugin instance) answers
++ // neither field and the client draws the list without a role.
++ case 'projects': {
++ const actorId = projectActorId(actor)
++ return {
++ projects: listProjects().map(project => ({
++ ...project,
++ role: rt.projectRoleOf === undefined ? null : rt.projectRoleOf(project, actorId),
++ choices: rt.projectRoleChoices === undefined ? [] : rt.projectRoleChoices(project, actorId),
++ })),
++ }
++ }
++ // #1828: the role switch. The policy lives in project-board.js (`changeRole`), so the endpoint
++ // and the menu cannot disagree; a refusal answers the reason and writes nothing.
++ case 'project-role': {
++ const project = readProject(projectText(body.id, 200))
++ if (project === null) throw projectRefusal('Unknown Project', 'bot/not-found')
++ const result = rt.projectRole === undefined
++ ? { ok: false, reason: 'not-owner' }
++ : rt.projectRole(project, { actor, actorId: projectActorId(actor), targetId: body.memberId, role: body.role })
++ if (result.ok !== true) return { project, changed: false, reason: result.reason }
++ const roles = setProjectRoles(project, result.roles)
++ await save()
++ return { project, changed: true, roles }
++ }
+ case 'project-create': {
+ // The caller is stamped as the owner; a member id from the body cannot mint a
+ // Project someone else manages.
@@ -1276,7 +1309,7 @@ diff --git a/src/host/store.js b/src/host/store.js
const load = () => loaded ??= (async () => {
claimLock()
if (holder !== undefined) warn('ds-bot: %s', readOnly())
-@@ -185,14 +185,24 @@ export function install(rt) {
+@@ -185,14 +185,24 @@
for (const room of stale) void rt.retitleRoom(room)
})()
@@ -1302,7 +1335,7 @@ diff --git a/src/host/store.js b/src/host/store.js
warnedReadOnly = false
adopt(await readFileState())
return
-@@ -235,11 +235,37 @@ export function install(rt) {
+@@ -235,11 +235,37 @@
return warnReadOnly()
}
@@ -1340,7 +1373,7 @@ diff --git a/src/host/store.js b/src/host/store.js
state.revision += 1
state.exchanges = state.exchanges.slice(-MAX_EXCHANGES)
const snapshot = JSON.stringify(state, null, 2)
-@@ -260,9 +260,10 @@ export function install(rt) {
+@@ -260,9 +260,10 @@
ensureLock()
await rename(temp, statePath)
seen = stamp()
@@ -1352,7 +1385,7 @@ diff --git a/src/host/store.js b/src/host/store.js
throw error
}
})
-@@ -273,13 +273,37 @@ export function install(rt) {
+@@ -273,13 +273,37 @@
})
}
@@ -1392,7 +1425,7 @@ diff --git a/src/host/store.js b/src/host/store.js
const isMain = id => id !== undefined && state.mainBotIds.includes(id)
// Group Sessions. A member speaks in each group from a hidden Session of its own,
-@@ -313,6 +313,13 @@ export function install(rt) {
+@@ -313,6 +313,13 @@
}
for (const [botId, sessionId] of Object.entries(room.sessions ?? {})) groupOwners.set(sessionId, { botId, roomId: room.id })
}
@@ -1406,7 +1439,7 @@ diff --git a/src/host/store.js b/src/host/store.js
// The agents module owns the DSH Agent session index; it installs after this.
rt.indexAgents?.()
}
-@@ -328,6 +328,8 @@ export function install(rt) {
+@@ -328,6 +328,8 @@
delete room.sessions[botId]
if (room.segments !== undefined) delete room.segments[botId]
groupOwners.delete(sessionId)
@@ -1415,7 +1448,7 @@ diff --git a/src/host/store.js b/src/host/store.js
rt.cancelApprovals?.(sessionId)
// A plain archive refuses a running Session, which would go on working for a group
// that no longer has it.
-@@ -335,23 +335,29 @@ export function install(rt) {
+@@ -335,23 +335,29 @@
}
const findBot = (ref) => {
if (ref === undefined) return undefined
@@ -1445,7 +1478,7 @@ diff --git a/src/host/store.js b/src/host/store.js
chatOwners, groupOwners, pastOwners,
groupSessionsOf, dropGroupSession, findBot, findRooms, isAdminOf,
+ initialOwnerId, syncDesignatedChief,
-+ projectEndpoint, createProject, readProject, listProjects, bindProject, projectOfMember,
++ projectEndpoint, createProject, readProject, listProjects, bindProject, projectOfMember, setProjectRoles,
+ projectMemory, projectMemoryOf, projectMemoryFor, appendProjectMemory, setProjectDigest, deleteProject,
})
}
diff --git a/patches/ds-bot/0003-flybot-api-client.patch b/patches/ds-bot/0003-flybot-api-client.patch
index a60a23b74..e95706f0b 100644
--- a/patches/ds-bot/0003-flybot-api-client.patch
+++ b/patches/ds-bot/0003-flybot-api-client.patch
@@ -1352,7 +1352,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,70 @@
+@@ -1,44 +1,71 @@
+import { FLYBOT_CSS, FlyBotApprovals, FlyBotSessionDock, createClientApi as createFlyBotClient } from './flybot-slots.js'
+import { createElement as h } from 'react'
+import { registry as flybotSlots } from './flybot-registry.js'
@@ -1401,6 +1401,7 @@ diff --git a/src/client/index.js b/src/client/index.js
+import { MOBILE_CSS } from './mobile.js'
+import { PANE_MIN_WIDTH_CSS, watchShellLevel } from './panes.js'
+import { PROJECT_BOARD_CSS, ProjectBoardOverlay } from './project-board.js'
++import { PROJECTS_NAV_CSS } from './projects-nav.js'
import { STORAGE, migrateStorage } from './storage.js'
+import { friendLineNews } from './friend-lines.js'
import { groupsFor, readMode, surfaceGroups, writeMode } from './surface.js'
@@ -1432,7 +1433,7 @@ diff --git a/src/client/index.js b/src/client/index.js
migrateStorage()
const UNREAD_KEY = STORAGE.unread
let savedUnread = {}
-@@ -50,9 +50,12 @@ export function apply(ctx) {
+@@ -50,9 +50,12 @@
const SEEN_KEY = STORAGE.seen
let seenAt = {}
try { seenAt = JSON.parse(localStorage.getItem(SEEN_KEY) ?? '{}') ?? {} } catch { seenAt = {} }
@@ -1446,7 +1447,7 @@ diff --git a/src/client/index.js b/src/client/index.js
// Bot mode paints the whole team surface; Agent mode withdraws every slot and
// style so the shell's own interface returns. `plain` stays false until a Bot-mode
// view hosts a plain session, which drops only the conversation replacements.
-@@ -90,6 +90,8 @@ export function apply(ctx) {
+@@ -90,6 +90,8 @@
return () => window.removeEventListener('pagehide', onPageHide)
}, 'ds-bot: remember visits on page hide')
const activity = createSource({})
@@ -1455,7 +1456,7 @@ diff --git a/src/client/index.js b/src/client/index.js
// Per Bot, the turn it runs and its tool; and the approvals waiting for the user.
const work = createSource({ bots: {}, approvals: [] })
let workKey = ''
-@@ -125,6 +125,17 @@ export function apply(ctx) {
+@@ -125,6 +125,17 @@
try { localStorage.setItem(UNREAD_KEY, JSON.stringify(unread)) } catch { /* storage unavailable */ }
return { ...state, unread }
})
@@ -1473,7 +1474,7 @@ diff --git a/src/client/index.js b/src/client/index.js
const refreshed = new Set()
// Pulls can overlap (the poll and a navigation); an older answer never wins.
let pulls = 0
-@@ -132,11 +132,14 @@ export function apply(ctx) {
+@@ -132,11 +132,14 @@
const pull = async () => {
const order = ++pulls
const known = roster.getSnapshot()
@@ -1489,7 +1490,7 @@ diff --git a/src/client/index.js b/src/client/index.js
const nextWork = JSON.stringify(value.work ?? null)
if (nextWork !== workKey) { workKey = nextWork; work.set(value.work ?? { bots: {}, approvals: [] }) }
noteMemoryNews(value.memoryNews ?? {})
-@@ -145,11 +145,14 @@ export function apply(ctx) {
+@@ -145,11 +145,14 @@
const live = {}
if (value.connectors !== undefined && JSON.stringify(value.connectors) !== JSON.stringify(current.connectors ?? [])) live.connectors = value.connectors
if (value.capabilityStatuses !== undefined && JSON.stringify(value.capabilityStatuses) !== JSON.stringify(current.capabilityStatuses ?? {})) live.capabilityStatuses = value.capabilityStatuses
@@ -1504,7 +1505,7 @@ diff --git a/src/client/index.js b/src/client/index.js
try {
const ids = (value.agents ?? []).flatMap(agent => agent.sessions ?? [])
localStorage.setItem(STORAGE.agentSessions, JSON.stringify(ids))
-@@ -252,17 +252,7 @@ export function apply(ctx) {
+@@ -252,17 +252,7 @@
const style = installStyles('', 'mentions')
let last = ''
const render = () => {
@@ -1523,7 +1524,7 @@ diff --git a/src/client/index.js b/src/client/index.js
if (next !== last) style.textContent = last = next
}
render()
-@@ -325,7 +325,7 @@ export function apply(ctx) {
+@@ -325,7 +325,7 @@
ctx.inject(['shortcuts'], scope => { scope.effect(() => holdShellShortcuts(scope.shortcuts), 'ds-bot: settings shortcut') })
@@ -1532,7 +1533,7 @@ diff --git a/src/client/index.js b/src/client/index.js
// A Bot id, or any part of its chat, opens the Bot's current part.
const partFor = (id) => {
const bot = roster.getSnapshot().byId[id]
-@@ -342,6 +342,7 @@ export function apply(ctx) {
+@@ -342,6 +342,7 @@
// or the click appears to do nothing.
isAgentSession: id => isAgentSessionId(id),
openSession: (id) => {
@@ -1540,7 +1541,7 @@ diff --git a/src/client/index.js b/src/client/index.js
// A DSH Agent Session id lands on the plain surface, not the Bot skin.
if (isAgentSessionId(id)) return actions.openAgentSession(id)
ui.set(state => (state.newChat || state.exchange || state.menu || state.palette ? { ...state, newChat: null, exchange: null, menu: null, palette: null } : state))
-@@ -409,11 +409,11 @@ export function apply(ctx) {
+@@ -409,11 +409,11 @@
},
// `last` is the Bot conversation the view held before it cleared: when that part
// was archived for a fresh one, the view goes on in the fresh one.
@@ -1556,7 +1557,7 @@ diff --git a/src/client/index.js b/src/client/index.js
expected = target
ctx.uiWorkspace.openSession(target)
},
-@@ -444,10 +444,22 @@ export function apply(ctx) {
+@@ -444,10 +444,22 @@
// Opening the conversation would move focus to its composer and blur the name field.
renameBot: (id) => { ui.set(state => ({ ...state, details: id, view: 'details', renaming: true, tab: null })) },
// `id` opens one Bot (page 'bots') or one group chat (page 'groups').
@@ -1581,7 +1582,7 @@ diff --git a/src/client/index.js b/src/client/index.js
openConnectors: () => { ui.set(state => ({ ...state, settings: { page: 'connectors', id: null }, menu: null, palette: null, newChat: null })) },
// Agent mode hands the surface back to the shell; everything open on the Bot
// surface closes first so nothing stale reopens on the way back.
-@@ -455,7 +455,7 @@ export function apply(ctx) {
+@@ -455,7 +455,7 @@
if (mode !== 'bot' && mode !== 'agent') return
if (surface.getSnapshot().mode === mode) return
if (mode === 'agent') {
@@ -1590,7 +1591,7 @@ diff --git a/src/client/index.js b/src/client/index.js
}
writeMode(mode)
surface.set(state => ({ ...state, mode }))
-@@ -487,6 +487,12 @@ export function apply(ctx) {
+@@ -487,6 +487,12 @@
const seen = seenAt[seenKey(id)] ?? (made?.createdBy && Number.isFinite(made.createdAt) ? made.createdAt : null)
ui.set(state => ({ ...state, visits: { ...state.visits, [id]: { seen, entered: Date.now() } } }))
},
@@ -1603,7 +1604,7 @@ diff --git a/src/client/index.js b/src/client/index.js
endVisit: (id) => {
leaveVisits([id])
ui.set((state) => {
-@@ -530,8 +530,14 @@ export function apply(ctx) {
+@@ -530,8 +530,14 @@
updateRoom: (id, patch) => after(call('update-room', { id, ...patch })),
send: (sessionId, text) => after(call('send', { sessionId: partFor(sessionId), text })),
answerQuestion: (sessionId, questionId, answer) => after(call('answer-question', { sessionId: partFor(sessionId), questionId, answer })),
@@ -1618,7 +1619,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,33 @@ export function apply(ctx) {
+@@ -539,15 +539,33 @@
secretCancel: requestId => after(call('secret-cancel', { requestId })),
secretUpdate: (name, scope) => after(call('secret-update', { name, scope })),
secretDelete: name => after(call('secret-delete', { name })),
@@ -1653,7 +1654,7 @@ diff --git a/src/client/index.js b/src/client/index.js
scheduleDelete: (sessionId, id) => call('schedule-delete', { sessionId, id }),
exchange: (a, b) => call('exchange', { a, b }).catch(() => []),
roomProgress: roomId => call('room-progress', { roomId }).catch(() => []),
-@@ -566,6 +566,8 @@ export function apply(ctx) {
+@@ -566,6 +566,8 @@
noteClientError(where, error)
ui.set(state => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } }))
},
@@ -1662,7 +1663,7 @@ diff --git a/src/client/index.js b/src/client/index.js
dismissToast: token => ui.set(state => (state.toast && (token === undefined || state.toast.token === token) ? { ...state, toast: null } : state)),
}
// Without the Desktop bridge these stay undefined, and no browser control renders.
-@@ -610,6 +610,7 @@ export function apply(ctx) {
+@@ -610,6 +610,7 @@
}
actions.closePanel = () => { ui.set(state => (state.details ? { ...state, details: null, view: 'details', renaming: false, tab: null } : state)) }
// Whether AccountLauncher holds the shell's settings seat; FooterAccount steps aside if so.
@@ -1670,7 +1671,7 @@ diff --git a/src/client/index.js b/src/client/index.js
actions.signInEntry = () => ctx.slots.entries?.('settings.models.sign-in')?.[0] ?? null
actions.canSignIn = () => Boolean(actions.signInEntry())
actions.signIn = () => ui.set(state => ({ ...state, signIn: (state.signIn ?? 0) + 1 }))
-@@ -647,6 +647,13 @@ export function apply(ctx) {
+@@ -647,6 +647,13 @@
wakeUpdate()
}
actions.repoStars = () => call('repo-stars')
@@ -1684,7 +1685,7 @@ diff --git a/src/client/index.js b/src/client/index.js
actions.noteTurnTime = (sessionId, turn, key, time) => {
if (sessionId === undefined || !Number.isFinite(turn) || !Number.isFinite(time)) return
turnClock.set((state) => {
-@@ -689,7 +689,18 @@ export function apply(ctx) {
+@@ -689,7 +689,18 @@
})
const browserPages = browsers?.pages ?? createSource({})
const browserTabs = browsers?.tabs ?? createSource({})
@@ -1704,7 +1705,7 @@ diff --git a/src/client/index.js b/src/client/index.js
ctx.effect(() => noticeApprovals({
work,
roster,
-@@ -715,10 +715,23 @@ export function apply(ctx) {
+@@ -715,10 +715,23 @@
}
const shadow = (name, options, component) => ctx.slots.inject(name, () => ctx.slots.register({ name, ...options }, component))
@@ -1728,7 +1729,7 @@ diff --git a/src/client/index.js b/src/client/index.js
const installAtMenu = () => {
const fiber = ctx.inject(['inputTriggers'], (scope) => {
scope.effect(() => {
-@@ -730,7 +730,12 @@ export function apply(ctx) {
+@@ -730,7 +730,12 @@
const launched = sessionId => controllerOf(sessionId)?.launcher.getSnapshot() === CHATS_SOURCE
const offs = atSources({ roster, sessions: ctx.sessions, launched, iconFor: menuIcons(roster) }).map(source => triggers.registerSource(source))
atMenu = { controllerOf }
@@ -1741,7 +1742,7 @@ diff --git a/src/client/index.js b/src/client/index.js
atMenu = null
for (const off of offs) off()
}
-@@ -739,6 +739,14 @@ export function apply(ctx) {
+@@ -739,6 +739,14 @@
return () => fiber.dispose()
}
actions.canReferenceChat = () => atMenu !== null
@@ -1756,7 +1757,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,49 @@ export function apply(ctx) {
+@@ -752,33 +752,50 @@
installCompiledStyles(SHELL_CSS, 'shell'),
installCompiledStyles(SIDEBAR_CSS, 'sidebar'),
installCompiledStyles(SECRET_CSS, 'secrets'),
@@ -1774,6 +1775,7 @@ diff --git a/src/client/index.js b/src/client/index.js
+ installCompiledStyles(MOBILE_CSS, 'mobile'),
+ installCompiledStyles(PANE_MIN_WIDTH_CSS, 'panes'),
+ installCompiledStyles(PROJECT_BOARD_CSS, 'project-board'),
++ installCompiledStyles(PROJECTS_NAV_CSS, 'projects-nav'),
+ typeof document !== 'undefined' ? watchShellLevel(document) : null,
browsers ? installCompiledStyles(BROWSER_CSS, 'browser') : null,
installTheme(),
@@ -1809,7 +1811,7 @@ diff --git a/src/client/index.js b/src/client/index.js
shadow('conversation.chat.node', { key: 'assistant-step', priority: -10, locale: 'chat', inject: assistantFace }, AssistantCell),
shadow('conversation.chat.node', { key: 'turn-trigger', priority: -10, locale: 'chat', inject: face }, TriggerCell),
shadow('conversation.chat.node', { key: 'tool-call', priority: -10, locale: 'chat', inject: face }, ToolCell),
-@@ -790,15 +790,24 @@ export function apply(ctx) {
+@@ -790,15 +790,24 @@
// every message.
shadow('conversation.chat.node', { key: 'question-reply', priority: -10, locale: 'chat' }, AnsweredCell),
shadow('conversation.chat.node', { key: 'compaction', priority: -10, locale: 'chat' }, Nothing),
@@ -1837,7 +1839,7 @@ diff --git a/src/client/index.js b/src/client/index.js
shadow('conversation.input.right', { id: 'bot-voice', order: 100, inject: face }, ComposerVoice),
shadow('conversation.composer', {
priority: -10,
-@@ -808,7 +808,14 @@ export function apply(ctx) {
+@@ -808,7 +808,14 @@
shadow('conversation.input.dock', { id: 'bot-model', order: -110, inject: face }, ModelChoiceDock),
shadow('conversation.input.dock', { id: 'bot-question', order: -100, inject: face }, QuestionDock),
shadow('conversation.input.dock', { id: 'bot-secret', order: -90, inject: face }, SecretDock),
@@ -2211,7 +2213,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,21 @@
+@@ -1,16 +1,22 @@
-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'
@@ -2233,10 +2235,11 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
import { showsDshRow } from './dsh-offer.js'
+import { InboxRow } from './inbox.js'
+import { KanbanRow } from './kanban.js'
++import { ProjectsNav } from './projects-nav.js'
// -------------------------------------------------------------------------
// Sidebar
-@@ -37,77 +37,151 @@ const workKeys = text => (text === '' ? [] : text.split(',').map((key) => {
+@@ -37,77 +37,151 @@
}))
const workLine = (lead, tool, more) => h(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ''}`)
@@ -2406,7 +2409,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 @@ function useListGlide(listRef, layout, key) {
+@@ -235,6 +235,31 @@
}, [layout, key])
}
@@ -2438,7 +2441,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 @@ export function TeamSidebar(props) {
+@@ -249,14 +249,26 @@
const order = useMemo(() => {
const stamps = JSON.parse(updated)
const entries = [
@@ -2468,12 +2471,13 @@ 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,17 @@ export function TeamSidebar(props) {
+@@ -277,13 +277,18 @@
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(ProjectsNav, { wide }),
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'
@@ -2489,7 +2493,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 @@ function UpdateButton({ useUpdate, actions }) {
+@@ -327,29 +327,29 @@
setDshError('')
void actions.installDshUpdate().catch(failure => setDshError(hostText(failure?.message ?? String(failure))))
}
@@ -2526,7 +2530,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 @@ function UpdateButton({ useUpdate, actions }) {
+@@ -372,36 +372,12 @@
}, dshPhase === 'installing' ? t('Updating…') : dshPhase === 'failed' ? t('Try again') : t('Update')))) : null), document.body) : null)
}
@@ -2567,7 +2571,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 @@ export function openHarnessOnboarding(id) {
+@@ -442,6 +442,26 @@
})
}
@@ -2594,7 +2598,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 @@ function pressSettingsShortcut() {
+@@ -466,8 +466,12 @@
}))
}
@@ -2609,7 +2613,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 @@ export function AccountLauncher({ seat = true, settingsOpen, openSettings, openO
+@@ -511,9 +511,10 @@
const groups = [
[
[t('Usage'), GaugeIcon, () => actions.openSettings('usage')],
@@ -2623,7 +2627,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 @@ export function AccountLauncher({ seat = true, settingsOpen, openSettings, openO
+@@ -523,6 +523,7 @@
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 },
@@ -2631,7 +2635,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 @@ function useLandOnMainBot(roster, useSessions, actions) {
+@@ -556,7 +556,10 @@
return undefined
}
const last = lastBot.current
@@ -2643,7 +2647,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
return () => clearTimeout(timer)
}, [mainView, roster])
}
-@@ -572,9 +572,9 @@ export function FooterAccount(props) {
+@@ -572,9 +572,9 @@
export function BotReturn({ wide, useSurface, actions }) {
const mode = useSurface(value => value.mode)
if (mode !== 'agent') return null
@@ -2656,7 +2660,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 @@ export const RETURN_CSS = `
+@@ -582,22 +582,22 @@
.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)}
@@ -2684,7 +2688,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}
-@@ -623,7 +623,7 @@ export const SIDEBAR_CSS = `
+@@ -623,7 +623,7 @@
.bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite}
@keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}}
.bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)}
@@ -2693,7 +2697,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js
@keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}}
.bt-update-title{font-size:14px;line-height:20px;font-weight:600}
.bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)}
-@@ -658,4 +658,14 @@ export const SIDEBAR_CSS = `
+@@ -658,4 +658,14 @@
.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}}
diff --git a/patches/ds-bot/0005-social-seams.patch b/patches/ds-bot/0005-social-seams.patch
index 04289bd8e..9e93396a1 100644
--- a/patches/ds-bot/0005-social-seams.patch
+++ b/patches/ds-bot/0005-social-seams.patch
@@ -288,7 +288,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
const invalid = message => Object.assign(new Error(message), { code: 'bot/invalid' })
// Programming and file system errors are DS Bot's own faults, logged with their stack;
-@@ -21,32 +31,154 @@
+@@ -21,32 +21,154 @@
// The DSH interface language of the last browser that asked for the team.
let uiLocale
@@ -452,7 +452,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
// The team's DSH Agent (at most one): plain dsh Sessions, not Bots.
agents: Object.values(state.agents),
// Key names and who may use them; values never leave the host.
-@@ -54,7 +186,8 @@
+@@ -54,7 +54,8 @@
capabilities: rt.capabilityLibraryView(),
secretRequests: state.secretRequests ?? {},
colors: [...COLORS, ...EXTRA_COLORS],
@@ -462,7 +462,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
defaultModel: rt.cachedDefault() ?? null,
permissions: rt.permissionCatalog?.() ?? null,
modelsKey: modelsKey(),
-@@ -65,22 +198,50 @@
+@@ -65,22 +65,50 @@
memory: rt.memoryEnabled !== false,
// Set while another dsh host holds the team (store.js): why this one cannot change it.
readOnly: rt.readOnly() ?? null,
@@ -524,7 +524,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
function setPrefs(body) {
if (body.theme !== undefined) {
-@@ -97,28 +258,64 @@
+@@ -97,28 +97,64 @@
else if (body.motion === 'quiet' || body.motion === 'lively') state.prefs.motion = body.motion
else throw invalid('Motion must be quiet, normal or lively')
}
@@ -593,7 +593,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
case 'duplicate-bot':
await rt.load()
return ok(await rt.duplicateBot(body.id))
-@@ -141,7 +338,7 @@
+@@ -141,7 +141,7 @@
return ok([...rt.roomProgress.entries()].filter(([, entry]) => entry.roomId === body.roomId).map(([callId, entry]) => ({ callId, replies: entry.replies, speaking: entry.speaking ?? null })))
case 'create-bot': {
await rt.bootstrap()
@@ -602,7 +602,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
return ok(bot)
}
case 'update-bot':
-@@ -150,18 +347,23 @@
+@@ -150,18 +150,23 @@
case 'delete-bot':
await rt.load()
await rt.deleteBot(body.id)
@@ -630,7 +630,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
return ok(room)
}
case 'update-room': {
-@@ -171,12 +373,70 @@
+@@ -171,12 +171,70 @@
notice: body.notice,
mode: body.mode,
admin: body.admin,
@@ -703,7 +703,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
}
case 'folders':
case 'folder-pick':
-@@ -203,17 +463,19 @@
+@@ -203,17 +203,19 @@
case 'remove-agent':
await rt.load()
await rt.removeAgent(body.agentId)
@@ -726,7 +726,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
case 'stop-member-work':
return ok(await rt.stopMemberWork(String(body.roomId ?? ''), String(body.botId ?? '')))
case 'dismiss-question':
-@@ -231,8 +493,14 @@
+@@ -231,8 +231,14 @@
await rt.load()
return ok(await rt.secretEndpoint(endpoint, body))
case 'connector-connect':
@@ -741,7 +741,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
await rt.load()
return ok(await rt.connectorEndpoint(endpoint, body))
case 'capability-save':
-@@ -241,11 +509,24 @@
+@@ -241,11 +241,24 @@
case 'bot-capability':
await rt.load()
return ok(await rt.capabilityEndpoint(endpoint, body))
@@ -767,7 +767,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
case 'memory-view':
case 'memory-ask':
case 'memory-forget':
-@@ -258,8 +539,17 @@
+@@ -258,8 +258,17 @@
const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
const question = state.questions[sessionId]
if (question === undefined || question.id !== body.questionId) return fail('This question is no longer open', 'bot/stale')
@@ -785,7 +785,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
// Claim the question before queueing so a second answer is refused.
delete state.questions[sessionId]
try {
-@@ -278,14 +568,54 @@
+@@ -278,14 +278,54 @@
const text = String(body.text ?? '').trim()
if (text === '' || typeof body.sessionId !== 'string') return fail('Nothing to send', 'bot/invalid')
await rt.load()
@@ -841,7 +841,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
content: [{ type: 'text', text }],
}, AbortSignal.timeout(30_000))
return ok(true)
-@@ -319,32 +649,133 @@
+@@ -319,32 +319,134 @@
case 'diagnostics-clear':
await rt.diagnostics.clear()
return ok(true)
@@ -871,6 +871,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js
+ case 'project-bind':
+ case 'project-of':
+ case 'project-board':
++ case 'project-role':
+ case 'project-delete':
+ case 'project-memory':
+ case 'project-memory-write':
diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch
index a2a0a486d..72a53b1e6 100644
--- a/patches/ds-bot/0008-client-bundle.patch
+++ b/patches/ds-bot/0008-client-bundle.patch
@@ -836,7 +836,7 @@ diff --git a/client.js b/client.js
"Working folder": "工作文件夹",
"Working folder: {folder}": "工作文件夹:{folder}",
Folder: "文件夹",
-@@ -887,7 +887,51 @@
+@@ -887,7 +887,61 @@
"Use this folder": "使用这个文件夹",
"Up one level": "上一级",
"No folders inside.": "里面没有文件夹。",
@@ -860,6 +860,16 @@ diff --git a/client.js b/client.js
+ "e.g. Website": "例如:官网",
+ "Optional. Where this Project's work happens.": "可选。这个项目的工作目录。",
+ "Projects · {count}": "项目 · {count}",
++ // #1828: a Project's roles, and the sidebar row that lists them.
++ Maintainer: "维护者",
++ Member: "成员",
++ "No Projects yet": "还没有项目",
++ "Reading Projects…": "正在读取项目…",
++ "My role": "我的角色",
++ "No role to switch here.": "这里没有可切换的角色。",
++ "Role changed.": "角色已更改。",
++ "Role unchanged: {reason}": "角色未更改:{reason}",
++ refused: "被拒绝",
+ "No Project yet — create one.": "还没有项目——新建一个。",
+ "Bind a Bot or group": "绑定 Bot 或群聊",
+ "Bind to {name}": "绑定到 {name}",
@@ -953,7 +963,7 @@ diff --git a/client.js b/client.js
var service = null;
var bound = null;
function installLocale(locale) {
-@@ -1005,17 +1005,25 @@
+@@ -1005,15 +1005,23 @@
}
const message = String(error?.message ?? error ?? "error").slice(0, 300);
const code = typeof error?.code === "string" ? error.code : void 0;
@@ -969,19 +979,17 @@ diff --git a/client.js b/client.js
}
- entries.push({ time: now, level: "error", text, ...code ? { code } : {} });
+ entries.push({ time: now, level: "error", text: text2, ...code ? { code } : {} });
- if (entries.length > MAX) entries.splice(0, entries.length - MAX);
- }
++ if (entries.length > MAX) entries.splice(0, entries.length - MAX);
++}
+function noteNavigation(how, target, from) {
+ if (typeof navigator === "undefined" || navigator.userActivation?.isActive !== false) return;
+ const stack = String(new Error().stack ?? "").split("\n").slice(2, 6).map((line2) => line2.trim().replace(/\(?https?:\/\/[^/\s)]+/, "(")).join(" < ");
+ const text2 = `navigation: ${how} to ${String(target).slice(0, 60)}${from ? ` from ${String(from).slice(0, 60)}` : ""} with no click (${stack.slice(0, 300)})`;
+ console.warn(`[flybot] ${text2}`);
+ entries.push({ time: Date.now(), level: "warn", text: text2 });
-+ if (entries.length > MAX) entries.splice(0, entries.length - MAX);
-+}
+ if (entries.length > MAX) entries.splice(0, entries.length - MAX);
+ }
var quietly = (where) => (error) => noteClientError(where, error);
- var clearClientErrors = () => {
- entries.length = 0;
@@ -1042,16 +1042,21 @@
};
function failureLabel(code, message) {
@@ -1544,8 +1552,8 @@ diff --git a/client.js b/client.js
}
-var eyeMarkup = ([cx, cy, w, h34, tilt]) => ``;
-var happyEyeMarkup = ([cx, cy, w, h34, tilt]) => ``;
-+var eyeMarkup = ([cx, cy, w, h49, tilt]) => ``;
-+var happyEyeMarkup = ([cx, cy, w, h49, tilt]) => ``;
++var eyeMarkup = ([cx, cy, w, h50, tilt]) => ``;
++var happyEyeMarkup = ([cx, cy, w, h50, 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%";
@@ -1613,7 +1621,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 = {
@@ -1718,7 +1726,7 @@ 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;
@@ -1786,7 +1794,7 @@ diff --git a/client.js b/client.js
+ }
+ return missing;
+}
-
++
+// src/shared/preview.js
+var FENCE_TAG = /<\s*\/?\s*untrusted-(?:message|file)\b[^<>]*>/gi;
+var FENCE_ATTR = /\bfrom\s*=\s*(?:"[^"]*"|'[^']*')\s*>/gi;
@@ -1812,7 +1820,7 @@ diff --git a/client.js b/client.js
+ }
+ return stripFence(out).replace(/\s+/g, " ").trim();
+}
-+
+
// src/client/text.js
var MENTION_ORIGIN = "https://bots.local/";
-function linkMentions(text, bots, selfId) {
@@ -2079,6 +2087,7 @@ diff --git a/client.js b/client.js
+ 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;
@@ -2154,7 +2163,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";
@@ -2414,29 +2423,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: h49, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) {
-+ if (!preview || typeof h49 !== "function") return null;
++function RoomInvitePreview({ h: h50, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) {
++ if (!preview || typeof h50 !== "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 h49(
++ return h50(
+ "div",
+ { className: "fs-preview", "data-flybot": at, role: "status" },
-+ h49("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"),
-+ preview.lead ? h49("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null,
-+ preview.people ? h49("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null,
-+ preview.expiresAt ? h49("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null,
-+ onJoin || onClear ? h49(
++ h50("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"),
++ preview.lead ? h50("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null,
++ preview.people ? h50("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null,
++ preview.expiresAt ? h50("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null,
++ onJoin || onClear ? h50(
+ "div",
+ { className: "fs-preview-foot", "data-flybot": child("foot") },
-+ showClear && onClear ? h49("button", {
++ showClear && onClear ? h50("button", {
+ type: "button",
+ className: "fs-btn fs-btn-sm",
+ "data-flybot": child("clear"),
+ disabled: !!busy,
+ onClick: onClear
+ }, clearLabel) : null,
-+ onJoin ? h49("button", {
++ onJoin ? h50("button", {
+ type: "button",
+ className: "fs-btn fs-btn-sm fs-btn-primary",
+ "data-flybot": child("join"),
@@ -3032,8 +3041,7 @@ diff --git a/client.js b/client.js
+ }).join("\n");
+}
+
- // src/client/motion.js
--var import_react = require("react");
++// src/client/motion.js
+var import_react4 = require("react");
var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
function springEasing(stiffness, damping) {
@@ -3043,14 +3051,14 @@ diff --git a/client.js b/client.js
last = now;
const steps = Math.ceil(dt * 240);
- const h34 = dt / steps;
-+ const h49 = dt / steps;
++ const h50 = 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) * h49;
-+ value.x += value.v * h49;
++ value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h50;
++ value.x += value.v * h50;
}
if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) {
value.x = value.to;
@@ -3674,12 +3682,7 @@ diff --git a/client.js b/client.js
- document.documentElement.removeEventListener("pointerleave", onLeave);
- };
- }, [enabled]);
-+// src/shared/face-title.js
-+var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
-+function isGenericFaceTitle(name) {
-+ 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);
@@ -3709,58 +3712,24 @@ diff --git a/client.js b/client.js
- 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();
-+ for (const member of members ?? []) {
-+ const owner = String(member?.ownerName ?? member?.owner_name ?? "").trim();
-+ const key = owner.toLowerCase();
-+ if (!owner || seen.has(key)) continue;
-+ seen.add(key);
-+ const handle = String(member?.handle ?? member?.id ?? "").trim().toLowerCase();
-+ rows.push({ owner, sort: handle || key });
-+ }
-+ 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 = [];
-+ for (const raw of owners ?? []) {
-+ const owner = String(raw ?? "").trim();
-+ const key = owner.toLowerCase();
-+ if (!owner || seen.has(key)) continue;
-+ seen.add(key);
-+ out.push(owner);
-+ }
-+ 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);
@@ -3777,8 +3746,22 @@ diff --git a/client.js b/client.js
- return () => clearTimeout(timer);
- }, [state]);
- return settled;
-+function joinCodeOf(code) {
-+ return String(code ?? "").replace(/\D/g, "").slice(0, 6);
++// 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";
++}
++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 isAutoFaceTitle(name) {
++ const n = String(name ?? "").trim();
++ return isGenericFaceTitle(n) || n.startsWith("Face-to-face · ");
}
-function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) {
- const settled = useSettled(state);
@@ -3793,13 +3776,7 @@ diff --git a/client.js b/client.js
- 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]];
@@ -3807,6 +3784,76 @@ diff --git a/client.js b/client.js
- const size = frame * 5 / 9;
- const rest = frame - size;
- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]];
++function ownersFromMembers(members) {
++ const rows = [];
++ const seen = /* @__PURE__ */ new Set();
++ for (const member of members ?? []) {
++ const owner = String(member?.ownerName ?? member?.owner_name ?? "").trim();
++ const key = owner.toLowerCase();
++ if (!owner || seen.has(key)) continue;
++ seen.add(key);
++ const handle = String(member?.handle ?? member?.id ?? "").trim().toLowerCase();
++ rows.push({ owner, sort: handle || key });
+ }
+- const half = frame / 2;
+- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]];
++ rows.sort((a, b) => a.sort.localeCompare(b.sort));
++ return rows.slice(0, 3).map((row) => row.owner);
+ }
+-function RoomAvatar({ room: room2, roster, size = 36 }) {
+- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean);
+- const people = members.length === 1 ? [members[0], null] : members;
+- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } });
+- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size });
+- if (size <= 20 && people.length > 1) {
+- return (0, import_react2.createElement)(
+- "span",
+- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } },
+- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)),
+- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null
+- );
+- }
+- const seats = clusterSeats(Math.min(people.length, 4), size);
+- const overflow = people.length > 4 ? people.length - 3 : 0;
+- const gap = size / 14;
+- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap }));
+- return (0, import_react2.createElement)(
+- "span",
+- { className: "bt-cluster", style: { width: size, height: size } },
+- seats.map(([x, y, s], index) => {
+- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`);
+- const style = { left: x, top: y, width: s, height: s };
+- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" });
+- if (overflow > 0 && index === 3) {
+- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`);
+- }
+- const bot = people[index];
+- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s));
+- })
+- );
++function uniqueOwners(owners) {
++ const seen = /* @__PURE__ */ new Set();
++ const out = [];
++ for (const raw of owners ?? []) {
++ const owner = String(raw ?? "").trim();
++ const key = owner.toLowerCase();
++ if (!owner || seen.has(key)) continue;
++ seen.add(key);
++ out.push(owner);
++ }
++ out.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
++ return out.slice(0, 3);
++}
++function joinCodeOf(code) {
++ return String(code ?? "").replace(/\D/g, "").slice(0, 6);
++}
++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 formatFaceRoomTitle({ name, owners, members, code, at } = {}) {
+ const named = String(name ?? "").trim();
+ if (named && !isAutoFaceTitle(named)) return named;
@@ -3839,26 +3886,12 @@ diff --git a/client.js b/client.js
+ const room2 = roster.roomsById?.[sessionId];
+ if (bot?.remote) {
+ return friendBotTitle(bot);
- }
-- const half = frame / 2;
-- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]];
++ }
+ if (room2 && room2.origin === "face-to-face" && (room2.face?.status === "closed" || room2.face?.status === "left") && isAutoFaceTitle(room2.name)) return "Face-to-face · closed";
+ if (room2?.origin === "face-to-face") return faceRowTitle(roomTitle(room2));
+ if (room2) return roomTitle(room2);
+ return bot?.name || "";
- }
--function RoomAvatar({ room: room2, roster, size = 36 }) {
-- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean);
-- const people = members.length === 1 ? [members[0], null] : members;
-- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } });
-- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size });
-- if (size <= 20 && people.length > 1) {
-- return (0, import_react2.createElement)(
-- "span",
-- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } },
-- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)),
-- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null
-- );
++}
+function installTabTitle(sessions, roster, doc = globalThis.document) {
+ if (!doc || !sessions || !roster) return () => {
+ };
@@ -3888,25 +3921,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);
@@ -5122,8 +5137,8 @@ diff --git a/client.js b/client.js
- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null
+ reasoning ? (0, import_react9.createElement)(ThinkingRow, { text: reasoning }) : null,
+ held && bubbles ? (0, import_react9.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles
- );
- }
++ );
++}
+function HeldReply({ held, roster, sessionId, actions, closing, children }) {
+ const bot = roster.byId[sessionId];
+ const chat = (roster.bots ?? []).find((item) => item.remote?.handle === held.to);
@@ -5142,8 +5157,8 @@ diff --git a/client.js b/client.js
+ { className: "bt-group-msg bt-person-msg", "data-person": name },
+ (0, import_react9.createElement)("span", { className: "bt-group-name" }, name),
+ (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi, lead })
-+ );
-+}
+ );
+ }
function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) {
if (whole && bots.length === 1) {
- return (0, import_react6.createElement)(
@@ -5309,8 +5324,8 @@ diff --git a/client.js b/client.js
- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead })
+ (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text2)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }),
+ files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null
- );
- }
++ );
++}
+function FirstContact({ source, roster, sessionId }) {
+ const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : "";
+ const who = person || remoteSpeaker(source, roster).name;
@@ -5354,8 +5369,8 @@ diff --git a/client.js b/client.js
+ `📎 ${file.name} · ${fileSize(file.size)}`
+ )),
+ error ? (0, import_react9.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null
-+ );
-+}
+ );
+ }
function GroupEvent({ prefix, room: room2, name, roster, actions }) {
- return (0, import_react6.createElement)(
+ return (0, import_react9.createElement)(
@@ -5686,10 +5701,10 @@ diff --git a/client.js b/client.js
if (!tool) return t("Thinking about it");
- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it");
+ return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool });
- }
++}
+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)(() => {
@@ -5848,14 +5863,14 @@ diff --git a/client.js b/client.js
const seen = /* @__PURE__ */ new Set();
- const visit = (call2) => {
- const id = call2.callId ?? call2;
-+ const visit = (call4) => {
-+ const id = call4.callId ?? call4;
++ const visit = (call5) => {
++ const id = call5.callId ?? call5;
if (seen.has(id)) return;
seen.add(id);
- calls.push(call2);
- for (const child of call2.subCalls ?? []) visit(child);
-+ calls.push(call4);
-+ for (const child of call4.subCalls ?? []) visit(child);
++ calls.push(call5);
++ for (const child of call5.subCalls ?? []) visit(child);
};
visit(root);
return calls;
@@ -5874,24 +5889,24 @@ diff --git a/client.js b/client.js
- const tool = toolName(call2);
- const done = call2.kind === "tool-result";
- const { text: arg, value: argValue } = toolArgs(call2);
-+function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) {
-+ const tool = toolName(call4);
-+ const done = call4.kind === "tool-result";
-+ const { text: arg, value: argValue } = toolArgs(call4);
++function renderCall(call5, { sessionId, roster, actions, useUi, findBot }) {
++ const tool = toolName(call5);
++ const done = call5.kind === "tool-result";
++ const { text: arg, value: argValue } = toolArgs(call5);
const self = roster.byId[sessionId];
switch (tool) {
case "message_bot": {
const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" };
- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
-+ if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
++ if (done && call5.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
+ return (0, import_react9.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
}
case "stop_bot": {
const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" };
- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
-+ if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
++ if (done && call5.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
+ return (0, import_react9.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
}
case "create_bot": {
@@ -5907,7 +5922,7 @@ diff --git a/client.js b/client.js
}
case "group_relay":
- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork });
-+ return (0, import_react9.createElement)(GroupReplies, { sessionId, callId: call4.callId, done, content: call4.content, roster, actions, useUi });
++ return (0, import_react9.createElement)(GroupReplies, { sessionId, callId: call5.callId, done, content: call5.content, roster, actions, useUi });
case "create_group":
case "update_group":
case "delete_group":
@@ -5916,10 +5931,10 @@ diff --git a/client.js b/client.js
}[tool];
if (!done) return null;
- const result = contentText(call2.content);
-+ const result = contentText(call4.content);
++ const result = contentText(call5.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]));
-+ if (call4.isError || !named) return (0, import_react9.createElement)("div", { className: "bt-event", title: result }, t(verbs[1]));
++ if (call5.isError || !named) return (0, import_react9.createElement)("div", { className: "bt-event", title: result }, t(verbs[1]));
const name = named[2];
const room2 = roster.rooms.find((entry) => entry.name === name);
- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions });
@@ -5929,18 +5944,18 @@ diff --git a/client.js b/client.js
const question = roster.questions?.[sessionId];
- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true });
- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true });
-+ if (!question || question.callId !== call4.callId) return (0, import_react9.createElement)("span", { className: "bt-q-gone", hidden: true });
++ if (!question || question.callId !== call5.callId) return (0, import_react9.createElement)("span", { className: "bt-q-gone", hidden: true });
+ return (0, import_react9.createElement)(PendingQuestion, { sessionId, question, actions, inline: true });
}
case "request_secret":
- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions });
-+ return (0, import_react9.createElement)(SecretToolCell, { sessionId, callId: call4.callId, name: arg("name"), done, content: call4.content, roster, actions });
++ return (0, import_react9.createElement)(SecretToolCell, { sessionId, callId: call5.callId, name: arg("name"), done, content: call5.content, roster, actions });
case "ask_user_question": {
if (!done) return null;
let parsed = null;
try {
- parsed = JSON.parse(contentText(call2.content));
-+ parsed = JSON.parse(contentText(call4.content));
++ parsed = JSON.parse(contentText(call5.content));
} catch {
parsed = null;
}
@@ -5959,7 +5974,7 @@ diff --git a/client.js b/client.js
case "remember":
case "forget":
- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null;
-+ return done && !call4.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null;
++ return done && !call5.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call5.content), roster, actions, findBot }) : null;
default:
return null;
}
@@ -5975,19 +5990,19 @@ diff --git a/client.js b/client.js
const calls = hidden ? [] : callTree(root);
- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2)));
- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result");
-+ const drawn = calls.filter((call4) => DRAWN.has(toolName(call4)));
-+ const running = root.kind === "tool-result" ? void 0 : calls.findLast((call4) => call4.kind !== "tool-result");
++ const drawn = calls.filter((call5) => DRAWN.has(toolName(call5)));
++ const running = root.kind === "tool-result" ? void 0 : calls.findLast((call5) => call5.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));
-+ useShownCards(actions, sessionId, drawn.filter((call4) => CARDS.has(toolName(call4)) && call4.callId).map((call4) => call4.callId));
++ useShownCards(actions, sessionId, drawn.filter((call5) => CARDS.has(toolName(call5)) && call5.callId).map((call5) => call5.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 env = { sessionId, roster, actions, useUi, findBot };
-+ return (0, import_react9.createElement)(import_react9.Fragment, null, drawn.map((call4, index) => (0, import_react9.createElement)(import_react9.Fragment, { key: call4.callId ?? index }, renderCall(call4, env))));
- }
-
++ return (0, import_react9.createElement)(import_react9.Fragment, null, drawn.map((call5, index) => (0, import_react9.createElement)(import_react9.Fragment, { key: call5.callId ?? index }, renderCall(call5, env))));
++}
++
+// src/client/awaiting.js
+var AWAIT_REPLY_MS = 10 * 60 * 1e3;
+function awaitingReply(roster, sessionId, now = Date.now()) {
@@ -6012,8 +6027,8 @@ diff --git a/client.js b/client.js
+ if (typeof root.matches === "function" && root.matches("[data-chat-running]")) return root;
+ if (typeof Element !== "undefined" && root instanceof Element) return root.querySelector("[data-chat-running]");
+ return root.querySelector(RUNNING_HOST);
-+}
-+
+ }
+
// src/client/activity-row.js
-var import_react7 = require("react");
+var import_react10 = require("react");
@@ -6252,10 +6267,12 @@ diff --git a/client.js b/client.js
)
)
), host);
-@@ -4731,49 +4731,1697 @@
+@@ -4731,49 +4731,1905 @@
return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified);
}
+-// src/client/sidebar.js
+-var import_react8 = require("react");
+// src/client/overlay-hooks.js
+var import_react11 = require("react");
+var escapeLayers = [];
@@ -7899,19 +7916,226 @@ diff --git a/client.js b/client.js
+}
+`;
+
- // src/client/sidebar.js
--var import_react8 = require("react");
++// src/client/projects-nav.js
+var import_react16 = 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 call4 = (endpoint, payload) => call(`flybot/${endpoint}`, payload);
++var PROJECTS_NAV_CSS = `
++.bt-projects-row { display: flex; align-items: center; gap: 8px; }
++.bt-projects-chevron { margin-left: auto; display: inline-flex; transition: transform 120ms ease; }
++.bt-projects-row[aria-expanded="true"] .bt-projects-chevron { transform: rotate(90deg); }
++.bt-projects-list { display: flex; flex-direction: column; margin: 0 0 4px; padding-left: 30px; }
++.bt-project-entry { display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left; border: 0; background: none; color: inherit; font: inherit; padding: 4px 8px; border-radius: 6px; cursor: pointer; }
++.bt-project-entry:hover, .bt-project-entry:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; }
++.bt-project-entry[aria-current="true"] { background: var(--bt-hover, rgba(127,127,127,.14)); }
++.bt-project-entry-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
++.bt-project-entry-role { margin-left: auto; opacity: .62; font-size: .85em; white-space: nowrap; }
++.bt-projects-empty, .bt-projects-note { padding: 2px 8px 6px; opacity: .7; font-size: .85em; }
++.bt-project-menu { position: fixed; z-index: 60; min-width: 168px; padding: 4px; border-radius: 8px; background: var(--bt-menu-bg, #1f2126); border: 1px solid var(--bt-menu-border, rgba(255,255,255,.12)); box-shadow: 0 8px 24px rgba(0,0,0,.32); }
++.bt-project-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: none; color: inherit; font: inherit; text-align: left; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
++.bt-project-menu-item:hover, .bt-project-menu-item:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; }
++.bt-project-menu-item[aria-checked="true"]::before { content: "✓"; margin-right: 2px; }
++.bt-project-menu-item:not([aria-checked="true"])::before { content: " "; margin-right: 2px; }
++.bt-project-menu-item[disabled] { opacity: .6; cursor: default; }
++`;
++var ROLE_LABELS = { owner: "Owner", maintainer: "Maintainer", member: "Member" };
++function roleText(role) {
++ if (role === null || role === void 0 || role === "") return "";
++ return t(ROLE_LABELS[role] ?? String(role));
++}
++function projectHash(id) {
++ return `#project=${encodeURIComponent(String(id ?? ""))}`;
++}
++function projectName(project) {
++ const name = typeof project?.name === "string" ? project.name.trim() : "";
++ return name === "" ? `#${project?.id ?? ""}` : name;
++}
++function projectMenuItems(project) {
++ const current = project?.role ?? null;
++ const choices = Array.isArray(project?.choices) ? project.choices : [];
++ const items = current === null ? [] : [{ role: current, label: roleText(current), current: true, pick: false }];
++ for (const role of choices) {
++ if (role === current) continue;
++ items.push({ role, label: roleText(role), current: false, pick: true });
++ }
++ return items;
++}
++function roleNote(answer) {
++ if (answer?.changed === true) return t("Role changed.");
++ const reason = typeof answer?.reason === "string" && answer.reason !== "" ? answer.reason : t("refused");
++ return t("Role unchanged: {reason}", { reason });
++}
++function ProjectList({ projects, error, note, onOpen, onRole }) {
++ const rows = projects === null ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, t("Reading Projects…")) : error !== "" ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, error) : projects.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-projects-empty" }, t("No Projects yet")) : projects.map((project) => (0, import_react16.createElement)(
++ "button",
++ {
++ key: String(project.id),
++ type: "button",
++ className: "bt-project-entry",
++ "data-project-id": String(project.id),
++ "aria-current": project.current === true ? "true" : void 0,
++ title: roleText(project.role) === "" ? projectName(project) : `${projectName(project)} · ${roleText(project.role)}`,
++ onClick: () => onOpen?.(project.id),
++ onContextMenu: (event) => {
++ event.preventDefault();
++ onRole?.(project, event);
++ }
++ },
++ (0, import_react16.createElement)("span", { className: "bt-project-entry-name" }, projectName(project)),
++ roleText(project.role) === "" ? null : (0, import_react16.createElement)("span", { className: "bt-project-entry-role" }, roleText(project.role))
++ ));
++ return (0, import_react16.createElement)(
++ "div",
++ { className: "bt-projects-list", role: "group", "aria-label": t("Projects") },
++ rows,
++ note === "" || note === void 0 ? null : (0, import_react16.createElement)("div", { className: "bt-projects-note", role: "status" }, note)
++ );
++}
++function Chevron() {
++ return (0, import_react16.createElement)(
++ "svg",
++ { width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
++ (0, import_react16.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })
++ );
++}
++function ProjectsMark() {
++ return (0, import_react16.createElement)(
++ "svg",
++ { width: 18, height: 18, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinejoin: "round", "aria-hidden": true },
++ (0, import_react16.createElement)("path", { d: "M2.5 5.5h6l1.5 2h7.5v8a1 1 0 0 1-1 1h-13a1 1 0 0 1-1-1z" }),
++ (0, import_react16.createElement)("path", { d: "M2.5 5.5v-1a1 1 0 0 1 1-1h4l1.5 2" })
++ );
++}
++function ProjectRoleMenu({ project, x, y, onPick, onClose }) {
++ const ref = (0, import_react16.useRef)(null);
+ (0, import_react16.useEffect)(() => {
++ const onDown = (event) => {
++ if (!ref.current?.contains(event.target)) onClose();
++ };
++ const onKey = (event) => {
++ if (event.key === "Escape") onClose();
++ };
++ window.addEventListener("mousedown", onDown, true);
++ window.addEventListener("keydown", onKey, true);
++ return () => {
++ window.removeEventListener("mousedown", onDown, true);
++ window.removeEventListener("keydown", onKey, true);
++ };
++ }, [onClose]);
++ const items = projectMenuItems(project);
++ return (0, import_react_dom3.createPortal)((0, import_react16.createElement)(
++ "div",
++ {
++ ref,
++ className: "bt-project-menu",
++ role: "menu",
++ "aria-label": t("My role"),
++ style: { left: `${x}px`, top: `${y}px` },
++ "data-project-id": String(project?.id ?? "")
++ },
++ (0, import_react16.createElement)("div", { className: "bt-projects-note" }, projectName(project)),
++ items.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, t("No role to switch here.")) : items.map((item) => (0, import_react16.createElement)("button", {
++ key: item.role,
++ type: "button",
++ role: "menuitemradio",
++ className: "bt-project-menu-item",
++ "aria-checked": item.current ? "true" : "false",
++ disabled: !item.pick,
++ onClick: () => {
++ if (item.pick) onPick(item.role);
++ else onClose();
++ }
++ }, item.label))
++ ), document.body);
++}
++function ProjectsNav({ wide = true, request, onOpen } = {}) {
++ const read = request ?? call4;
++ const open = (0, import_react16.useCallback)((id) => {
++ if (onOpen) {
++ onOpen(id);
++ return;
++ }
++ if (typeof window !== "undefined") window.location.hash = projectHash(id);
++ }, [onOpen]);
++ const [expanded, setExpanded] = (0, import_react16.useState)(false);
++ const [projects, setProjects] = (0, import_react16.useState)(null);
++ const [error, setError] = (0, import_react16.useState)("");
++ const [note, setNote] = (0, import_react16.useState)("");
++ const [menu, setMenu] = (0, import_react16.useState)(null);
++ const load = (0, import_react16.useCallback)(async () => {
++ try {
++ const listing = await read("projects");
++ const list = Array.isArray(listing?.projects) ? listing.projects : [];
++ setProjects(list);
++ setError("");
++ } catch (failure) {
++ setProjects([]);
++ setError(failure?.message ?? String(failure));
++ }
++ }, [read]);
++ (0, import_react16.useEffect)(() => {
++ if (!expanded) return;
++ void load();
++ }, [expanded, load]);
++ const toggle = () => {
++ setNote("");
++ if (!wide) {
++ void open("");
++ return;
++ }
++ setExpanded((value) => !value);
++ };
++ const listed = expanded && wide;
++ return (0, import_react16.createElement)(
++ "div",
++ { className: "bt-projects" },
++ (0, import_react16.createElement)(
++ "button",
++ {
++ type: "button",
++ className: "bt-row bt-projects-row",
++ "aria-expanded": listed ? "true" : "false",
++ "aria-label": t("Projects"),
++ title: wide ? void 0 : t("Projects"),
++ onClick: toggle
++ },
++ (0, import_react16.createElement)("span", { className: "bt-projects-mark" }, (0, import_react16.createElement)(ProjectsMark)),
++ wide ? (0, import_react16.createElement)("span", { className: "bt-projects-label" }, t("Projects")) : null,
++ wide ? (0, import_react16.createElement)("span", { className: "bt-projects-chevron", "aria-hidden": true }, (0, import_react16.createElement)(Chevron)) : null
++ ),
++ listed ? (0, import_react16.createElement)(ProjectList, { projects, error, note, onOpen: open, onRole: (project, event) => setMenu({ project, x: event.clientX, y: event.clientY }) }) : null,
++ menu === null ? null : (0, import_react16.createElement)(ProjectRoleMenu, {
++ project: menu.project,
++ x: menu.x,
++ y: menu.y,
++ onClose: () => setMenu(null),
++ onPick: async (role) => {
++ const project = menu.project;
++ setMenu(null);
++ try {
++ const answer = await read("project-role", { id: project.id, role });
++ setNote(roleNote(answer));
++ if (answer?.changed === true) void load();
++ } catch (failure) {
++ setNote(failure?.message ?? String(failure));
++ }
++ }
++ })
++ );
++}
++
++// src/client/sidebar.js
++var import_react17 = require("react");
++var import_react_dom4 = 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_react17.useEffect)(() => {
if (active && marked) actions.clearUnread(id);
}, [active, marked]);
}
-var PinMark = () => (0, import_react8.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react8.createElement)(PinMarkIcon));
-+var PinMark = () => (0, import_react16.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react16.createElement)(PinMarkIcon));
++var PinMark = () => (0, import_react17.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react17.createElement)(PinMarkIcon));
var useApproval = (useWork, match) => useWork ? useWork((work) => (work.approvals ?? []).some(match)) : false;
var useGroupWork = (useWork, pick2) => useWork ? useWork((work) => pick2(work.rooms ?? {}).join(",")) : "";
-var workKeys = (text) => text === "" ? [] : text.split(",").map((key) => {
@@ -7921,7 +8145,7 @@ diff --git a/client.js b/client.js
});
-var workLine = (lead, tool, more) => (0, import_react8.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`);
-function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked }) {
-+var workLine = (lead, tool, more) => (0, import_react16.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`);
++var workLine = (lead, tool, more) => (0, import_react17.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`);
+function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked, asRow = false }) {
const id = bot.id;
const part = currentPart(bot);
@@ -7959,7 +8183,7 @@ diff --git a/client.js b/client.js
+ const label = [shown, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", ");
if (tile && wide) {
- return (0, import_react8.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react8.createElement)(
-+ return (0, import_react16.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react16.createElement)(
++ return (0, import_react17.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react17.createElement)(
"button",
{
type: "button",
@@ -7970,16 +8194,16 @@ diff --git a/client.js b/client.js
- (0, import_react8.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
- (0, import_react8.createElement)("span", { className: "bt-tile-name" }, bot.name),
- bot.role ? (0, import_react8.createElement)("span", { className: "bt-chip" }, bot.role) : null
-+ (0, import_react16.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
-+ (0, import_react16.createElement)("span", { className: "bt-tile-name" }, shown),
++ (0, import_react17.createElement)(BotAvatar, { bot, size: 60, state, main: true }),
++ (0, import_react17.createElement)("span", { className: "bt-tile-name" }, shown),
+ // The Main Bot's tile gets the same unread dot as a row (#420); a wait shows on its avatar.
-+ unread && !waiting && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null,
-+ bot.role ? (0, import_react16.createElement)("span", { className: "bt-chip" }, bot.role) : null
++ unread && !waiting && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-tile-unread", "aria-hidden": true }) : null,
++ bot.role ? (0, import_react17.createElement)("span", { className: "bt-chip" }, bot.role) : null
));
}
- return (0, import_react8.createElement)(
-+ const unreadMark = wide && waiting && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null;
-+ const row = (0, import_react16.createElement)(
++ const unreadMark = wide && waiting && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null;
++ const row = (0, import_react17.createElement)(
"button",
{
type: "button",
@@ -7996,27 +8220,27 @@ diff --git a/client.js b/client.js
- (0, import_react8.createElement)(BotAvatar, { bot, size: 36, state, main }),
- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null,
- wide ? (0, import_react8.createElement)(
-+ (0, import_react16.createElement)(BotAvatar, { bot, size: 36, state, main }),
-+ pinned && !wide ? (0, import_react16.createElement)(PinMark) : null,
-+ wide ? (0, import_react16.createElement)(
++ (0, import_react17.createElement)(BotAvatar, { bot, size: 36, state, main }),
++ pinned && !wide ? (0, import_react17.createElement)(PinMark) : null,
++ wide ? (0, import_react17.createElement)(
"span",
{ className: "bt-row-body" },
- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, bot.name), pinned ? (0, import_react8.createElement)(PinMark) : null),
- awayLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, awayLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
+ "span",
+ { className: "bt-row-name" },
-+ (0, import_react16.createElement)("span", { className: "bt-row-title" }, shown),
-+ bot.remote ? (0, import_react16.createElement)(RemoteMark, { remote: bot.remote }) : null,
-+ pinned ? (0, import_react16.createElement)(PinMark) : null
++ (0, import_react17.createElement)("span", { className: "bt-row-title" }, shown),
++ bot.remote ? (0, import_react17.createElement)(RemoteMark, { remote: bot.remote }) : null,
++ pinned ? (0, import_react17.createElement)(PinMark) : null
+ ),
-+ secondLine ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, secondLine) : null
++ secondLine ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, secondLine) : null
) : null,
- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
+ unreadMark
- );
++ );
+ const settingsLabel = bot.remote ? t("Bot settings") : t("Edit Bot");
-+ const settings = wide ? (0, import_react16.createElement)("button", {
++ const settings = wide ? (0, import_react17.createElement)("button", {
+ type: "button",
+ className: "bt-row-settings",
+ "aria-label": settingsLabel,
@@ -8027,10 +8251,10 @@ diff --git a/client.js b/client.js
+ keepSidebarStill(event.currentTarget);
+ actions.openSettings("bots", id);
+ }
-+ }, (0, import_react16.createElement)(GearIcon)) : null;
-+ if (settings) return (0, import_react16.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
++ }, (0, import_react17.createElement)(GearIcon)) : null;
++ if (settings) return (0, import_react17.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings);
+ return row;
- }
++}
+function keepSidebarStill(node) {
+ const reset = () => {
+ for (let at = node?.parentElement; at; at = at.parentElement) if (at.scrollLeft > 0) at.scrollLeft = 0;
@@ -8041,14 +8265,14 @@ diff --git a/client.js b/client.js
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset));
+}
+function RemoteMark({ remote }) {
-+ return (0, import_react16.createElement)(
++ return (0, import_react17.createElement)(
+ "span",
+ { className: "bt-row-remote", title: remote.gone ? `${remote.handle} · ${t("no longer shared")}` : remote.handle, "aria-label": t("Friend's Bot") },
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
+ "svg",
+ { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
-+ (0, import_react16.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
-+ (0, import_react16.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
++ (0, import_react17.createElement)("circle", { cx: 12, cy: 12, r: 9 }),
++ (0, import_react17.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
+ )
+ );
+}
@@ -8060,18 +8284,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_react16.createElement)(
++ return (0, import_react17.createElement)(
+ "span",
+ { className: "bt-row-remote bt-row-face", title: t("Face-to-face"), "aria-label": t("Face-to-face") },
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
+ "svg",
+ { width: 11, height: 11, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": true },
-+ (0, import_react16.createElement)("circle", { cx: 9, cy: 8, r: 3 }),
-+ (0, import_react16.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }),
-+ (0, import_react16.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" })
++ (0, import_react17.createElement)("circle", { cx: 9, cy: 8, r: 3 }),
++ (0, import_react17.createElement)("circle", { cx: 16, cy: 9, r: 2.5 }),
++ (0, import_react17.createElement)("path", { d: "M4 18a5 5 0 0 1 10 0M14 18a4 4 0 0 1 6 0" })
+ )
-+ );
-+}
+ );
+ }
function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) {
const id = room2.id;
- const fallback = useSessions((list) => previewOf(list, id));
@@ -8097,7 +8321,7 @@ diff --git a/client.js b/client.js
+ const alert = question || waiting;
+ const face = room2.origin === "face-to-face";
+ const label = [hover, face ? t("Face-to-face") : "", pinned ? t("Pinned") : "", members.length > 0 ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : ""].filter(Boolean).join(", ");
-+ return (0, import_react16.createElement)(
++ return (0, import_react17.createElement)(
"button",
{
type: "button",
@@ -8117,25 +8341,25 @@ diff --git a/client.js b/client.js
- (0, import_react8.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null,
- wide ? (0, import_react8.createElement)(
-+ (0, import_react16.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
-+ pinned && !wide ? (0, import_react16.createElement)(PinMark) : null,
-+ wide ? (0, import_react16.createElement)(
++ (0, import_react17.createElement)(RoomAvatar, { room: room2, roster, size: 36 }),
++ pinned && !wide ? (0, import_react17.createElement)(PinMark) : null,
++ wide ? (0, import_react17.createElement)(
"span",
{ className: "bt-row-body" },
- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, room2.name), pinned ? (0, import_react8.createElement)(PinMark) : null),
- workingLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null
-+ (0, import_react16.createElement)("span", { className: "bt-row-name" }, (0, import_react16.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react16.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react16.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react16.createElement)(PinMark) : null),
-+ workingLine ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, preview) : null
++ (0, import_react17.createElement)("span", { className: "bt-row-name" }, (0, import_react17.createElement)("span", { className: face ? "bt-row-title bt-row-title-face" : "bt-row-title" }, face ? faceRowTitle(title) : title), face ? (0, import_react17.createElement)(FaceMark) : room2.sharedFrom ? (0, import_react17.createElement)("span", { className: "bt-tag", title: from }, t("Shared")) : null, pinned ? (0, import_react17.createElement)(PinMark) : null),
++ workingLine ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, preview) : null
) : null,
- wide && approval && !active ? (0, import_react8.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
-+ wide && alert && !active ? (0, import_react16.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
++ wide && alert && !active ? (0, import_react17.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
);
}
function AgentSessionRow({ session, agent, actions, renaming, index }) {
- const input = (0, import_react8.useRef)(null);
- (0, import_react8.useLayoutEffect)(() => {
-+ const input = (0, import_react16.useRef)(null);
-+ (0, import_react16.useLayoutEffect)(() => {
++ const input = (0, import_react17.useRef)(null);
++ (0, import_react17.useLayoutEffect)(() => {
if (!renaming) return;
input.current?.focus();
input.current?.select();
@@ -8144,11 +8368,11 @@ diff --git a/client.js b/client.js
})();
};
- return (0, import_react8.createElement)(
-+ return (0, import_react16.createElement)(
++ return (0, import_react17.createElement)(
"div",
{ className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } },
- (0, import_react8.createElement)("input", {
-+ (0, import_react16.createElement)("input", {
++ (0, import_react17.createElement)("input", {
ref: input,
defaultValue: session.title,
"aria-label": t("Rename"),
@@ -8157,7 +8381,7 @@ diff --git a/client.js b/client.js
);
}
- return (0, import_react8.createElement)(
-+ return (0, import_react16.createElement)(
++ return (0, import_react17.createElement)(
"button",
{
type: "button",
@@ -8167,8 +8391,8 @@ diff --git a/client.js b/client.js
},
- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, session.title),
- (0, import_react8.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "")
-+ (0, import_react16.createElement)("span", { className: "bt-agent-sub-title" }, session.title),
-+ (0, import_react16.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "")
++ (0, import_react17.createElement)("span", { className: "bt-agent-sub-title" }, session.title),
++ (0, import_react17.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "")
);
}
function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) {
@@ -8177,11 +8401,11 @@ diff --git a/client.js b/client.js
const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", ");
const open = active && wide;
- return (0, import_react8.createElement)(
-+ return (0, import_react16.createElement)(
++ return (0, import_react17.createElement)(
"div",
{ className: "bt-agent" },
- (0, import_react8.createElement)(
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
"button",
{
type: "button",
@@ -8191,39 +8415,39 @@ diff --git a/client.js b/client.js
},
- (0, import_react8.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react8.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })),
- wide ? (0, import_react8.createElement)(
-+ (0, import_react16.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react16.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })),
-+ wide ? (0, import_react16.createElement)(
++ (0, import_react17.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react17.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })),
++ wide ? (0, import_react17.createElement)(
"span",
{ className: "bt-row-body" },
- (0, import_react8.createElement)("span", { className: "bt-row-name" }, "DSH Agent"),
- preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null
-+ (0, import_react16.createElement)("span", { className: "bt-row-name" }, "DSH Agent"),
-+ preview ? (0, import_react16.createElement)("span", { className: "bt-row-preview" }, preview) : null
++ (0, import_react17.createElement)("span", { className: "bt-row-name" }, "DSH Agent"),
++ preview ? (0, import_react17.createElement)("span", { className: "bt-row-preview" }, preview) : null
) : null,
- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
-+ wide && unread && !active ? (0, import_react16.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
++ wide && unread && !active ? (0, import_react17.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null
),
- (0, import_react8.createElement)(
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
"div",
{ className: "bt-agent-sub", "data-open": open || void 0 },
- (0, import_react8.createElement)(
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
"div",
{ className: "bt-agent-sub-in" },
- (0, import_react8.createElement)(
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
"button",
{ type: "button", className: "bt-agent-sub-row bt-agent-new", style: { "--i": 0 }, onClick: () => {
void actions.newAgentSession(agent.id).catch((error) => actions.reportError(error, "agent-session"));
} },
- (0, import_react8.createElement)(PlusIcon),
- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, t("New session"))
-+ (0, import_react16.createElement)(PlusIcon),
-+ (0, import_react16.createElement)("span", { className: "bt-agent-sub-title" }, t("New session"))
++ (0, import_react17.createElement)(PlusIcon),
++ (0, import_react17.createElement)("span", { className: "bt-agent-sub-title" }, t("New session"))
),
- sessions.map((session, index) => (0, import_react8.createElement)(AgentSessionRow, {
-+ sessions.map((session, index) => (0, import_react16.createElement)(AgentSessionRow, {
++ sessions.map((session, index) => (0, import_react17.createElement)(AgentSessionRow, {
key: session.id,
session,
agent,
@@ -8233,8 +8457,8 @@ diff --git a/client.js b/client.js
function useListGlide(listRef, layout, key) {
- const tops = (0, import_react8.useRef)({ layout, map: /* @__PURE__ */ new Map() });
- (0, import_react8.useLayoutEffect)(() => {
-+ const tops = (0, import_react16.useRef)({ layout, map: /* @__PURE__ */ new Map() });
-+ (0, import_react16.useLayoutEffect)(() => {
++ const tops = (0, import_react17.useRef)({ layout, map: /* @__PURE__ */ new Map() });
++ (0, import_react17.useLayoutEffect)(() => {
const list = listRef.current;
if (!list) return;
const next = /* @__PURE__ */ new Map();
@@ -8242,7 +8466,7 @@ diff --git a/client.js b/client.js
tops.current = { layout, map: next };
}, [layout, key]);
}
-+var NodeActions = class extends import_react16.Component {
++var NodeActions = class extends import_react17.Component {
+ constructor(props) {
+ super(props);
+ this.state = { failed: false };
@@ -8254,33 +8478,33 @@ diff --git a/client.js b/client.js
+ console.error("flybot: nodeActions failed", error);
+ }
+ render() {
-+ return this.state.failed ? null : (0, import_react16.createElement)(this.props.component, { bot: this.props.bot });
++ return this.state.failed ? null : (0, import_react17.createElement)(this.props.component, { bot: this.props.bot });
+ }
+};
+function TreeNode({ node, rowProps, markedUnread, collapsed, toggle, actions }) {
+ const { bot, depth, children } = node;
+ const closed = collapsed[bot.id] === true;
-+ return (0, import_react16.createElement)(
-+ import_react16.Fragment,
++ return (0, import_react17.createElement)(
++ import_react17.Fragment,
+ null,
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
+ "div",
+ { className: "bt-tree-item", "data-depth": depth, style: { paddingLeft: `${depth * 16}px` } },
-+ children.length ? (0, import_react16.createElement)(
++ children.length ? (0, import_react17.createElement)(
+ "button",
+ { type: "button", className: "bt-tree-toggle", "aria-expanded": !closed, "aria-label": `${closed ? t("Expand") : t("Collapse")} ${bot.name}`, onClick: () => toggle(bot.id) },
-+ (0, import_react16.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾")
-+ ) : (0, import_react16.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }),
-+ (0, import_react16.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }),
-+ typeof actions === "function" ? (0, import_react16.createElement)(NodeActions, { component: actions, bot }) : null
++ (0, import_react17.createElement)("span", { className: "bt-tree-chev", "aria-hidden": true }, "▾")
++ ) : (0, import_react17.createElement)("span", { className: "bt-tree-toggle bt-tree-leaf", "aria-hidden": true }),
++ (0, import_react17.createElement)(BotRow, { ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }),
++ typeof actions === "function" ? (0, import_react17.createElement)(NodeActions, { component: actions, bot }) : null
+ ),
-+ closed ? null : children.map((child) => (0, import_react16.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions }))
++ closed ? null : children.map((child) => (0, import_react17.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions }))
+ );
+}
function TeamSidebar(props) {
const { wide, useRoster, useUi, useSessions, actions } = props;
- const listRef = (0, import_react8.useRef)(null);
-+ const listRef = (0, import_react16.useRef)(null);
++ const listRef = (0, import_react17.useRef)(null);
const roster = useRoster((value) => value);
const markedUnread = useUi((value) => value.unread);
props.useRegistry((value) => value);
@@ -8289,7 +8513,7 @@ diff --git a/client.js b/client.js
return JSON.stringify(stamps);
});
- const order = (0, import_react8.useMemo)(() => {
-+ const order = (0, import_react16.useMemo)(() => {
++ const order = (0, import_react17.useMemo)(() => {
const stamps = JSON.parse(updated);
const entries2 = [
- ...roster.bots.filter((bot) => bot.id !== roster.mainBotId).map((bot) => ({ kind: "bot", item: bot, at: stamps[currentPart(bot)] ?? bot.createdAt })),
@@ -8307,7 +8531,7 @@ diff --git a/client.js b/client.js
}, [roster, updated]);
const main = roster.byId[roster.mainBotId];
+ const structure = useSidebarStructure();
-+ const [collapsed, setCollapsed] = (0, import_react16.useState)({});
++ const [collapsed, setCollapsed] = (0, import_react17.useState)({});
+ const toggle = (id) => setCollapsed((value) => ({ ...value, [id]: !value[id] }));
+ const inTree = (entry) => entry.kind === "bot" && !entry.item.remote;
+ const listed = (entry) => inTree(entry) && structure?.include?.(entry.item.id) !== false;
@@ -8315,16 +8539,16 @@ diff --git a/client.js b/client.js
useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(","));
useLandOnMainBot(roster, useSessions, actions);
const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide };
-@@ -5045,39 +5045,42 @@
+@@ -5045,39 +5045,43 @@
event.preventDefault();
actions.openPalette(event.key);
};
- return (0, import_react8.createElement)(
-+ return (0, import_react16.createElement)(
++ return (0, import_react17.createElement)(
"div",
{ className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch },
- wide ? (0, import_react8.createElement)(
-+ wide ? (0, import_react16.createElement)(
++ wide ? (0, import_react17.createElement)(
"div",
{ className: "bt-side-top" },
- (0, import_react8.createElement)("span", { className: "bt-brand" }, (0, import_react8.createElement)(BrandMark, { size: 22 }), (0, import_react8.createElement)(BrandName)),
@@ -8333,31 +8557,32 @@ diff --git a/client.js b/client.js
- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon))
- ) : (0, import_react8.createElement)(
- import_react8.Fragment,
-+ (0, import_react16.createElement)("span", { className: "bt-brand" }, (0, import_react16.createElement)(BrandMark, { size: 22 }), (0, import_react16.createElement)(BrandName)),
-+ (0, import_react16.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
-+ (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react16.createElement)(SearchIcon)),
-+ (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react16.createElement)(PlusIcon))
-+ ) : (0, import_react16.createElement)(
-+ import_react16.Fragment,
++ (0, import_react17.createElement)("span", { className: "bt-brand" }, (0, import_react17.createElement)(BrandMark, { size: 22 }), (0, import_react17.createElement)(BrandName)),
++ (0, import_react17.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
++ (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react17.createElement)(SearchIcon)),
++ (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react17.createElement)(PlusIcon))
++ ) : (0, import_react17.createElement)(
++ import_react17.Fragment,
null,
- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon))
-+ (0, import_react16.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
-+ (0, import_react16.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react16.createElement)(PlusIcon))
++ (0, import_react17.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
++ (0, import_react17.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react17.createElement)(PlusIcon))
),
- wide && roster.readOnly ? (0, import_react8.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
- (0, import_react8.createElement)(
-+ wide && roster.readOnly ? (0, import_react16.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
-+ (0, import_react16.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
-+ (0, import_react16.createElement)(KanbanRow, { wide, useUi, actions }),
-+ (0, import_react16.createElement)(
++ wide && roster.readOnly ? (0, import_react17.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
++ (0, import_react17.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
++ (0, import_react17.createElement)(KanbanRow, { wide, useUi, actions }),
++ (0, import_react17.createElement)(ProjectsNav, { wide }),
++ (0, import_react17.createElement)(
"div",
{ ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") },
- main ? (0, import_react8.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react8.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")),
- order.map((entry) => entry.kind === "bot" ? (0, import_react8.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react8.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react8.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true }))
-+ tree ? (0, import_react16.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react16.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react16.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react16.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")),
-+ order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react16.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react16.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react16.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
-+ wide ? (0, import_react16.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react16.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null
++ tree ? (0, import_react17.createElement)("div", { className: "bt-tree", role: "group", "aria-label": t("Bot tree") }, tree.map((node) => (0, import_react17.createElement)(TreeNode, { key: node.bot.id, node, rowProps, markedUnread, collapsed, toggle, actions: structure?.nodeActions }))) : main ? (0, import_react17.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react17.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")),
++ order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react17.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react17.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react17.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })),
++ wide ? (0, import_react17.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react17.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null
)
);
}
@@ -8369,12 +8594,12 @@ diff --git a/client.js b/client.js
- const [dshError, setDshError] = (0, import_react8.useState)("");
- const button = (0, import_react8.useRef)(null);
- (0, import_react8.useEffect)(() => {
-+ const [open, setOpen] = (0, import_react16.useState)(false);
-+ const [box, setBox] = (0, import_react16.useState)(null);
-+ const [error, setError] = (0, import_react16.useState)("");
-+ const [dshError, setDshError] = (0, import_react16.useState)("");
-+ const button = (0, import_react16.useRef)(null);
-+ (0, import_react16.useEffect)(() => {
++ const [open, setOpen] = (0, import_react17.useState)(false);
++ const [box, setBox] = (0, import_react17.useState)(null);
++ const [error, setError] = (0, import_react17.useState)("");
++ const [dshError, setDshError] = (0, import_react17.useState)("");
++ const button = (0, import_react17.useRef)(null);
++ (0, import_react17.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return;
@@ -8390,11 +8615,11 @@ diff --git a/client.js b/client.js
- return (0, import_react8.createElement)(
- import_react8.Fragment,
+ const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update FlyBot core {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current });
-+ return (0, import_react16.createElement)(
-+ import_react16.Fragment,
++ return (0, import_react17.createElement)(
++ import_react17.Fragment,
null,
- (0, import_react8.createElement)("button", {
-+ (0, import_react16.createElement)("button", {
++ (0, import_react17.createElement)("button", {
ref: button,
type: "button",
className: "bt-round bt-update",
@@ -8404,30 +8629,30 @@ diff --git a/client.js b/client.js
onClick: toggle
- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)(
-+ }, (0, import_react16.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react16.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
-+ open && box ? (0, import_react_dom3.createPortal)((0, import_react16.createElement)(
++ }, (0, import_react17.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react17.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
++ open && box ? (0, import_react_dom4.createPortal)((0, import_react17.createElement)(
"div",
- { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box },
- update?.available || phase !== "idle" ? (0, import_react8.createElement)(
- import_react8.Fragment,
+ { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("FlyBot core update") : t("DSH update"), style: box },
-+ update?.available || phase !== "idle" ? (0, import_react16.createElement)(
-+ import_react16.Fragment,
++ update?.available || phase !== "idle" ? (0, import_react17.createElement)(
++ import_react17.Fragment,
null,
- (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })),
- (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })),
- message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
- (0, import_react8.createElement)(
-+ (0, import_react16.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })),
-+ (0, import_react16.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })),
-+ message ? (0, import_react16.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("FlyBot core {version}", { version: update.latest })),
++ (0, import_react17.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })),
++ message ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
++ (0, import_react17.createElement)(
"div",
{ className: "bt-update-actions" },
- update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
- phase === "installed" ? null : (0, import_react8.createElement)("button", {
-+ update.notesUrl ? (0, import_react16.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
-+ phase === "installed" ? null : (0, import_react16.createElement)("button", {
++ update.notesUrl ? (0, import_react17.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
++ phase === "installed" ? null : (0, import_react17.createElement)("button", {
type: "button",
className: "bt-update-go",
disabled: phase === "installing",
@@ -8436,23 +8661,23 @@ diff --git a/client.js b/client.js
)
) : null,
- dshRow ? (0, import_react8.createElement)(
-+ dshRow ? (0, import_react16.createElement)(
++ dshRow ? (0, import_react17.createElement)(
"div",
{ className: "bt-update-dsh" },
- (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
- (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText),
- dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
- (0, import_react8.createElement)(
-+ (0, import_react16.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
-+ (0, import_react16.createElement)("div", { className: "bt-update-text" }, dshText),
-+ dshMessage ? (0, import_react16.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
++ (0, import_react17.createElement)("div", { className: "bt-update-text" }, dshText),
++ dshMessage ? (0, import_react17.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
++ (0, import_react17.createElement)(
"div",
{ className: "bt-update-actions" },
- dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => {
- }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", {
-+ dsh.kind === "app" ? dsh.available ? (0, import_react16.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => {
-+ }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react16.createElement)("button", {
++ dsh.kind === "app" ? dsh.available ? (0, import_react17.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => {
++ }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react17.createElement)("button", {
type: "button",
className: "bt-update-go",
disabled: dshPhase === "installing",
@@ -8484,11 +8709,11 @@ diff --git a/client.js b/client.js
- setTip(false);
- };
- return (0, import_react8.createElement)(
-+ return (0, import_react16.createElement)(
++ return (0, import_react17.createElement)(
"span",
{ className: "bt-connect-wrap" },
- (0, import_react8.createElement)(
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
"button",
- {
- ref: button,
@@ -8511,20 +8736,20 @@ diff --git a/client.js b/client.js
- t("Plugin compatibility is not fully verified.")
- ), document.body) : null
+ { type: "button", className: "bt-connect bt-connect-foot", onClick: () => actions.openConnectors() },
-+ (0, import_react16.createElement)(PlugIcon),
++ (0, import_react17.createElement)(PlugIcon),
+ t("Connect apps")
+ )
);
}
function YouAvatar() {
- return (0, import_react8.createElement)(
-+ return (0, import_react16.createElement)(
++ return (0, import_react17.createElement)(
"span",
{ className: "bt-you-wrap" },
- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })),
- (0, import_react8.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings"))
-+ (0, import_react16.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react16.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })),
-+ (0, import_react16.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings"))
++ (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })),
++ (0, import_react17.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings"))
);
}
var openShellSettings = null;
@@ -8535,7 +8760,7 @@ diff --git a/client.js b/client.js
+var MODELS_STEP_ID = "flybot-models";
+var MODELS_STEP_ORDER = 1e3;
+function ModelsSettingsStep({ explicit, complete, openSection }) {
-+ (0, import_react16.useLayoutEffect)(() => {
++ (0, import_react17.useLayoutEffect)(() => {
+ if (explicit) openSection("models");
+ complete();
+ }, []);
@@ -8553,8 +8778,8 @@ diff --git a/client.js b/client.js
function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) {
- const [open, setOpen] = (0, import_react8.useState)(false);
- (0, import_react8.useLayoutEffect)(() => {
-+ const [open, setOpen] = (0, import_react16.useState)(false);
-+ (0, import_react16.useLayoutEffect)(() => {
++ const [open, setOpen] = (0, import_react17.useState)(false);
++ (0, import_react17.useLayoutEffect)(() => {
if (!seat) return void 0;
actions.seatAccount(true);
return () => actions.seatAccount(false);
@@ -8562,13 +8787,13 @@ diff --git a/client.js b/client.js
- const [box, setBox] = (0, import_react8.useState)(null);
- const button = (0, import_react8.useRef)(null);
- (0, import_react8.useEffect)(() => {
-+ const [box, setBox] = (0, import_react16.useState)(null);
-+ const button = (0, import_react16.useRef)(null);
-+ (0, import_react16.useEffect)(() => {
++ const [box, setBox] = (0, import_react17.useState)(null);
++ const button = (0, import_react17.useRef)(null);
++ (0, import_react17.useEffect)(() => {
if (settingsOpen) setOpen(false);
}, [settingsOpen]);
- (0, import_react8.useEffect)(() => {
-+ (0, import_react16.useEffect)(() => {
++ (0, import_react17.useEffect)(() => {
if (!openSettings) return void 0;
openShellSettings = openSettings;
return () => {
@@ -8576,7 +8801,7 @@ diff --git a/client.js b/client.js
};
}, [openSettings]);
- (0, import_react8.useEffect)(() => {
-+ (0, import_react16.useEffect)(() => {
++ (0, import_react17.useEffect)(() => {
if (!openOnboarding) return void 0;
openShellOnboarding = openOnboarding;
return () => {
@@ -8584,7 +8809,7 @@ diff --git a/client.js b/client.js
};
}, [openOnboarding]);
- (0, import_react8.useEffect)(() => {
-+ (0, import_react16.useEffect)(() => {
++ (0, import_react17.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return;
@@ -8606,30 +8831,30 @@ diff --git a/client.js b/client.js
];
- return (0, import_react8.createElement)(
- import_react8.Fragment,
-+ return (0, import_react16.createElement)(
-+ import_react16.Fragment,
++ return (0, import_react17.createElement)(
++ import_react17.Fragment,
null,
- (0, import_react8.createElement)(
-+ (0, import_react16.createElement)(
++ (0, import_react17.createElement)(
"button",
{ ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle },
- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 }))
-+ (0, import_react16.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react16.createElement)(BotMark, { look: MAIN_LOOK, size: 24 }))
++ (0, import_react17.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react17.createElement)(BotMark, { look: MAIN_LOOK, size: 24 }))
),
- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)(
-+ open && box ? (0, import_react_dom3.createPortal)((0, import_react16.createElement)(
++ open && box ? (0, import_react_dom4.createPortal)((0, import_react17.createElement)(
"div",
{ className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box },
- groups.map((group, index) => (0, import_react8.createElement)(
- import_react8.Fragment,
-+ (0, import_react16.createElement)(FlyBotAccountHeader, { actions }),
-+ groups.map((group, index) => (0, import_react16.createElement)(
-+ import_react16.Fragment,
++ (0, import_react17.createElement)(FlyBotAccountHeader, { actions }),
++ groups.map((group, index) => (0, import_react17.createElement)(
++ import_react17.Fragment,
{ key: index },
- index > 0 ? (0, import_react8.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
- group.map(([label, Icon, run, hint]) => (0, import_react8.createElement)("button", {
-+ index > 0 ? (0, import_react16.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
-+ group.map(([label, Icon, run, hint]) => (0, import_react16.createElement)("button", {
++ index > 0 ? (0, import_react17.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
++ group.map(([label, Icon, run, hint]) => (0, import_react17.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
@@ -8638,7 +8863,7 @@ diff --git a/client.js b/client.js
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null))
-+ }, (0, import_react16.createElement)(Icon), (0, import_react16.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react16.createElement)("span", { className: "bt-menu-hint" }, hint) : null))
++ }, (0, import_react17.createElement)(Icon), (0, import_react17.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, hint) : null))
))
), document.body) : null
);
@@ -8648,8 +8873,8 @@ diff --git a/client.js b/client.js
});
- const lastBot = (0, import_react8.useRef)(null);
- (0, import_react8.useEffect)(() => {
-+ const lastBot = (0, import_react16.useRef)(null);
-+ (0, import_react16.useEffect)(() => {
++ const lastBot = (0, import_react17.useRef)(null);
++ (0, import_react17.useEffect)(() => {
if (!roster.ready || !roster.mainBotId) return void 0;
const [id, blank3] = mainView.split("|");
const bot = id === "" ? void 0 : roster.byId[id];
@@ -8666,20 +8891,20 @@ diff --git a/client.js b/client.js
const seated = props.useUi((value) => value.accountSeated === true);
if (seated) return null;
- return (0, import_react8.createElement)("span", { className: "bt-foot-account" }, (0, import_react8.createElement)(AccountLauncher, { ...props, seat: false }));
-+ return (0, import_react16.createElement)("span", { className: "bt-foot-account" }, (0, import_react16.createElement)(AccountLauncher, { ...props, seat: false }));
++ return (0, import_react17.createElement)("span", { className: "bt-foot-account" }, (0, import_react17.createElement)(AccountLauncher, { ...props, seat: false }));
}
function BotReturn({ wide, useSurface, actions }) {
const mode = useSurface((value) => value.mode);
if (mode !== "agent") return null;
- return (0, import_react8.createElement)(
-+ return (0, import_react16.createElement)(
++ return (0, import_react17.createElement)(
"button",
- { type: "button", className: "bt-return", "aria-label": t("DS Bot"), title: t("DS Bot"), onClick: () => actions.setSurface("bot") },
- (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 20 }),
- wide ? (0, import_react8.createElement)("span", { className: "bt-return-name" }, t("DS Bot")) : null
+ { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") },
-+ (0, import_react16.createElement)(FlyBotMark, { size: 20 }),
-+ wide ? (0, import_react16.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
++ (0, import_react17.createElement)(FlyBotMark, { size: 20 }),
++ wide ? (0, import_react17.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null
);
}
var RETURN_CSS = `
@@ -8696,11 +8921,11 @@ diff --git a/client.js b/client.js
`;
function BrandMark({ size = 24 }) {
- return (0, import_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size }));
-+ return (0, import_react16.createElement)("span", { className: "bt-brand-mark" }, (0, import_react16.createElement)(FlyBotMark, { size }));
++ return (0, import_react17.createElement)("span", { className: "bt-brand-mark" }, (0, import_react17.createElement)(FlyBotMark, { size }));
}
function BrandName() {
- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot");
-+ return (0, import_react16.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot"));
++ return (0, import_react17.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, t("FlyBot"));
}
var SIDEBAR_CSS = `
.bt-side-top .bt-brand{cursor:default;padding-left:6px}
@@ -8727,8 +8952,8 @@ diff --git a/client.js b/client.js
+.bt-row-wrap:hover .bt-row-settings,.bt-row-wrap:focus-within .bt-row-settings{opacity:1;pointer-events:auto;color:var(--bt-ink-2);background:var(--bt-hover)}
+.bt-row-settings:hover{opacity:1;color:var(--bt-ink-2);background:var(--bt-hover)}
+.bt-row-settings svg{width:15px;height:15px}
- `;
-
++`;
++
+// src/client/busy-ack-logic.js
+function busyAckText(hostAck) {
+ return typeof hostAck?.text === "string" ? hostAck.text.trim() : "";
@@ -8756,11 +8981,11 @@ diff --git a/client.js b/client.js
+}
+
+// src/client/busy-ack.js
-+var import_react17 = require("react");
-+var import_react_dom4 = require("react-dom");
++var import_react18 = require("react");
++var import_react_dom5 = require("react-dom");
+function useRunningHost2(active) {
-+ const [host, setHost] = (0, import_react17.useState)(null);
-+ (0, import_react17.useEffect)(() => {
++ const [host, setHost] = (0, import_react18.useState)(null);
++ (0, import_react18.useEffect)(() => {
+ if (!active) {
+ setHost(null);
+ return void 0;
@@ -8794,11 +9019,11 @@ diff --git a/client.js b/client.js
+ const visible = room2 ? roomBusyAckVisible({ ack: memberAck, pendingBusy }) : running && busyAckVisible({ hostAck, pendingBusy, streamingText });
+ const bot = room2 ? roster.byId[memberAck?.botId] : roster.byId[sessionId];
+ const host = useRunningHost2(visible);
-+ const shown = (0, import_react17.useRef)(null);
++ const shown = (0, import_react18.useRef)(null);
+ if (visible) shown.current = room2 ? { text: memberAck?.text ?? "", at: memberAck?.at ?? Date.now() } : { text: busyAckText(hostAck), at: hostAck?.at ?? Date.now() };
+ const text2 = shown.current?.text ?? "";
+ if (!visible || !host || !shown.current || !bot) return null;
-+ return (0, import_react_dom4.createPortal)((0, import_react17.createElement)(
++ return (0, import_react_dom5.createPortal)((0, import_react18.createElement)(
+ "div",
+ {
+ className: "bt-busy-ack",
@@ -8808,22 +9033,22 @@ diff --git a/client.js b/client.js
+ "aria-busy": text2 ? void 0 : "true",
+ "data-pending": text2 ? void 0 : ""
+ },
-+ (0, import_react17.createElement)(BotMark, { bot, size: 28, state: "thinking", live: true }),
-+ (0, import_react17.createElement)(
++ (0, import_react18.createElement)(BotMark, { bot, size: 28, state: "thinking", live: true }),
++ (0, import_react18.createElement)(
+ "div",
+ { className: "bt-astack" },
-+ (0, import_react17.createElement)(
++ (0, import_react18.createElement)(
+ "div",
+ { className: "bt-msg" },
-+ (0, import_react17.createElement)(
++ (0, import_react18.createElement)(
+ "div",
+ { className: "bt-msg-line" },
-+ text2 ? (0, import_react17.createElement)("div", { className: "bt-bubble" }, text2) : (0, import_react17.createElement)(
++ text2 ? (0, import_react18.createElement)("div", { className: "bt-bubble" }, text2) : (0, import_react18.createElement)(
+ "div",
+ { className: "bt-bubble bt-busy-dots", "aria-hidden": "true" },
-+ (0, import_react17.createElement)("span", null),
-+ (0, import_react17.createElement)("span", null),
-+ (0, import_react17.createElement)("span", null)
++ (0, import_react18.createElement)("span", null),
++ (0, import_react18.createElement)("span", null),
++ (0, import_react18.createElement)("span", null)
+ )
+ )
+ )
@@ -8839,23 +9064,23 @@ diff --git a/client.js b/client.js
+.bt-busy-dots>span:nth-child(2){animation-delay:.2s}.bt-busy-dots>span:nth-child(3){animation-delay:.4s}
+@keyframes bt-busy-blink{0%,80%,100%{opacity:.25}40%{opacity:1}}
+@media (prefers-reduced-motion:reduce){.bt-busy-dots>span{animation:none;opacity:.6}}
-+`;
-+
+ `;
+
// src/client/header.js
-var import_react9 = 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 import_react18 = require("react");
-+var import_react_dom5 = require("react-dom");
-+var GoArrow = () => (0, import_react18.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react18.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" }));
++var import_react19 = require("react");
++var import_react_dom6 = require("react-dom");
++var GoArrow = () => (0, import_react19.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_react19.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_react18.useState)(false);
-+ (0, import_react18.useEffect)(() => {
++ const [shown, setShown] = (0, import_react19.useState)(false);
++ (0, import_react19.useEffect)(() => {
const left = seen + MEMORY_NOTICE_MS - Date.now();
setShown(seen > 0 && left > 0);
if (seen === 0 || left <= 0) return void 0;
@@ -8865,19 +9090,19 @@ diff --git a/client.js b/client.js
function MemoryNotice({ shown }) {
- const inner = (0, import_react9.useRef)(null);
- const [width, setWidth] = (0, import_react9.useState)(0);
-+ const inner = (0, import_react18.useRef)(null);
-+ const [width, setWidth] = (0, import_react18.useState)(0);
++ const inner = (0, import_react19.useRef)(null);
++ const [width, setWidth] = (0, import_react19.useState)(0);
const label = t("Memory updated");
- (0, import_react9.useLayoutEffect)(() => {
-+ (0, import_react18.useLayoutEffect)(() => {
++ (0, import_react19.useLayoutEffect)(() => {
if (shown) setWidth(inner.current?.scrollWidth ?? 0);
}, [label, shown]);
- return (0, import_react9.createElement)(
-+ return (0, import_react18.createElement)(
++ return (0, import_react19.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_react18.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react18.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react18.createElement)(MemoryIcon)), label)
++ (0, import_react19.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react19.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react19.createElement)(MemoryIcon)), label)
);
}
-function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) {
@@ -8890,7 +9115,7 @@ diff --git a/client.js b/client.js
return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : "";
});
- (0, import_react9.useEffect)(() => {
-+ (0, import_react18.useEffect)(() => {
++ (0, import_react19.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");
@@ -8924,11 +9149,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_react18.createElement)(
++ return (0, import_react19.createElement)(
"div",
{ className: "bt-head" },
- (0, import_react9.createElement)(
-+ (0, import_react18.createElement)(
++ (0, import_react19.createElement)(
"button",
{
type: "button",
@@ -8944,25 +9169,26 @@ 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_react18.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react18.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null,
-+ (0, import_react18.createElement)("span", { className: face ? "bt-pill-name bt-pill-name-face" : "bt-pill-name", title: face ? full2 : void 0 }, name || title || t("New chat")),
-+ (0, import_react18.createElement)(MemoryNotice, { shown: memoryNotice }),
-+ (0, import_react18.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react18.createElement)(GoArrow))
- ),
+- ),
- (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
- (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }),
- (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions })
-+ (0, import_react18.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
-+ (0, import_react18.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }),
-+ (0, import_react18.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }),
-+ (0, import_react18.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive })
++ bot ? (0, import_react19.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react19.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null,
++ (0, import_react19.createElement)("span", { className: face ? "bt-pill-name bt-pill-name-face" : "bt-pill-name", title: face ? full2 : void 0 }, name || title || t("New chat")),
++ (0, import_react19.createElement)(MemoryNotice, { shown: memoryNotice }),
++ (0, import_react19.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react19.createElement)(GoArrow))
++ ),
++ (0, import_react19.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
++ (0, import_react19.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }),
++ (0, import_react19.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }),
++ (0, import_react19.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive })
);
}
function useTranscriptView() {
- const [view, setView] = (0, import_react9.useState)(null);
- (0, import_react9.useEffect)(() => {
-+ const [view, setView] = (0, import_react18.useState)(null);
-+ (0, import_react18.useEffect)(() => {
++ const [view, setView] = (0, import_react19.useState)(null);
++ (0, import_react19.useEffect)(() => {
let frame = 0;
let watched = null;
const observer = new MutationObserver(() => schedule());
@@ -8982,24 +9208,24 @@ diff --git a/client.js b/client.js
- const [release, setRelease] = (0, import_react9.useState)({ base: null, dismissed: false });
- const [above, setAbove] = (0, import_react9.useState)({ seen: false, dismissed: false });
- (0, import_react9.useEffect)(() => {
-+ const [release, setRelease] = (0, import_react18.useState)({ base: null, dismissed: false });
-+ const [above, setAbove] = (0, import_react18.useState)({ seen: false, dismissed: false });
-+ (0, import_react18.useEffect)(() => {
++ const [release, setRelease] = (0, import_react19.useState)({ base: null, dismissed: false });
++ const [above, setAbove] = (0, import_react19.useState)({ seen: false, dismissed: false });
++ (0, import_react19.useEffect)(() => {
setRelease({ base: unpinned ? replies : null, dismissed: false });
}, [unpinned]);
- (0, import_react9.useEffect)(() => {
-+ (0, import_react18.useEffect)(() => {
++ (0, import_react19.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_react18.useEffect)(() => {
++ (0, import_react19.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_react18.useEffect)(() => {
++ (0, import_react19.useEffect)(() => {
const scroller = document.querySelector("[data-conversation-scroll]");
if (!scroller) return void 0;
scroller.toggleAttribute("data-bt-news", showDown);
@@ -9008,19 +9234,19 @@ diff --git a/client.js b/client.js
if (!view || !showDown && !showUp) return null;
const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages");
- const pill = (direction, count, style, onJump, onDismiss) => (0, import_react9.createElement)(
-+ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react18.createElement)(
++ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react19.createElement)(
"div",
{ className: "bt-news", "data-direction": direction, style },
- (0, import_react9.createElement)(
-+ (0, import_react18.createElement)(
++ (0, import_react19.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_react18.createElement)("span", { className: "bt-news-ico" }, (0, import_react18.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
++ (0, import_react19.createElement)("span", { className: "bt-news-ico" }, (0, import_react19.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)),
label(count)
),
- (0, import_react9.createElement)("button", {
-+ (0, import_react18.createElement)("button", {
++ (0, import_react19.createElement)("button", {
type: "button",
className: "bt-news-x",
"aria-label": t("Dismiss new messages"),
@@ -9029,7 +9255,7 @@ diff --git a/client.js b/client.js
onDismiss();
}
- }, (0, import_react9.createElement)(CloseIcon))
-+ }, (0, import_react18.createElement)(CloseIcon))
++ }, (0, import_react19.createElement)(CloseIcon))
);
const toLatest = () => {
const scroller = document.querySelector("[data-conversation-scroll]");
@@ -9040,9 +9266,9 @@ diff --git a/client.js b/client.js
- return (0, import_react_dom4.createPortal)(
- (0, import_react9.createElement)(
- import_react9.Fragment,
-+ return (0, import_react_dom5.createPortal)(
-+ (0, import_react18.createElement)(
-+ import_react18.Fragment,
++ return (0, import_react_dom6.createPortal)(
++ (0, import_react19.createElement)(
++ import_react19.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
@@ -9169,8 +9395,8 @@ diff --git a/client.js b/client.js
// src/client/composer.js
-var import_react10 = require("react");
-var import_react_dom5 = require("react-dom");
-+var import_react19 = require("react");
-+var import_react_dom6 = require("react-dom");
++var import_react20 = require("react");
++var import_react_dom7 = require("react-dom");
var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition;
var keepComposerFocus = (event) => event.preventDefault();
function useDictation(onText) {
@@ -9178,15 +9404,15 @@ diff --git a/client.js b/client.js
- const [note, setNote] = (0, import_react10.useState)("");
- const recognition = (0, import_react10.useRef)(null);
- const latest = (0, import_react10.useRef)(onText);
-+ const [listening, setListening] = (0, import_react19.useState)(false);
-+ const [note, setNote] = (0, import_react19.useState)("");
-+ const recognition = (0, import_react19.useRef)(null);
-+ const latest = (0, import_react19.useRef)(onText);
++ const [listening, setListening] = (0, import_react20.useState)(false);
++ const [note, setNote] = (0, import_react20.useState)("");
++ const recognition = (0, import_react20.useRef)(null);
++ const latest = (0, import_react20.useRef)(onText);
latest.current = onText;
- (0, import_react10.useEffect)(() => () => recognition.current?.abort(), []);
- (0, import_react10.useEffect)(() => {
-+ (0, import_react19.useEffect)(() => () => recognition.current?.abort(), []);
-+ (0, import_react19.useEffect)(() => {
++ (0, import_react20.useEffect)(() => () => recognition.current?.abort(), []);
++ (0, import_react20.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
@@ -9214,14 +9440,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_react19.createElement)(
++ return (0, import_react20.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_react19.createElement)(
++ return (0, import_react20.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 })
@@ -9235,7 +9461,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_react19.useEffect)(() => {
++ (0, import_react20.useEffect)(() => {
if (!quote || !inputActions) return;
actions.clearQuote(quote.token);
const excerpt = quote.text.replace(/\s+/g, " ").trim();
@@ -9254,11 +9480,11 @@ diff --git a/client.js b/client.js
const pluginMic = actions.speech() !== void 0;
- return (0, import_react10.createElement)(
- import_react10.Fragment,
-+ return (0, import_react19.createElement)(
-+ import_react19.Fragment,
++ return (0, import_react20.createElement)(
++ import_react20.Fragment,
null,
- pluginMic ? null : (0, import_react10.createElement)("button", {
-+ pluginMic ? null : (0, import_react19.createElement)("button", {
++ pluginMic ? null : (0, import_react20.createElement)("button", {
type: "button",
className: "bt-mic",
"aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"),
@@ -9268,8 +9494,8 @@ diff --git a/client.js b/client.js
onClick: () => dictation.listening ? dictation.stop() : dictation.start()
- }, (0, import_react10.createElement)(MicIcon)),
- (0, import_react10.createElement)("button", {
-+ }, (0, import_react19.createElement)(MicIcon)),
-+ (0, import_react19.createElement)("button", {
++ }, (0, import_react20.createElement)(MicIcon)),
++ (0, import_react20.createElement)("button", {
type: "button",
className: "bt-voice",
"aria-label": t("Start voice chat"),
@@ -9278,8 +9504,8 @@ diff --git a/client.js b/client.js
onClick: () => actions.openVoice(sessionId)
- }, (0, import_react10.createElement)(WaveIcon, { size: 18 })),
- dictation.note ? (0, import_react10.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null
-+ }, (0, import_react19.createElement)(WaveIcon, { size: 18 })),
-+ dictation.note ? (0, import_react19.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null
++ }, (0, import_react20.createElement)(WaveIcon, { size: 18 })),
++ dictation.note ? (0, import_react20.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null
);
}
function menuIcons(roster) {
@@ -9288,21 +9514,21 @@ diff --git a/client.js b/client.js
if (!row.bot) return PeopleIcon;
const id = row.bot.id;
- if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react10.createElement)(BotMark, { bot: roster.getSnapshot().byId[id] ?? row.bot, size }));
-+ if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react19.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size }));
++ if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react20.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size }));
return marks.get(id);
};
}
function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
const roster = useRoster((value) => value);
- const [open, setOpen] = (0, import_react10.useState)(null);
-+ const [open, setOpen] = (0, import_react19.useState)(null);
++ const [open, setOpen] = (0, import_react20.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_react19.useRef)(null);
-+ const menu = (0, import_react19.useRef)(null);
-+ (0, import_react19.useEffect)(() => {
++ const button = (0, import_react20.useRef)(null);
++ const menu = (0, import_react20.useRef)(null);
++ (0, import_react20.useEffect)(() => {
if (!open) return void 0;
const onDown = (event) => {
if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null);
@@ -9312,11 +9538,11 @@ diff --git a/client.js b/client.js
const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat();
- return (0, import_react10.createElement)(
- import_react10.Fragment,
-+ return (0, import_react19.createElement)(
-+ import_react19.Fragment,
++ return (0, import_react20.createElement)(
++ import_react20.Fragment,
null,
- (0, import_react10.createElement)("button", {
-+ (0, import_react19.createElement)("button", {
++ (0, import_react20.createElement)("button", {
ref: button,
type: "button",
className: "bt-plus",
@@ -9326,8 +9552,8 @@ diff --git a/client.js b/client.js
onClick: toggle
- }, (0, import_react10.createElement)(PlusIcon)),
- shown ? (0, import_react_dom5.createPortal)((0, import_react10.createElement)(
-+ }, (0, import_react19.createElement)(PlusIcon)),
-+ shown ? (0, import_react_dom6.createPortal)((0, import_react19.createElement)(
++ }, (0, import_react20.createElement)(PlusIcon)),
++ shown ? (0, import_react_dom7.createPortal)((0, import_react20.createElement)(
"div",
{
ref: menu,
@@ -9338,9 +9564,9 @@ diff --git a/client.js b/client.js
- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react10.createElement)(PaperclipIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
- canReference ? (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react10.createElement)(ChatsIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react10.createElement)(ClockIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task")))
-+ (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react19.createElement)(PaperclipIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
-+ canReference ? (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react19.createElement)(ChatsIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
-+ (0, import_react19.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react19.createElement)(ClockIcon), (0, import_react19.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task")))
++ (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react20.createElement)(PaperclipIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
++ canReference ? (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react20.createElement)(ChatsIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
++ (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react20.createElement)(ClockIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task")))
), document.body) : null
);
}
@@ -9781,8 +10007,8 @@ diff --git a/client.js b/client.js
// src/client/model-menu.js
-var import_react13 = require("react");
-var import_react_dom6 = require("react-dom");
-+var import_react20 = require("react");
-+var import_react_dom7 = require("react-dom");
++var import_react21 = require("react");
++var import_react_dom8 = require("react-dom");
var MENU_EXIT_MS = 120;
function groupModels(models) {
const groups = /* @__PURE__ */ new Map();
@@ -9801,8 +10027,8 @@ diff --git a/client.js b/client.js
function useEfforts(served, ref, actions) {
- const [fetched, setFetched] = (0, import_react13.useState)(void 0);
- (0, import_react13.useEffect)(() => {
-+ const [fetched, setFetched] = (0, import_react20.useState)(void 0);
-+ (0, import_react20.useEffect)(() => {
++ const [fetched, setFetched] = (0, import_react21.useState)(void 0);
++ (0, import_react21.useEffect)(() => {
setFetched(void 0);
if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0;
let live = true;
@@ -9812,15 +10038,15 @@ diff --git a/client.js b/client.js
function EffortRows({ reasoning, current, onPick }) {
- return (0, import_react13.createElement)(
- import_react13.Fragment,
-+ return (0, import_react20.createElement)(
-+ import_react20.Fragment,
++ return (0, import_react21.createElement)(
++ import_react21.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_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ (0, import_react20.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
-+ effortRows(reasoning).map((row) => (0, import_react20.createElement)(
++ (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
++ (0, import_react21.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
++ effortRows(reasoning).map((row) => (0, import_react21.createElement)(
"button",
{
key: row.effort ?? "default",
@@ -9830,21 +10056,21 @@ diff --git a/client.js b/client.js
},
- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, row.label),
- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react13.createElement)(CheckIcon) : null)
-+ (0, import_react20.createElement)("span", { className: "bt-menu-label" }, row.label),
-+ (0, import_react20.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react20.createElement)(CheckIcon) : null)
++ (0, import_react21.createElement)("span", { className: "bt-menu-label" }, row.label),
++ (0, import_react21.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react21.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_react20.createElement)(
-+ import_react20.Fragment,
++ return groupModels(roster.models).map(([provider, list]) => (0, import_react21.createElement)(
++ import_react21.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_react20.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
-+ list.map((model) => (0, import_react20.createElement)(
++ (0, import_react21.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
++ list.map((model) => (0, import_react21.createElement)(
"button",
{
key: model.ref,
@@ -9855,25 +10081,25 @@ diff --git a/client.js b/client.js
- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, model.name),
- model.ref === roster.defaultModel ? (0, import_react13.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null,
- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react13.createElement)(CheckIcon) : null)
-+ (0, import_react20.createElement)("span", { className: "bt-menu-label" }, model.name),
-+ model.ref === roster.defaultModel ? (0, import_react20.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null,
-+ (0, import_react20.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react20.createElement)(CheckIcon) : null)
++ (0, import_react21.createElement)("span", { className: "bt-menu-label" }, model.name),
++ model.ref === roster.defaultModel ? (0, import_react21.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null,
++ (0, import_react21.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react21.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_react20.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react20.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" }));
++var Caret = () => (0, import_react21.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_react21.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_react20.useState)(null);
++ const [place, setPlace] = (0, import_react21.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_react20.useRef)(null);
-+ const menu = (0, import_react20.useRef)(null);
++ const button = (0, import_react21.useRef)(null);
++ const menu = (0, import_react21.useRef)(null);
const close = () => setPlace(null);
- (0, import_react13.useEffect)(() => {
-+ (0, import_react20.useEffect)(() => {
++ (0, import_react21.useEffect)(() => {
if (place === null) return void 0;
const onDown = (event) => {
if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return;
@@ -9891,11 +10117,11 @@ diff --git a/client.js b/client.js
const models = roster.models ?? [];
- return (0, import_react13.createElement)(
- import_react13.Fragment,
-+ return (0, import_react20.createElement)(
-+ import_react20.Fragment,
++ return (0, import_react21.createElement)(
++ import_react21.Fragment,
null,
- (0, import_react13.createElement)(
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)(
"button",
{
ref: button,
@@ -9904,34 +10130,34 @@ diff --git a/client.js b/client.js
onClick: toggle
},
- (0, import_react13.createElement)(
-+ (0, import_react20.createElement)(
++ (0, import_react21.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_react20.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")),
-+ shown ? (0, import_react20.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null
++ (0, import_react21.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")),
++ shown ? (0, import_react21.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_react20.createElement)(Caret)
++ (0, import_react21.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom6.createPortal)((0, import_react13.createElement)(
-+ shownPlace ? (0, import_react_dom7.createPortal)((0, import_react20.createElement)(
++ shownPlace ? (0, import_react_dom8.createPortal)((0, import_react21.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_react20.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react20.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")),
-+ effortRows(reasoning).length > 0 ? (0, import_react20.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
-+ (0, import_react20.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ (0, import_react20.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
++ models.length > 0 ? (0, import_react21.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react21.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")),
++ effortRows(reasoning).length > 0 ? (0, import_react21.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
++ (0, import_react21.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
++ (0, import_react21.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_react20.createElement)(SlidersIcon), (0, import_react20.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…")))
++ } }, (0, import_react21.createElement)(SlidersIcon), (0, import_react21.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…")))
), document.body) : null
);
}
@@ -9940,15 +10166,15 @@ diff --git a/client.js b/client.js
const bot = roster.byId[sessionId];
- const [busy, setBusy] = (0, import_react13.useState)("");
- const [error, setError] = (0, import_react13.useState)("");
-+ const [busy, setBusy] = (0, import_react20.useState)("");
-+ const [error, setError] = (0, import_react20.useState)("");
++ const [busy, setBusy] = (0, import_react21.useState)("");
++ const [error, setError] = (0, import_react21.useState)("");
const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready;
- (0, import_react13.useEffect)(() => {
-+ (0, import_react20.useEffect)(() => {
++ (0, import_react21.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_react20.createElement)(RemoteGoneDock, { bot }) : null;
++ if (bot.remote) return bot.remote.gone ? (0, import_react21.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;
@@ -9960,14 +10186,14 @@ diff --git a/client.js b/client.js
- 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 text2 = 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_react20.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react20.createElement)(KeyHelpLink, { key: "key-help" })] : [
-+ ...groupModels(models).map(([provider, list]) => (0, import_react20.createElement)(
++ const choices = models.length === 0 ? [...setupChoices(actions).map(([label, run]) => (0, import_react21.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react21.createElement)(KeyHelpLink, { key: "key-help" })] : [
++ ...groupModels(models).map(([provider, list]) => (0, import_react21.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_react20.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
-+ (0, import_react20.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react20.createElement)("button", {
++ (0, import_react21.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
++ (0, import_react21.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react21.createElement)("button", {
key: model.ref,
type: "button",
className: "bt-model-chip",
@@ -9976,60 +10202,60 @@ diff --git a/client.js b/client.js
title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref,
onClick: () => pick2(model.ref)
- }, model.name, busy === model.ref ? (0, import_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)))
-+ }, model.name, busy === model.ref ? (0, import_react20.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)))
++ }, model.name, busy === model.ref ? (0, import_react21.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_react20.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react20.createElement)(SlidersIcon), t("Manage models"))
++ (0, import_react21.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react21.createElement)(SlidersIcon), t("Manage models"))
];
- return (0, import_react13.createElement)(
-+ return (0, import_react20.createElement)(
++ return (0, import_react21.createElement)(
"div",
{ className: "bt-model-dock" },
- (0, import_react13.createElement)(
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)(
"div",
{ className: "bt-astack", "data-lead": "" },
- (0, import_react13.createElement)(
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)(
"div",
{ className: "bt-msg" },
- (0, import_react13.createElement)(
-+ (0, import_react20.createElement)(
++ (0, import_react21.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_react20.createElement)(BotMark, { bot, size: 22 }),
-+ (0, import_react20.createElement)("div", { className: "bt-bubble" }, text2)
++ (0, import_react21.createElement)(BotMark, { bot, size: 22 }),
++ (0, import_react21.createElement)("div", { className: "bt-bubble" }, text2)
)
)
),
- (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_react20.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices),
-+ error ? (0, import_react20.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null
- );
- }
++ (0, import_react21.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices),
++ error ? (0, import_react21.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null
++ );
++}
+function RemoteGoneDock({ bot }) {
-+ return (0, import_react20.createElement)(
++ return (0, import_react21.createElement)(
+ "div",
+ { className: "bt-model-dock", "data-flybot": "remote-gone" },
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)(
+ "div",
+ { className: "bt-astack", "data-lead": "" },
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)(
+ "div",
+ { className: "bt-msg" },
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)(
+ "div",
+ { className: "bt-msg-line" },
-+ (0, import_react20.createElement)(BotMark, { bot, size: 22 }),
-+ (0, import_react20.createElement)("div", { className: "bt-bubble" }, t("{name} ({handle}) is no longer shared with you, so messages to it cannot be delivered. It runs on its owner's machine, so another model cannot stand in. Ask its owner for a new invite.", { name: bot.name, handle: bot.remote.handle }))
++ (0, import_react21.createElement)(BotMark, { bot, size: 22 }),
++ (0, import_react21.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,
@@ -10040,7 +10266,7 @@ diff --git a/client.js b/client.js
}
+var KEY_HELP_URL = "https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md";
+function KeyHelpLink() {
-+ return (0, import_react20.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_react21.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 text2 = String(message ?? "");
@@ -10064,22 +10290,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_react20.createElement)(
++ return (0, import_react21.createElement)(
+ "div",
+ { className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 },
-+ (0, import_react20.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
-+ (0, import_react20.createElement)(
++ (0, import_react21.createElement)("span", { className: "bt-turn-error-dot", "aria-hidden": true }),
++ (0, import_react21.createElement)(
+ "div",
+ { className: "bt-turn-error-copy" },
-+ (0, import_react20.createElement)("span", { className: "bt-turn-error-title" }, title),
-+ (0, import_react20.createElement)("span", { className: "bt-turn-error-message" }, message),
-+ code ? (0, import_react20.createElement)("code", { className: "bt-turn-error-code" }, code) : null
++ (0, import_react21.createElement)("span", { className: "bt-turn-error-title" }, title),
++ (0, import_react21.createElement)("span", { className: "bt-turn-error-message" }, message),
++ code ? (0, import_react21.createElement)("code", { className: "bt-turn-error-code" }, code) : null
+ ),
-+ choices.length > 0 ? (0, import_react20.createElement)(
++ choices.length > 0 ? (0, import_react21.createElement)(
+ "div",
+ { className: "bt-turn-error-actions" },
-+ choices.map(([label, run]) => (0, import_react20.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)),
-+ deepseek ? (0, import_react20.createElement)(KeyHelpLink, { key: "key-help" }) : null
++ choices.map(([label, run]) => (0, import_react21.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)),
++ deepseek ? (0, import_react21.createElement)(KeyHelpLink, { key: "key-help" }) : null
+ ) : null
+ );
+}
@@ -10088,18 +10314,18 @@ diff --git a/client.js b/client.js
const entry = open ? actions.signInEntry?.() : null;
if (!entry) return null;
- return (0, import_react13.createElement)(SignInHost, { key: open, entry, actions });
-+ return (0, import_react20.createElement)(SignInHost, { key: open, entry, actions });
++ return (0, import_react21.createElement)(SignInHost, { key: open, entry, actions });
}
function SignInHost({ entry, actions }) {
- const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {});
-+ const [ops] = (0, import_react20.useState)(() => entry.inject?.() ?? {});
++ const [ops] = (0, import_react21.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_react20.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot()));
-+ const [hooks] = (0, import_react20.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) }));
-+ return (0, import_react20.createElement)(entry.component, {
++ const useHook = (source) => (selector) => (0, import_react21.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot()));
++ const [hooks] = (0, import_react21.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) }));
++ return (0, import_react21.createElement)(entry.component, {
...ops,
...hooks,
t: actions.accountT(),
@@ -10125,7 +10351,7 @@ diff --git a/client.js b/client.js
`;
+// src/client/model-test.js
-+var import_react21 = require("react");
++var import_react22 = 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.",
@@ -10150,12 +10376,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_react21.useState)({ busy: false, outcome: void 0 });
-+ const live = (0, import_react21.useRef)(true);
-+ (0, import_react21.useEffect)(() => () => {
++ const [state, setState] = (0, import_react22.useState)({ busy: false, outcome: void 0 });
++ const live = (0, import_react22.useRef)(true);
++ (0, import_react22.useEffect)(() => () => {
+ live.current = false;
+ }, []);
-+ (0, import_react21.useEffect)(() => {
++ (0, import_react22.useEffect)(() => {
+ setState({ busy: false, outcome: void 0 });
+ }, [bot.id, bot.model]);
+ const start = () => {
@@ -10169,21 +10395,21 @@ diff --git a/client.js b/client.js
+ });
+ };
+ const { busy, outcome } = state;
-+ return (0, import_react21.createElement)(
++ return (0, import_react22.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row bt-model-test" },
-+ (0, import_react21.createElement)(
++ (0, import_react22.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react21.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")),
-+ (0, import_react21.createElement)(
++ (0, import_react22.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")),
++ (0, import_react22.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_react21.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
++ outcome?.code ? (0, import_react22.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null
+ )
+ ),
-+ (0, import_react21.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection"))
++ (0, import_react22.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 = `
@@ -10205,11 +10431,11 @@ diff --git a/client.js b/client.js
// src/client/folder-menu.js
-var import_react14 = require("react");
-var import_react_dom7 = require("react-dom");
-+var import_react22 = require("react");
-+var import_react_dom8 = require("react-dom");
++var import_react23 = require("react");
++var import_react_dom9 = 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_react22.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react22.createElement)(CheckIcon) : null);
++var Tick = ({ on }) => (0, import_react23.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react23.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);
@@ -10217,11 +10443,11 @@ diff --git a/client.js b/client.js
- const [draft, setDraft] = (0, import_react14.useState)("");
- const [listing, setListing] = (0, import_react14.useState)(null);
- const [problem, setProblem] = (0, import_react14.useState)("");
-+ const [info, setInfo] = (0, import_react22.useState)(null);
-+ const [view, setView] = (0, import_react22.useState)("list");
-+ const [draft, setDraft] = (0, import_react22.useState)("");
-+ const [listing, setListing] = (0, import_react22.useState)(null);
-+ const [problem, setProblem] = (0, import_react22.useState)("");
++ const [info, setInfo] = (0, import_react23.useState)(null);
++ const [view, setView] = (0, import_react23.useState)("list");
++ const [draft, setDraft] = (0, import_react23.useState)("");
++ const [listing, setListing] = (0, import_react23.useState)(null);
++ const [problem, setProblem] = (0, import_react23.useState)("");
const fallback = group ? t("Each Bot's own folder") : t("Team workspace");
const toggle = () => {
if (isOpen) {
@@ -10230,50 +10456,50 @@ diff --git a/client.js b/client.js
const known = info?.folders ?? [];
const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known;
- const listView = (0, import_react14.createElement)(
-+ const listView = (0, import_react22.createElement)(
++ const listView = (0, import_react23.createElement)(
"div",
{ key: "list", className: "bt-folder-view" },
- (0, import_react14.createElement)(
-+ (0, import_react22.createElement)(
++ (0, import_react23.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_react22.createElement)("span", { className: "bt-menu-label" }, fallback),
-+ (0, import_react22.createElement)(Tick, { on: !value })
++ (0, import_react23.createElement)("span", { className: "bt-menu-label" }, fallback),
++ (0, import_react23.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_react22.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null,
-+ folders.map((folder) => (0, import_react22.createElement)(
++ folders.length > 0 ? (0, import_react23.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null,
++ folders.map((folder) => (0, import_react23.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_react22.createElement)("span", { className: "bt-folder-copy" }, (0, import_react22.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react22.createElement)("span", { className: "bt-folder-path" }, folder.path)),
-+ (0, import_react22.createElement)(Tick, { on: folder.path === value })
++ (0, import_react23.createElement)("span", { className: "bt-folder-copy" }, (0, import_react23.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react23.createElement)("span", { className: "bt-folder-path" }, folder.path)),
++ (0, import_react23.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_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ info?.picker === "native" ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null,
-+ info?.picker === "browse" ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null,
-+ (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
++ (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
++ info?.picker === "native" ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null,
++ info?.picker === "browse" ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null,
++ (0, import_react23.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_react22.createElement)(PencilIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…")))
++ } }, (0, import_react23.createElement)(PencilIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…")))
);
- const typeView = (0, import_react14.createElement)(
-+ const typeView = (0, import_react22.createElement)(
++ const typeView = (0, import_react23.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_react22.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react22.createElement)(ChevronLeftIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
-+ (0, import_react22.createElement)("input", {
++ (0, import_react23.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react23.createElement)(ChevronLeftIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
++ (0, import_react23.createElement)("input", {
className: "bt-folder-input",
autoFocus: true,
value: draft,
@@ -10282,48 +10508,48 @@ diff --git a/client.js b/client.js
}
}),
- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder"))
-+ (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder"))
++ (0, import_react23.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_react22.createElement)(
++ const browseView = listing ? (0, import_react23.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_react22.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react22.createElement)(ChevronLeftIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
-+ (0, import_react22.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path),
-+ parent ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react22.createElement)(FolderIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null,
-+ inside.map((entry) => (0, import_react22.createElement)(
++ (0, import_react23.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react23.createElement)(ChevronLeftIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Back"))),
++ (0, import_react23.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path),
++ parent ? (0, import_react23.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react23.createElement)(FolderIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null,
++ inside.map((entry) => (0, import_react23.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_react22.createElement)(FolderIcon),
-+ (0, import_react22.createElement)("span", { className: "bt-menu-label" }, entry.name),
-+ (0, import_react22.createElement)(ChevronRightIcon)
++ (0, import_react23.createElement)(FolderIcon),
++ (0, import_react23.createElement)("span", { className: "bt-menu-label" }, entry.name),
++ (0, import_react23.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_react22.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null,
-+ listing.truncated ? (0, import_react22.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null,
-+ (0, import_react22.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
-+ (0, import_react22.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder"))
++ inside.length === 0 ? (0, import_react23.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null,
++ listing.truncated ? (0, import_react23.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null,
++ (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
++ (0, import_react23.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_react22.createElement)(
-+ import_react22.Fragment,
++ return (0, import_react23.createElement)(
++ import_react23.Fragment,
null,
- (0, import_react14.createElement)(
-+ (0, import_react22.createElement)(
++ (0, import_react23.createElement)(
"button",
{
ref: button,
@@ -10332,24 +10558,24 @@ diff --git a/client.js b/client.js
onClick: toggle
},
- (0, import_react14.createElement)(
-+ (0, import_react22.createElement)(
++ (0, import_react23.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_react22.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback),
-+ value ? (0, import_react22.createElement)("span", { className: "bt-model-provider" }, value) : null
++ (0, import_react23.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback),
++ value ? (0, import_react23.createElement)("span", { className: "bt-model-provider" }, value) : null
),
- (0, import_react14.createElement)(Caret)
-+ (0, import_react22.createElement)(Caret)
++ (0, import_react23.createElement)(Caret)
),
- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react14.createElement)(
-+ shownPlace ? (0, import_react_dom8.createPortal)((0, import_react22.createElement)(
++ shownPlace ? (0, import_react_dom9.createPortal)((0, import_react23.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_react22.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
++ problem ? (0, import_react23.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null
), document.body) : null
);
}
@@ -10359,7 +10585,7 @@ diff --git a/client.js b/client.js
// src/client/schedules.js
-var import_react15 = require("react");
-var POLL_MS = 3e4;
-+var import_react23 = require("react");
++var import_react24 = require("react");
+var POLL_MS4 = 3e4;
var shared = null;
var listeners = /* @__PURE__ */ new Set();
@@ -10370,8 +10596,8 @@ diff --git a/client.js b/client.js
function useSchedules(actions) {
- const [view, setView] = (0, import_react15.useState)(shared);
- (0, import_react15.useEffect)(() => {
-+ const [view, setView] = (0, import_react23.useState)(shared);
-+ (0, import_react23.useEffect)(() => {
++ const [view, setView] = (0, import_react24.useState)(shared);
++ (0, import_react24.useEffect)(() => {
listeners.add(setView);
let stopped = false;
let timer;
@@ -10430,9 +10656,9 @@ diff --git a/client.js b/client.js
+ }
+}
+function TaskRow({ task, actions, onView, onEdit, onCopy, showBot, roster }) {
-+ const [confirming, setConfirming] = (0, import_react23.useState)(false);
-+ const [busy, setBusy] = (0, import_react23.useState)(false);
-+ (0, import_react23.useEffect)(() => {
++ const [confirming, setConfirming] = (0, import_react24.useState)(false);
++ const [busy, setBusy] = (0, import_react24.useState)(false);
++ (0, import_react24.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
@@ -10448,7 +10674,7 @@ diff --git a/client.js b/client.js
+ onView(next);
+ }, () => setBusy(false));
+ };
-+ const [retryError, setRetryError] = (0, import_react23.useState)("");
++ const [retryError, setRetryError] = (0, import_react24.useState)("");
+ const retry = () => {
+ setBusy(true);
+ setRetryError("");
@@ -10465,33 +10691,33 @@ 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_react23.createElement)(
++ return (0, import_react24.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_react23.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
-+ (0, import_react23.createElement)(
++ showBot ? (0, import_react24.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
++ (0, import_react24.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_react23.createElement)("span", { className: "bt-task-title" }, task.title),
-+ (0, import_react23.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")),
-+ task.prompt && task.prompt !== task.title ? (0, import_react23.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null,
-+ last ? (0, import_react23.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null,
-+ retryError ? (0, import_react23.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null
++ (0, import_react24.createElement)("span", { className: "bt-task-title" }, task.title),
++ (0, import_react24.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_react24.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null,
++ last ? (0, import_react24.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null,
++ retryError ? (0, import_react24.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_react23.createElement)("span", { className: "bt-task-next" }, nextText(task)),
-+ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, title: t("Run the task once now"), onClick: retry }, t("Retry")) : null,
-+ onEdit ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react23.createElement)(PencilIcon)) : null,
-+ onCopy ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Copy to another Bot or group chat"), title: t("Copy to…"), onClick: () => onCopy(task) }, (0, import_react23.createElement)(DuplicateIcon)) : null,
-+ stoppable ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null,
-+ (0, import_react23.createElement)("button", {
++ (0, import_react24.createElement)("span", { className: "bt-task-next" }, nextText(task)),
++ failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react24.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_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react24.createElement)(PencilIcon)) : null,
++ onCopy ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Copy to another Bot or group chat"), title: t("Copy to…"), onClick: () => onCopy(task) }, (0, import_react24.createElement)(DuplicateIcon)) : null,
++ stoppable ? (0, import_react24.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null,
++ (0, import_react24.createElement)("button", {
type: "button",
className: "bt-mini bt-task-remove",
"data-confirm": confirming || void 0,
@@ -10500,7 +10726,7 @@ diff --git a/client.js b/client.js
title: confirming ? void 0 : t("Delete task"),
onClick: remove
- }, confirming ? t("Delete") : (0, import_react15.createElement)(TrashIcon))
-+ }, confirming ? t("Delete") : (0, import_react23.createElement)(TrashIcon))
++ }, confirming ? t("Delete") : (0, import_react24.createElement)(TrashIcon))
);
}
var pad = (number) => String(number).padStart(2, "0");
@@ -10544,17 +10770,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_react23.useState)(task ? task.botId ?? task.roomId : start ?? "");
-+ const [title, setTitle] = (0, import_react23.useState)(first.title);
-+ const [prompt, setPrompt] = (0, import_react23.useState)(first.prompt);
-+ const [kind, setKind] = (0, import_react23.useState)(first.kind);
-+ const [at, setAt] = (0, import_react23.useState)(first.at);
-+ const [time, setTime] = (0, import_react23.useState)(first.time);
-+ const [weekdays, setWeekdays] = (0, import_react23.useState)(first.weekdays);
-+ const [count, setCount] = (0, import_react23.useState)(first.count);
-+ const [unit, setUnit] = (0, import_react23.useState)(first.unit);
-+ const [busy, setBusy] = (0, import_react23.useState)(false);
-+ const [error, setError] = (0, import_react23.useState)("");
++ const [target, setTarget] = (0, import_react24.useState)(task ? task.botId ?? task.roomId : start ?? "");
++ const [title, setTitle] = (0, import_react24.useState)(first.title);
++ const [prompt, setPrompt] = (0, import_react24.useState)(first.prompt);
++ const [kind, setKind] = (0, import_react24.useState)(first.kind);
++ const [at, setAt] = (0, import_react24.useState)(first.at);
++ const [time, setTime] = (0, import_react24.useState)(first.time);
++ const [weekdays, setWeekdays] = (0, import_react24.useState)(first.weekdays);
++ const [count, setCount] = (0, import_react24.useState)(first.count);
++ const [unit, setUnit] = (0, import_react24.useState)(first.unit);
++ const [busy, setBusy] = (0, import_react24.useState)(false);
++ const [error, setError] = (0, import_react24.useState)("");
const timing = () => {
- if (kind === "at") return { kind, at: new Date(at).toISOString() };
- if (kind === "every") return { kind, minutes: Math.round(Number(count) * UNITS.find(([id]) => id === unit)[1]) };
@@ -10584,61 +10810,61 @@ 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_react23.createElement)(
++ return (0, import_react24.createElement)(
"form",
{ className: "bt-set-card bt-task-form", onSubmit: submit },
- (0, import_react15.createElement)(
-+ (0, import_react23.createElement)(
++ (0, import_react24.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_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Send to"))),
-+ (0, import_react23.createElement)(
++ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Send to"))),
++ (0, import_react24.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_react23.createElement)("option", { value: "" }, t("Pick a Bot or group chat")),
-+ (0, import_react23.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react23.createElement)("option", { key: bot.id, value: bot.id }, bot.name))),
-+ roster.rooms.length > 0 ? (0, import_react23.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react23.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null
++ (0, import_react24.createElement)("option", { value: "" }, t("Pick a Bot or group chat")),
++ (0, import_react24.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react24.createElement)("option", { key: bot.id, value: bot.id }, bot.name))),
++ roster.rooms.length > 0 ? (0, import_react24.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react24.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null
)
),
- (0, import_react15.createElement)(
-+ (0, import_react23.createElement)(
++ (0, import_react24.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_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
-+ (0, import_react23.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) })
++ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
++ (0, import_react24.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_react23.createElement)(
++ (0, import_react24.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_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("What to do"))),
-+ (0, import_react23.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) })
++ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("What to do"))),
++ (0, import_react24.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_react23.createElement)(
++ (0, import_react24.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_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("When"))),
-+ (0, import_react23.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react23.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))),
-+ (0, import_react23.createElement)(
++ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("When"))),
++ (0, import_react24.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react24.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))),
++ (0, import_react24.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_react23.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null,
-+ kind === "at" ? (0, import_react23.createElement)("input", { className: "bt-input", type: "datetime-local", value: at, "aria-label": t("Date and time"), onChange: (event) => setAt(event.target.value) }) : null,
-+ kind === "weekly" ? (0, import_react23.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react23.createElement)("button", {
++ kind === "cron" ? (0, import_react24.createElement)("span", { className: "bt-task-every" }, timingText(task)) : null,
++ kind === "at" ? (0, import_react24.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_react24.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react24.createElement)("button", {
key: day,
type: "button",
className: "bt-task-day",
@@ -10648,34 +10874,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_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: time, "aria-label": t("Time"), onChange: (event) => setTime(event.target.value) }) : null,
-+ kind === "every" ? (0, import_react23.createElement)(
-+ import_react23.Fragment,
++ kind === "daily" || kind === "weekly" ? (0, import_react24.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_react24.createElement)(
++ import_react24.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_react23.createElement)("span", { className: "bt-task-every" }, t("Every")),
-+ (0, import_react23.createElement)("input", { className: "bt-input bt-task-count", type: "number", min: 1, value: count, "aria-label": t("How many"), onChange: (event) => setCount(event.target.value) }),
-+ (0, import_react23.createElement)(
++ (0, import_react24.createElement)("span", { className: "bt-task-every" }, t("Every")),
++ (0, import_react24.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_react24.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_react23.createElement)("option", { key: id, value: id }, t(label)))
++ UNITS.map(([id, , label]) => (0, import_react24.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_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
-+ (0, import_react23.createElement)(
++ error ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
++ (0, import_react24.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_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
-+ (0, import_react23.createElement)(
++ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
++ (0, import_react24.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")
@@ -10690,25 +10916,25 @@ diff --git a/client.js b/client.js
+ actions.selectPanel("plugins");
+}
+function AutomationOff({ actions }) {
-+ return (0, import_react23.createElement)(
++ return (0, import_react24.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_react23.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react23.createElement)(ClockIcon)),
-+ (0, import_react23.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")),
-+ (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")),
-+ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
- );
- }
++ (0, import_react24.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react24.createElement)(ClockIcon)),
++ (0, import_react24.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")),
++ (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page.")),
++ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
++ );
++}
+function QuietHoursCard({ view, actions }) {
+ const quiet = view.quietHours ?? { on: false };
-+ const [busy, setBusy] = (0, import_react23.useState)(false);
-+ const [error, setError] = (0, import_react23.useState)("");
-+ const [from, setFrom] = (0, import_react23.useState)(quiet.from ?? "22:00");
-+ const [to, setTo] = (0, import_react23.useState)(quiet.to ?? "07:00");
++ const [busy, setBusy] = (0, import_react24.useState)(false);
++ const [error, setError] = (0, import_react24.useState)("");
++ const [from, setFrom] = (0, import_react24.useState)(quiet.from ?? "22:00");
++ const [to, setTo] = (0, import_react24.useState)(quiet.to ?? "07:00");
+ const unchecked = view.quietUnchecked === true;
+ const refresh = () => {
+ void Promise.resolve(actions.schedules?.()).then((next) => {
@@ -10726,23 +10952,23 @@ diff --git a/client.js b/client.js
+ refresh();
+ }, (failure) => setError(hostText(failure?.message ?? String(failure)))).finally(() => setBusy(false));
+ };
-+ return (0, import_react23.createElement)(
++ return (0, import_react24.createElement)(
+ "section",
+ { className: "bt-set-section" },
-+ (0, import_react23.createElement)("h3", null, t("Quiet hours")),
-+ (0, import_react23.createElement)(
++ (0, import_react24.createElement)("h3", null, t("Quiet hours")),
++ (0, import_react24.createElement)(
+ "div",
+ { className: "bt-set-card" },
-+ (0, import_react23.createElement)(
++ (0, import_react24.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row" },
-+ (0, import_react23.createElement)(
++ (0, import_react24.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react23.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")),
-+ (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists."))
++ (0, import_react24.createElement)("span", { className: "bt-bs-label" }, quiet.on ? t("On, {from}–{to}", { from: quiet.from, to: quiet.to }) : t("Off")),
++ (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("A new or edited task cannot be due inside the window. It never mutes a task that already exists."))
+ ),
-+ (0, import_react23.createElement)("button", {
++ (0, import_react24.createElement)("button", {
+ type: "button",
+ className: "bt-soft bt-soft-sm",
+ disabled: busy,
@@ -10750,80 +10976,80 @@ diff --git a/client.js b/client.js
+ onClick: () => save(quiet.on ? { on: false } : { on: true, from, to })
+ }, quiet.on ? t("Turn off") : t("Turn on"))
+ ),
-+ quiet.on ? (0, import_react23.createElement)(
++ quiet.on ? (0, import_react24.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row" },
-+ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Window"))),
-+ (0, import_react23.createElement)(
++ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Window"))),
++ (0, import_react24.createElement)(
+ "div",
+ { className: "bt-task-when" },
-+ (0, import_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }),
-+ (0, import_react23.createElement)("span", { className: "bt-task-every" }, "–"),
-+ (0, import_react23.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }),
-+ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window"))
++ (0, import_react24.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: from, "aria-label": t("Quiet hours start"), disabled: busy, onChange: (event) => setFrom(event.target.value) }),
++ (0, import_react24.createElement)("span", { className: "bt-task-every" }, "–"),
++ (0, import_react24.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: to, "aria-label": t("Quiet hours end"), disabled: busy, onChange: (event) => setTo(event.target.value) }),
++ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => save({ on: true, from, to }) }, t("Save window"))
+ )
+ ) : null,
-+ error ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
-+ quiet.on && unchecked ? (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : null
++ error ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null,
++ quiet.on && unchecked ? (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("A repeat or cron task can still run inside quiet hours, because DSH decides its next run. FlyBot says so here rather than claiming a silence it cannot keep."))) : null
+ )
-+ );
-+}
+ );
+ }
function SchedulesPage({ roster, actions, start }) {
const view = useSchedules(actions);
- const [adding, setAdding] = (0, import_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_react23.useState)(Boolean(start));
-+ const [editing, setEditing] = (0, import_react23.useState)(null);
-+ if (view === null) return (0, import_react23.createElement)("div", { className: "bt-set-hint" }, t("Loading…"));
-+ if (!view.available) return (0, import_react23.createElement)(AutomationOff, { actions });
++ const [adding, setAdding] = (0, import_react24.useState)(Boolean(start));
++ const [editing, setEditing] = (0, import_react24.useState)(null);
++ if (view === null) return (0, import_react24.createElement)("div", { className: "bt-set-hint" }, t("Loading…"));
++ if (!view.available) return (0, import_react24.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_react23.createElement)(BotAvatar, { bot, size: 18, badge: false, live: false }), bot.name, view.tasks.filter((task) => task.botId === bot.id)]),
-+ ...roster.rooms.map((room2) => [room2.id, (0, import_react23.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)])
++ ...roster.bots.map((bot) => [bot.id, (0, import_react24.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_react24.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)])
].filter(([, , , tasks]) => tasks.length > 0);
return [
- adding ? (0, import_react15.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react15.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setAdding(true) }, (0, import_react15.createElement)(PlusIcon), t("New task"))),
- byChat.length === 0 && !adding ? (0, import_react15.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react15.createElement)(
-+ (0, import_react23.createElement)(QuietHoursCard, { key: "quiet", view, actions }),
-+ adding ? (0, import_react23.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react23.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => {
++ (0, import_react24.createElement)(QuietHoursCard, { key: "quiet", view, actions }),
++ adding ? (0, import_react24.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react24.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => {
+ setEditing(null);
+ setAdding(true);
-+ } }, (0, import_react23.createElement)(PlusIcon), t("New task"))),
-+ byChat.length === 0 && !adding ? (0, import_react23.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react23.createElement)("div", { className: "bt-set-row" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react23.createElement)(
++ } }, (0, import_react24.createElement)(PlusIcon), t("New task"))),
++ byChat.length === 0 && !adding ? (0, import_react24.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react24.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_react23.createElement)("h3", { className: "bt-task-head" }, avatar, name),
-+ (0, import_react23.createElement)(TaskList, { tasks, editing, roster, actions, onEdit: edit, onDone: shown, onCancel: () => setEditing(null) })
++ (0, import_react24.createElement)("h3", { className: "bt-task-head" }, avatar, name),
++ (0, import_react24.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 [copying, setCopying] = (0, import_react23.useState)(null);
++ const [copying, setCopying] = (0, import_react24.useState)(null);
+ const source = copying ? tasks.find((task) => task.id === copying.id && task.sessionId === copying.sessionId) : void 0;
-+ const rows = tasks.filter((task) => task !== open && task !== source).map((task) => (0, import_react23.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit, onCopy: actions.scheduleCreate ? setCopying : void 0 }));
-+ return (0, import_react23.createElement)(
-+ import_react23.Fragment,
++ const rows = tasks.filter((task) => task !== open && task !== source).map((task) => (0, import_react24.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish, onEdit, onCopy: actions.scheduleCreate ? setCopying : void 0 }));
++ return (0, import_react24.createElement)(
++ import_react24.Fragment,
+ null,
-+ open ? (0, import_react23.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null,
-+ source ? (0, import_react23.createElement)(CopyRow, { key: `copy-${source.id}`, task: source, roster, actions, onView: (next) => {
++ open ? (0, import_react24.createElement)(TaskForm, { key: `edit-${open.id}`, roster, actions, task: open, onDone, onCancel }) : null,
++ source ? (0, import_react24.createElement)(CopyRow, { key: `copy-${source.id}`, task: source, roster, actions, onView: (next) => {
+ setCopying(null);
+ onDone(next);
+ }, onCancel: () => setCopying(null) }) : null,
-+ rows.length + extra.length > 0 ? (0, import_react23.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null
++ rows.length + extra.length > 0 ? (0, import_react24.createElement)("div", { className: "bt-set-card" }, ...rows, ...extra) : null
+ );
+}
+function CopyRow({ task, roster, actions, onView, onCancel }) {
@@ -10831,9 +11057,9 @@ diff --git a/client.js b/client.js
+ ...roster.bots.filter((bot) => bot.id !== task.botId).map((bot) => [bot.id, bot.name]),
+ ...roster.rooms.filter((room2) => room2.id !== task.roomId).map((room2) => [room2.id, room2.name])
+ ];
-+ const [target, setTarget] = (0, import_react23.useState)("");
-+ const [busy, setBusy] = (0, import_react23.useState)(false);
-+ const [error, setError] = (0, import_react23.useState)("");
++ const [target, setTarget] = (0, import_react24.useState)("");
++ const [busy, setBusy] = (0, import_react24.useState)(false);
++ const [error, setError] = (0, import_react24.useState)("");
+ const submit = (event) => {
+ event.preventDefault();
+ if (busy || target === "") return;
@@ -10844,38 +11070,38 @@ diff --git a/client.js b/client.js
+ setError(hostText(failure?.message ?? String(failure)));
+ });
+ };
-+ return (0, import_react23.createElement)(
++ return (0, import_react24.createElement)(
+ "form",
+ { className: "bt-set-row bt-task-copy", onSubmit: submit },
-+ (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Copy “{name}” to", { name: task.title }))),
-+ (0, import_react23.createElement)(
++ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Copy “{name}” to", { name: task.title }))),
++ (0, import_react24.createElement)(
+ "select",
+ { className: "bt-select bt-task-bot", value: target, "aria-label": t("Copy to"), onChange: (event) => setTarget(event.target.value) },
-+ (0, import_react23.createElement)("option", { value: "" }, t("Pick a Bot or group chat")),
-+ others.map(([id, name]) => (0, import_react23.createElement)("option", { key: id, value: id }, name))
++ (0, import_react24.createElement)("option", { value: "" }, t("Pick a Bot or group chat")),
++ others.map(([id, name]) => (0, import_react24.createElement)("option", { key: id, value: id }, name))
+ ),
-+ (0, import_react23.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm", disabled: busy || target === "" }, busy ? t("Copying…") : t("Copy")),
-+ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
-+ error ? (0, import_react23.createElement)("span", { className: "bt-danger bt-set-hint", role: "alert" }, error) : null
++ (0, import_react24.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm", disabled: busy || target === "" }, busy ? t("Copying…") : t("Copy")),
++ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
++ error ? (0, import_react24.createElement)("span", { className: "bt-danger bt-set-hint", role: "alert" }, error) : null
+ );
+}
function BotSchedules({ bot, roster, actions }) {
const view = useSchedules(actions);
-+ const [editing, setEditing] = (0, import_react23.useState)(null);
++ const [editing, setEditing] = (0, import_react24.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_react23.createElement)(
++ return (0, import_react24.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react15.createElement)("h3", null, t("Scheduled tasks")),
- (0, import_react15.createElement)(
-+ (0, import_react23.createElement)("h3", null, t("Scheduled tasks")),
-+ !view.available ? (0, import_react23.createElement)(
++ (0, import_react24.createElement)("h3", null, t("Scheduled tasks")),
++ !view.available ? (0, import_react24.createElement)(
"div",
{ className: "bt-set-card" },
- !view.available ? (0, import_react15.createElement)(
-+ (0, import_react23.createElement)(
++ (0, import_react24.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."))),
@@ -10885,10 +11111,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_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))),
-+ (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
++ (0, import_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page."))),
++ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openPlugins(actions) }, t("Open Plugins"))
+ )
-+ ) : (0, import_react23.createElement)(TaskList, {
++ ) : (0, import_react24.createElement)(TaskList, {
+ tasks,
+ editing,
+ roster,
@@ -10899,7 +11125,7 @@ diff --git a/client.js b/client.js
+ setEditing(null);
+ },
+ onCancel: () => setEditing(null),
-+ extra: [(0, import_react23.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react23.createElement)(PlusIcon), t("New task"))]
++ extra: [(0, import_react24.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react24.createElement)(PlusIcon), t("New task"))]
+ })
);
}
@@ -10907,21 +11133,21 @@ diff --git a/client.js b/client.js
const view = useSchedules(actions);
const tasks = view?.available ? view.tasks.filter((task) => task.botId === bot.id && task.status === "active") : [];
- return (0, import_react15.createElement)(
-+ return (0, import_react23.createElement)(
++ return (0, import_react24.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_react23.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")),
-+ tasks.length === 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-more", onClick: () => actions.openSettings("schedules", bot.id) }, view?.available === false ? t("Switch on Automation tasks on the Plugins page.") : t("No scheduled tasks yet. Add one")) : (0, import_react23.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react23.createElement)(
++ (0, import_react24.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")),
++ tasks.length === 0 ? (0, import_react24.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_react24.createElement)("div", { className: "bt-task-list" }, tasks.slice(0, 3).map((task) => (0, import_react24.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_react23.createElement)(ClockIcon),
-+ (0, import_react23.createElement)("span", { className: "bt-task-line-title" }, task.title),
-+ (0, import_react23.createElement)("span", { className: "bt-task-line-when" }, timingText(task))
++ (0, import_react24.createElement)(ClockIcon),
++ (0, import_react24.createElement)("span", { className: "bt-task-line-title" }, task.title),
++ (0, import_react24.createElement)("span", { className: "bt-task-line-when" }, timingText(task))
)))
);
}
@@ -10950,7 +11176,7 @@ diff --git a/client.js b/client.js
// src/client/capability-settings.js
-var import_react16 = require("react");
-+var import_react24 = require("react");
++var import_react25 = require("react");
var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] });
-var parseMap = (text, label) => {
+var parseMap = (text2, label) => {
@@ -10966,13 +11192,13 @@ diff --git a/client.js b/client.js
var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…");
function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) {
- return (0, import_react16.createElement)(
-+ return (0, import_react24.createElement)(
++ return (0, import_react25.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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t(label))),
-+ (0, import_react24.createElement)(multiline ? "textarea" : "input", {
++ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t(label))),
++ (0, import_react25.createElement)(multiline ? "textarea" : "input", {
className: multiline ? "bt-input bt-input-area" : "bt-input",
value,
disabled,
@@ -10981,7 +11207,7 @@ diff --git a/client.js b/client.js
}
function CapabilityForm({ draft, setDraft, items, actions, onClose }) {
- const [error, setError] = (0, import_react16.useState)("");
-+ const [error, setError] = (0, import_react24.useState)("");
++ const [error, setError] = (0, import_react25.useState)("");
const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value }));
const submit = async (event) => {
event.preventDefault();
@@ -10999,89 +11225,89 @@ diff --git a/client.js b/client.js
};
const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind));
- return (0, import_react16.createElement)(
-+ return (0, import_react24.createElement)(
++ return (0, import_react25.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_react24.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }),
-+ (0, import_react24.createElement)(
++ (0, import_react25.createElement)(Field, { label: "Name", value: draft.name, disabled: draft.originalName !== void 0, onChange: (value) => set("name", value), placeholder: "my-tool" }),
++ (0, import_react25.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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Type"))),
-+ (0, import_react24.createElement)(
++ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Type"))),
++ (0, import_react25.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_react24.createElement)("option", { key: kind, value: kind }, t(kind === "mcp" ? "MCP server" : kind === "skill" ? "Skill" : "Pack")))
++ ["mcp", "skill", "pack"].map((kind) => (0, import_react25.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_react24.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
-+ draft.kind === "mcp" ? (0, import_react24.createElement)(
++ (0, import_react25.createElement)(Field, { label: "Description", value: draft.description, onChange: (value) => set("description", value) }),
++ draft.kind === "mcp" ? (0, import_react25.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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, t("Connection"))),
-+ (0, import_react24.createElement)(
++ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, t("Connection"))),
++ (0, import_react25.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_react24.createElement)("option", { value: "stdio" }, t("Local command")),
-+ (0, import_react24.createElement)("option", { value: "streamable-http" }, t("HTTP server"))
++ (0, import_react25.createElement)("option", { value: "stdio" }, t("Local command")),
++ (0, import_react25.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_react24.createElement)(
++ draft.kind === "mcp" && draft.transport === "stdio" ? (0, import_react25.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_react24.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }),
-+ (0, import_react24.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }),
-+ (0, import_react24.createElement)(Field, { label: "Environment (JSON)", value: draft.envText, onChange: (value) => set("envText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') })
++ (0, import_react25.createElement)(Field, { label: "Command", value: draft.command, onChange: (value) => set("command", value) }),
++ (0, import_react25.createElement)(Field, { label: "Arguments (one per line)", value: draft.argsText, onChange: (value) => set("argsText", value), multiline: true }),
++ (0, import_react25.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_react24.createElement)(
++ draft.kind === "mcp" && draft.transport === "streamable-http" ? (0, import_react25.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_react24.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }),
-+ (0, import_react24.createElement)(Field, { label: "Headers (JSON)", value: draft.headersText, onChange: (value) => set("headersText", value), multiline: true, placeholder: t('{"API_KEY": "$DSH_SECRET_MY_KEY"}') })
++ (0, import_react25.createElement)(Field, { label: "URL", value: draft.url, onChange: (value) => set("url", value) }),
++ (0, import_react25.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_react24.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null,
-+ draft.kind === "pack" ? (0, import_react24.createElement)(
++ draft.kind === "skill" ? (0, import_react25.createElement)(Field, { label: "Skill instructions", value: draft.content, onChange: (value) => set("content", value), multiline: true }) : null,
++ draft.kind === "pack" ? (0, import_react25.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_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react24.createElement)(
++ available.length === 0 ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("Save an MCP server or skill first."))) : available.map((item) => (0, import_react25.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_react24.createElement)("span", { className: "bt-set-copy" }, (0, import_react24.createElement)("span", { className: "bt-bs-label" }, item.name)),
-+ (0, import_react24.createElement)("input", { type: "checkbox", checked: draft.items.includes(item.name), onChange: (event) => set("items", event.target.checked ? [...draft.items, item.name] : draft.items.filter((name) => name !== item.name)) })
++ (0, import_react25.createElement)("span", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", { className: "bt-bs-label" }, item.name)),
++ (0, import_react25.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_react24.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
-+ (0, import_react24.createElement)(
++ error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null,
++ (0, import_react25.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_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")),
-+ (0, import_react24.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability"))
++ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onClose }, t("Cancel")),
++ (0, import_react25.createElement)("button", { type: "submit", className: "bt-soft bt-soft-sm" }, draft.originalName ? t("Save changes") : t("Add capability"))
)
);
}
@@ -11089,32 +11315,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_react24.createElement)(
++ return (0, import_react25.createElement)(
"div",
{ className: "bt-connector" },
- (0, import_react16.createElement)(
-+ (0, import_react24.createElement)(
++ (0, import_react25.createElement)(
"div",
{ className: "bt-connector-head" },
- (0, import_react16.createElement)(
-+ (0, import_react24.createElement)(
++ (0, import_react25.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_react24.createElement)("span", { className: "bt-connector-name" }, item.name),
-+ (0, import_react24.createElement)("span", { className: "bt-connector-status" }, secondary)
++ (0, import_react25.createElement)("span", { className: "bt-connector-name" }, item.name),
++ (0, import_react25.createElement)("span", { className: "bt-connector-status" }, secondary)
),
- (0, import_react16.createElement)(
-+ (0, import_react24.createElement)(
++ (0, import_react25.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_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null,
-+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")),
-+ (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete"))
++ item.kind === "mcp" && item.transport === "stdio" && !item.approved ? (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onApprove }, t("Allow")) : null,
++ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onEdit }, t("Edit")),
++ (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDelete }, confirming ? t("Click again to delete.") : t("Delete"))
)
)
);
@@ -11124,9 +11350,9 @@ diff --git a/client.js b/client.js
- const [draft, setDraft] = (0, import_react16.useState)(null);
- const [confirming, setConfirming] = (0, import_react16.useState)("");
- (0, import_react16.useEffect)(() => {
-+ const [draft, setDraft] = (0, import_react24.useState)(null);
-+ const [confirming, setConfirming] = (0, import_react24.useState)("");
-+ (0, import_react24.useEffect)(() => {
++ const [draft, setDraft] = (0, import_react25.useState)(null);
++ const [confirming, setConfirming] = (0, import_react25.useState)("");
++ (0, import_react25.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(""), 4e3);
return () => clearTimeout(timer);
@@ -11135,26 +11361,26 @@ diff --git a/client.js b/client.js
void run(actions.deleteCapability(item.name));
};
- return (0, import_react16.createElement)(
-+ return (0, import_react24.createElement)(
++ return (0, import_react25.createElement)(
"section",
{ className: "bt-set-section" },
- (0, import_react16.createElement)("h3", null, t("Shared capabilities")),
- (0, import_react16.createElement)(
-+ (0, import_react24.createElement)("h3", null, t("Shared capabilities")),
-+ (0, import_react24.createElement)(
++ (0, import_react25.createElement)("h3", null, t("Shared capabilities")),
++ (0, import_react25.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_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: startNew }, t("Add capability"))
++ (0, import_react25.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_react24.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
-+ (0, import_react24.createElement)(
++ draft ? (0, import_react25.createElement)(CapabilityForm, { draft, setDraft, items, actions, onClose: () => setDraft(null) }) : null,
++ (0, import_react25.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_react24.createElement)("div", { className: "bt-set-row" }, (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react24.createElement)(CapabilityRow, {
++ items.length === 0 ? (0, import_react25.createElement)("div", { className: "bt-set-row" }, (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react25.createElement)(CapabilityRow, {
key: item.name,
item,
confirming: confirming === item.name,
@@ -11163,11 +11389,11 @@ diff --git a/client.js b/client.js
const statuses = roster.capabilityStatuses?.[bot.id] ?? {};
if (items.length === 0) return null;
- return (0, import_react16.createElement)(
-+ return (0, import_react24.createElement)(
++ return (0, import_react25.createElement)(
Card2,
{ title: t("Capabilities") },
- items.map((item) => (0, import_react16.createElement)(SwitchRow2, {
-+ items.map((item) => (0, import_react24.createElement)(SwitchRow2, {
++ items.map((item) => (0, import_react25.createElement)(SwitchRow2, {
key: item.name,
label: item.name,
hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0,
@@ -11176,7 +11402,7 @@ diff --git a/client.js b/client.js
}
+// src/client/connector-bots.js
-+var import_react25 = require("react");
++var import_react26 = 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));
@@ -11192,31 +11418,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_react25.createElement)(SwitchRow2, { label: name, hint: ownHint(bot, connector.id), on: isOn(bot, connector.id), disabled: false, onChange: (on) => void run(actions.setBotConnector(bot.id, connector.id, on)) });
++ return (0, import_react26.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_react25.createElement)(
++ return (0, import_react26.createElement)(
+ "div",
+ { className: "bt-connector-bots" },
-+ bots.length === 0 ? (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react25.createElement)(
++ bots.length === 0 ? (0, import_react26.createElement)("span", { className: "bt-set-hint" }, t("No Bots")) : bots.map((bot) => (0, import_react26.createElement)(
+ "div",
+ { key: bot.id, className: "bt-connector-bot" },
-+ (0, import_react25.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
++ (0, import_react26.createElement)(BotSwitch, { bot, connector, name: bot.name, actions, run, SwitchRow: SwitchRow2 })
+ )),
-+ (0, import_react25.createElement)("span", { className: "bt-set-hint" }, t("On by default for each of your Bots, on every turn: in its own chat, in groups, on schedules and when a friend uses it. A friend's own Bot never gets it."))
++ (0, import_react26.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_react25.createElement)(
++ return (0, import_react26.createElement)(
+ Card2,
+ { title: t("Connectors") },
-+ connected.map((connector) => (0, import_react25.createElement)(
++ connected.map((connector) => (0, import_react26.createElement)(
+ "div",
+ { key: connector.id, className: "bt-connector-bot" },
-+ (0, import_react25.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
++ (0, import_react26.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 })
+ ))
+ );
+}
@@ -11224,7 +11450,7 @@ diff --git a/client.js b/client.js
// src/client/memory-panel.js
-var import_react17 = require("react");
-var POLL_MS2 = 1500;
-+var import_react26 = require("react");
++var import_react27 = require("react");
+var POLL_MS5 = 1500;
var MENU_EXIT_MS2 = 120;
var counts = /* @__PURE__ */ new Map();
@@ -11235,8 +11461,8 @@ diff --git a/client.js b/client.js
function useMemoryCount(botId, actions) {
- const [count, setCount] = (0, import_react17.useState)(counts.get(botId) ?? null);
- (0, import_react17.useEffect)(() => {
-+ const [count, setCount] = (0, import_react26.useState)(counts.get(botId) ?? null);
-+ (0, import_react26.useEffect)(() => {
++ const [count, setCount] = (0, import_react27.useState)(counts.get(botId) ?? null);
++ (0, import_react27.useEffect)(() => {
const listener = () => setCount(counts.get(botId) ?? null);
countListeners.add(listener);
listener();
@@ -11251,21 +11477,21 @@ diff --git a/client.js b/client.js
const count = useMemoryCount(bot.id, actions);
const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count });
- return (0, import_react17.createElement)(
-+ return (0, import_react26.createElement)(
++ return (0, import_react27.createElement)(
"div",
null,
- (0, import_react17.createElement)("div", { className: "bt-section-title" }, t("Memory")),
- (0, import_react17.createElement)(
-+ (0, import_react26.createElement)("div", { className: "bt-section-title" }, t("Memory")),
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Memory")),
++ (0, import_react27.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_react26.createElement)(MemoryIcon),
-+ (0, import_react26.createElement)("span", { className: "bt-mem-row-copy" }, label),
-+ (0, import_react26.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react26.createElement)(ChevronRightIcon))
++ (0, import_react27.createElement)(MemoryIcon),
++ (0, import_react27.createElement)("span", { className: "bt-mem-row-copy" }, label),
++ (0, import_react27.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react27.createElement)(ChevronRightIcon))
)
);
}
@@ -11274,63 +11500,63 @@ diff --git a/client.js b/client.js
}
function Skeleton({ label }) {
- return (0, import_react17.createElement)(
-+ return (0, import_react26.createElement)(
++ return (0, import_react27.createElement)(
"div",
{ className: "bt-mem-skel", "aria-busy": true },
- [0, 1].map((block) => (0, import_react17.createElement)(
-+ [0, 1].map((block) => (0, import_react26.createElement)(
++ [0, 1].map((block) => (0, import_react27.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_react26.createElement)("span", { className: "bt-mem-skel-head" }),
-+ (0, import_react26.createElement)("span", null),
-+ (0, import_react26.createElement)("span", null),
-+ (0, import_react26.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } })
++ (0, import_react27.createElement)("span", { className: "bt-mem-skel-head" }),
++ (0, import_react27.createElement)("span", null),
++ (0, import_react27.createElement)("span", null),
++ (0, import_react27.createElement)("span", { style: { width: block === 0 ? "62%" : "48%" } })
)),
- label ? (0, import_react17.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
-+ label ? (0, import_react26.createElement)("div", { className: "bt-mem-skel-label" }, label) : null
++ label ? (0, import_react27.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_react26.createElement)(
++ return (0, import_react27.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_react26.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react26.createElement)(MemoryIcon)),
-+ (0, import_react26.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })),
-+ (0, import_react26.createElement)("div", { className: "bt-mem-empty-text" }, t("Bots keep lasting preferences, decisions, and what you ask them to remember. Tell {name} below, or in its chat.", { name: bot.name }))
++ (0, import_react27.createElement)("div", { className: "bt-mem-empty-mark" }, (0, import_react27.createElement)(MemoryIcon)),
++ (0, import_react27.createElement)("div", { className: "bt-mem-empty-title" }, t("{name} has not saved anything yet", { name: bot.name })),
++ (0, import_react27.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_react26.createElement)(
++ const failed = view.error && !view.summarizing ? (0, import_react27.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_react26.createElement)("span", null, view.summary ? t("The summary is out of date: {reason}", { reason: hostText(view.error) }) : t("The summary could not be written: {reason}", { reason: hostText(view.error) })),
-+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: regenerate }, t("Try again"))
++ (0, import_react27.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_react27.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_react26.createElement)(import_react26.Fragment, null, failed, failed ? null : (0, import_react26.createElement)(Skeleton, { label: t("Writing the summary…") }));
-+ return (0, import_react26.createElement)(
-+ import_react26.Fragment,
++ if (!view.summary) return (0, import_react27.createElement)(import_react27.Fragment, null, failed, failed ? null : (0, import_react27.createElement)(Skeleton, { label: t("Writing the summary…") }));
++ return (0, import_react27.createElement)(
++ import_react27.Fragment,
null,
failed,
- (0, import_react17.createElement)(
-+ (0, import_react26.createElement)(
++ (0, import_react27.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_react26.createElement)("h3", { key: index }, block.text) : (0, import_react26.createElement)("p", { key: index }, block.text))
++ summaryBlocks(view.summary.text).map((block, index) => block.kind === "h" ? (0, import_react27.createElement)("h3", { key: index }, block.text) : (0, import_react27.createElement)("p", { key: index }, block.text))
)
);
}
@@ -11341,9 +11567,9 @@ diff --git a/client.js b/client.js
- const [confirming, setConfirming] = (0, import_react17.useState)(false);
- const [busy, setBusy] = (0, import_react17.useState)(false);
- (0, import_react17.useEffect)(() => {
-+ const [confirming, setConfirming] = (0, import_react26.useState)(false);
-+ const [busy, setBusy] = (0, import_react26.useState)(false);
-+ (0, import_react26.useEffect)(() => {
++ const [confirming, setConfirming] = (0, import_react27.useState)(false);
++ const [busy, setBusy] = (0, import_react27.useState)(false);
++ (0, import_react27.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 3e3);
return () => clearTimeout(timer);
@@ -11352,20 +11578,20 @@ diff --git a/client.js b/client.js
if (!await remove(entry)) setBusy(false);
};
- return (0, import_react17.createElement)(
-+ return (0, import_react26.createElement)(
++ return (0, import_react27.createElement)(
"li",
{ className: "bt-mem-entry", "data-busy": busy || void 0 },
- (0, import_react17.createElement)(
-+ (0, import_react26.createElement)(
++ (0, import_react27.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_react26.createElement)("span", { className: "bt-mem-entry-text" }, entry.text),
-+ meta ? (0, import_react26.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null
++ (0, import_react27.createElement)("span", { className: "bt-mem-entry-text" }, entry.text),
++ meta ? (0, import_react27.createElement)("span", { className: "bt-mem-entry-meta" }, meta) : null
),
- (0, import_react17.createElement)("button", {
-+ (0, import_react26.createElement)("button", {
++ (0, import_react27.createElement)("button", {
type: "button",
className: "bt-mem-remove",
"data-confirm": confirming || void 0,
@@ -11374,7 +11600,7 @@ diff --git a/client.js b/client.js
title: confirming ? void 0 : t("Remove from memory"),
onClick: click
- }, confirming ? t("Remove") : (0, import_react17.createElement)(TrashIcon))
-+ }, confirming ? t("Remove") : (0, import_react26.createElement)(TrashIcon))
++ }, confirming ? t("Remove") : (0, import_react27.createElement)(TrashIcon))
);
}
function Entries({ bot, view, remove }) {
@@ -11383,35 +11609,35 @@ diff --git a/client.js b/client.js
{ scope: "team", title: t("Team memory"), hint: t("Every Bot reads it"), size: view.sizes.team }
];
- return (0, import_react17.createElement)(
-+ return (0, import_react26.createElement)(
++ return (0, import_react27.createElement)(
"div",
{ className: "bt-mem-entries" },
- sections.map((section) => (0, import_react17.createElement)(
-+ sections.map((section) => (0, import_react26.createElement)(
++ sections.map((section) => (0, import_react27.createElement)(
"section",
{ key: section.scope, className: "bt-mem-section" },
- (0, import_react17.createElement)(
-+ (0, import_react26.createElement)(
++ (0, import_react27.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_react26.createElement)("h3", null, section.title),
-+ section.hint ? (0, import_react26.createElement)("span", null, section.hint) : null,
-+ (0, import_react26.createElement)("span", { className: "bt-mem-size", title: t("MEMORY.md, which every conversation reads") }, t("{used} of {limit} characters", { used: section.size.toLocaleString(dateLocale()), limit: view.limits.main.toLocaleString(dateLocale()) }))
++ (0, import_react27.createElement)("h3", null, section.title),
++ section.hint ? (0, import_react27.createElement)("span", null, section.hint) : null,
++ (0, import_react27.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_react26.createElement)(
-+ import_react26.Fragment,
++ groups.some((group) => group.scope === section.scope) ? groups.filter((group) => group.scope === section.scope).map((group) => (0, import_react27.createElement)(
++ import_react27.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_react26.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null,
-+ (0, import_react26.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react26.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove })))
-+ )) : (0, import_react26.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet"))
++ group.topic ? (0, import_react27.createElement)("div", { className: "bt-mem-topic" }, group.topic) : null,
++ (0, import_react27.createElement)("ul", { className: "bt-mem-list" }, group.entries.map((entry) => (0, import_react27.createElement)(EntryRow, { key: `${entry.topic ?? ""}:${entry.text}`, entry, remove })))
++ )) : (0, import_react27.createElement)("div", { className: "bt-mem-none" }, t("Nothing saved yet"))
))
);
}
@@ -11429,33 +11655,33 @@ diff --git a/client.js b/client.js
}
function Reply({ answer, dismiss }) {
- return (0, import_react17.createElement)(
-+ return (0, import_react26.createElement)(
++ return (0, import_react27.createElement)(
"div",
{ className: "bt-mem-reply", role: "status" },
- (0, import_react17.createElement)(
-+ (0, import_react26.createElement)(
++ (0, import_react27.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_react26.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked),
-+ answer.pending ? null : (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react26.createElement)(CloseIcon))
++ (0, import_react27.createElement)("span", { className: "bt-mem-reply-asked" }, answer.asked),
++ answer.pending ? null : (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Dismiss"), onClick: dismiss }, (0, import_react27.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_react26.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react26.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react26.createElement)(
-+ import_react26.Fragment,
++ answer.pending ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text bt-shimmer" }, t("Thinking…")) : answer.error ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text bt-danger" }, hostText(answer.error)) : (0, import_react27.createElement)(
++ import_react27.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_react26.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null,
-+ answer.changes.length > 0 ? (0, import_react26.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react26.createElement)(
++ answer.reply ? (0, import_react27.createElement)("div", { className: "bt-mem-reply-text" }, answer.reply) : null,
++ answer.changes.length > 0 ? (0, import_react27.createElement)("ul", { className: "bt-mem-changes" }, answer.changes.map((change, index) => (0, import_react27.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_react26.createElement)(CheckIcon) : (0, import_react26.createElement)(CloseIcon),
-+ (0, import_react26.createElement)("span", null, changeLine(change))
++ change.ok ? (0, import_react27.createElement)(CheckIcon) : (0, import_react27.createElement)(CloseIcon),
++ (0, import_react27.createElement)("span", null, changeLine(change))
))) : null
)
);
@@ -11467,18 +11693,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_react26.useState)(null);
-+ const [error, setError] = (0, import_react26.useState)("");
-+ const timer = (0, import_react26.useRef)(0);
-+ const latest = (0, import_react26.useRef)(0);
-+ const alive = (0, import_react26.useRef)(true);
-+ const show = (0, import_react26.useCallback)((next) => {
++ const [view, setView] = (0, import_react27.useState)(null);
++ const [error, setError] = (0, import_react27.useState)("");
++ const timer = (0, import_react27.useRef)(0);
++ const latest = (0, import_react27.useRef)(0);
++ const alive = (0, import_react27.useRef)(true);
++ const show = (0, import_react27.useCallback)((next) => {
setView(next);
setError("");
noteCount(botId, next.entries.length);
}, [botId]);
- const load = (0, import_react17.useCallback)(async (flags = {}) => {
-+ const load = (0, import_react26.useCallback)(async (flags = {}) => {
++ const load = (0, import_react27.useCallback)(async (flags = {}) => {
clearTimeout(timer.current);
const ticket = ++latest.current;
try {
@@ -11492,7 +11718,7 @@ diff --git a/client.js b/client.js
}
}, [botId]);
- (0, import_react17.useEffect)(() => {
-+ (0, import_react26.useEffect)(() => {
++ (0, import_react27.useEffect)(() => {
alive.current = true;
void load({ summarize: true });
return () => {
@@ -11504,8 +11730,8 @@ diff --git a/client.js b/client.js
- const [now, setNow] = (0, import_react17.useState)(Date.now());
- (0, import_react17.useEffect)(() => {
+function useNow3(every) {
-+ const [now, setNow] = (0, import_react26.useState)(Date.now());
-+ (0, import_react26.useEffect)(() => {
++ const [now, setNow] = (0, import_react27.useState)(Date.now());
++ (0, import_react27.useEffect)(() => {
const id = setInterval(() => setNow(Date.now()), every);
return () => clearInterval(id);
}, [every]);
@@ -11515,8 +11741,8 @@ diff --git a/client.js b/client.js
const { view, error, load, show } = useMemoryView(bot.id, actions);
- const [page, setPage] = (0, import_react17.useState)("summary");
- const [menu, setMenu] = (0, import_react17.useState)(false);
-+ const [page, setPage] = (0, import_react26.useState)("summary");
-+ const [menu, setMenu] = (0, import_react26.useState)(false);
++ const [page, setPage] = (0, import_react27.useState)("summary");
++ const [menu, setMenu] = (0, import_react27.useState)(false);
const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2);
- const [text, setText] = (0, import_react17.useState)("");
- const [answer, setAnswer] = (0, import_react17.useState)(null);
@@ -11525,16 +11751,16 @@ diff --git a/client.js b/client.js
- const inputRef = (0, import_react17.useRef)(null);
- const now = useNow(3e4);
- const close = (0, import_react17.useCallback)(() => actions.closeMemory(), []);
-+ const [text2, setText] = (0, import_react26.useState)("");
-+ const [answer, setAnswer] = (0, import_react26.useState)(null);
-+ const menuRef = (0, import_react26.useRef)(null);
-+ const dotsRef = (0, import_react26.useRef)(null);
-+ const inputRef = (0, import_react26.useRef)(null);
++ const [text2, setText] = (0, import_react27.useState)("");
++ const [answer, setAnswer] = (0, import_react27.useState)(null);
++ const menuRef = (0, import_react27.useRef)(null);
++ const dotsRef = (0, import_react27.useRef)(null);
++ const inputRef = (0, import_react27.useRef)(null);
+ const now = useNow3(3e4);
-+ const close = (0, import_react26.useCallback)(() => actions.closeMemory(), []);
++ const close = (0, import_react27.useCallback)(() => actions.closeMemory(), []);
useEscape(close);
- (0, import_react17.useEffect)(() => {
-+ (0, import_react26.useEffect)(() => {
++ (0, import_react27.useEffect)(() => {
if (!menu) return void 0;
const onDown = (event) => {
if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return;
@@ -11552,17 +11778,17 @@ diff --git a/client.js b/client.js
const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : "";
const busy = answer?.pending === true;
- return (0, import_react17.createElement)(
-+ return (0, import_react26.createElement)(
++ return (0, import_react27.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_react26.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }),
++ (0, import_react27.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_react26.createElement)(
++ (0, import_react27.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,
@@ -11572,54 +11798,54 @@ diff --git a/client.js b/client.js
- (0, import_react17.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react17.createElement)(DotsIcon)),
- (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react17.createElement)(CloseIcon)),
- menuShown ? (0, import_react17.createElement)(
-+ page === "entries" ? (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react26.createElement)(ChevronLeftIcon)) : null,
-+ (0, import_react26.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
-+ (0, import_react26.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
-+ (0, import_react26.createElement)("span", { className: "bt-mem-head-gap" }),
-+ (0, import_react26.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react26.createElement)(DotsIcon)),
-+ (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react26.createElement)(CloseIcon)),
-+ menuShown ? (0, import_react26.createElement)(
++ page === "entries" ? (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react27.createElement)(ChevronLeftIcon)) : null,
++ (0, import_react27.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")),
++ (0, import_react27.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status),
++ (0, import_react27.createElement)("span", { className: "bt-mem-head-gap" }),
++ (0, import_react27.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_react27.createElement)(DotsIcon)),
++ (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react27.createElement)(CloseIcon)),
++ menuShown ? (0, import_react27.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_react26.createElement)(
++ (0, import_react27.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_react26.createElement)("span", { className: "bt-menu-label" }, t("All entries")),
-+ (0, import_react26.createElement)("span", { className: "bt-menu-hint" }, String(count))
++ (0, import_react27.createElement)("span", { className: "bt-menu-label" }, t("All entries")),
++ (0, import_react27.createElement)("span", { className: "bt-menu-hint" }, String(count))
),
- (0, import_react17.createElement)(
-+ (0, import_react26.createElement)(
++ (0, import_react27.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_react26.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary"))
++ (0, import_react27.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary"))
)
) : null
),
- (0, import_react17.createElement)(
-+ (0, import_react26.createElement)(
++ (0, import_react27.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_react26.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null,
-+ view === null ? error ? null : (0, import_react26.createElement)(Skeleton) : page === "entries" ? (0, import_react26.createElement)(Entries, { bot, view, remove }) : (0, import_react26.createElement)(Summary, { bot, view, regenerate })
++ error ? (0, import_react27.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null,
++ view === null ? error ? null : (0, import_react27.createElement)(Skeleton) : page === "entries" ? (0, import_react27.createElement)(Entries, { bot, view, remove }) : (0, import_react27.createElement)(Summary, { bot, view, regenerate })
),
- (0, import_react17.createElement)(
-+ (0, import_react26.createElement)(
++ (0, import_react27.createElement)(
"div",
{ className: "bt-mem-foot" },
- answer ? (0, import_react17.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
- (0, import_react17.createElement)(
-+ answer ? (0, import_react26.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
-+ (0, import_react26.createElement)(
++ answer ? (0, import_react27.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null,
++ (0, import_react27.createElement)(
"form",
{ className: "bt-mem-ask", onSubmit: ask },
- (0, import_react17.createElement)("input", {
-+ (0, import_react26.createElement)("input", {
++ (0, import_react27.createElement)("input", {
ref: inputRef,
className: "bt-mem-input",
- value: text,
@@ -11631,12 +11857,12 @@ diff --git a/client.js b/client.js
onChange: (event) => setText(event.target.value)
}),
- (0, import_react17.createElement)(
-+ (0, import_react26.createElement)(
++ (0, import_react27.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)
+ { type: "submit", className: "bt-mem-send", disabled: busy || text2.trim() === "", "aria-label": t("Send") },
-+ busy ? (0, import_react26.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react26.createElement)(ArrowUpIcon)
++ busy ? (0, import_react27.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react27.createElement)(ArrowUpIcon)
)
)
)
@@ -11645,7 +11871,7 @@ diff --git a/client.js b/client.js
}
+// src/client/standing-approvals.js
-+var import_react27 = require("react");
++var import_react28 = require("react");
+var standingOf = (target, roster) => {
+ const me = roster?.self?.memberId;
+ return (target?.standingApprovals ?? []).filter((grant) => me === void 0 || grant.ownerId === me);
@@ -11654,19 +11880,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_react27.createElement)(
++ return (0, import_react28.createElement)(
+ Card2,
+ { title: t("Always allowed") },
-+ grants.map((grant) => (0, import_react27.createElement)(
++ grants.map((grant) => (0, import_react28.createElement)(
+ "div",
+ { key: grant.kind, className: "bt-set-row bt-bs-row", "data-standing": grant.kind },
-+ (0, import_react27.createElement)(
++ (0, import_react28.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react27.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind),
-+ (0, import_react27.createElement)("span", { className: "bt-set-hint" }, hint)
++ (0, import_react28.createElement)("span", { className: "bt-bs-label" }, grant.label || grant.kind),
++ (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint)
+ ),
-+ (0, import_react27.createElement)("button", {
++ (0, import_react28.createElement)("button", {
+ type: "button",
+ className: "bt-soft bt-soft-sm",
+ "aria-label": t("Remove always allow for {kind}", { kind: grant.label || grant.kind }),
@@ -11678,7 +11904,7 @@ diff --git a/client.js b/client.js
+
// src/client/bot-settings.js
-var import_react18 = require("react");
-+var import_react28 = require("react");
++var import_react29 = 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"));
@@ -11688,8 +11914,8 @@ diff --git a/client.js b/client.js
function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) {
- const [draft, setDraft] = (0, import_react18.useState)(null);
- const reverting = (0, import_react18.useRef)(false);
-+ const [draft, setDraft] = (0, import_react28.useState)(null);
-+ const reverting = (0, import_react28.useRef)(false);
++ const [draft, setDraft] = (0, import_react29.useState)(null);
++ const reverting = (0, import_react29.useRef)(false);
const commit = () => {
const next = draft?.trim();
setDraft(null);
@@ -11698,7 +11924,7 @@ diff --git a/client.js b/client.js
void onSave(next);
};
- return (0, import_react18.createElement)(multiline ? "textarea" : "input", {
-+ return (0, import_react28.createElement)(multiline ? "textarea" : "input", {
++ return (0, import_react29.createElement)(multiline ? "textarea" : "input", {
className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input",
value: draft ?? value ?? "",
placeholder,
@@ -11707,37 +11933,37 @@ diff --git a/client.js b/client.js
}
function Row({ label, hint, stack = false, children }) {
- return (0, import_react18.createElement)(
-+ return (0, import_react28.createElement)(
++ return (0, import_react29.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_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) : null),
++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react29.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_react28.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react28.createElement)("h3", null, title) : null, (0, import_react28.createElement)("div", { className: "bt-set-card" }, children));
++ return (0, import_react29.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react29.createElement)("h3", null, title) : null, (0, import_react29.createElement)("div", { className: "bt-set-card" }, children));
}
function Switch({ on, label, disabled, onChange }) {
- return (0, import_react18.createElement)(
-+ return (0, import_react28.createElement)(
++ return (0, import_react29.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_react28.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true })
++ (0, import_react29.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true })
);
}
-function SwitchRow({ label, hint, on, disabled, onChange }) {
- return (0, import_react18.createElement)(
+function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) {
-+ return (0, import_react28.createElement)(
++ return (0, import_react29.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-set-hint" }, hint) : null),
- (0, import_react18.createElement)(Switch, { on, label, disabled, onChange })
-+ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react28.createElement)("span", { className: "bt-set-hint" }, hint) : null),
-+ (0, import_react28.createElement)(Switch, { on, label, disabled, onChange })
++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react29.createElement)("span", { className: "bt-set-hint" }, hint) : null),
++ (0, import_react29.createElement)(Switch, { on, label, disabled, onChange })
);
}
function modelHint(bot, roster) {
@@ -11746,13 +11972,13 @@ diff --git a/client.js b/client.js
}
function ShapeRing({ shape, size }) {
- const maskId = (0, import_react18.useRef)("");
-+ const maskId = (0, import_react28.useRef)("");
++ const maskId = (0, import_react29.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_react28.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } });
++ return (0, import_react29.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))];
@@ -11761,11 +11987,11 @@ diff --git a/client.js b/client.js
for (let index = 0; index < shapes.length; index += PICKER_COLUMNS) rows.push(shapes.slice(index, index + PICKER_COLUMNS));
- return (0, import_react18.createElement)(
- import_react18.Fragment,
-+ return (0, import_react28.createElement)(
-+ import_react28.Fragment,
++ return (0, import_react29.createElement)(
++ import_react29.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_react28.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react28.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react28.createElement)("button", {
++ (0, import_react29.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react29.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react29.createElement)("button", {
key: id,
type: "button",
className: "bt-shape",
@@ -11775,8 +12001,8 @@ diff --git a/client.js b/client.js
onClick: () => onShape(id)
- }, (0, import_react18.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react18.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))),
- (0, import_react18.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react18.createElement)("button", {
-+ }, (0, import_react28.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react28.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))),
-+ (0, import_react28.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react28.createElement)("button", {
++ }, (0, import_react29.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react29.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))),
++ (0, import_react29.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react29.createElement)("button", {
key: color,
type: "button",
className: "bt-color",
@@ -11785,7 +12011,7 @@ diff --git a/client.js b/client.js
"aria-pressed": look.color === color,
onClick: () => onColor(color)
- }, (0, import_react18.createElement)("span", { style: { background: paintCss(color) } }))))
-+ }, (0, import_react28.createElement)("span", { style: { background: paintCss(color) } }))))
++ }, (0, import_react29.createElement)("span", { style: { background: paintCss(color) } }))))
);
}
function AvatarEditor({ bot, actions, run, onClose }) {
@@ -11794,15 +12020,15 @@ diff --git a/client.js b/client.js
- const [over, setOver] = (0, import_react18.useState)(false);
- const ref = (0, import_react18.useRef)(null);
- const fileRef = (0, import_react18.useRef)(null);
-+ const [tab, setTab] = (0, import_react28.useState)(look.image ? "upload" : "bot");
-+ const [over, setOver] = (0, import_react28.useState)(false);
-+ const ref = (0, import_react28.useRef)(null);
-+ const fileRef = (0, import_react28.useRef)(null);
++ const [tab, setTab] = (0, import_react29.useState)(look.image ? "upload" : "bot");
++ const [over, setOver] = (0, import_react29.useState)(false);
++ const ref = (0, import_react29.useRef)(null);
++ const fileRef = (0, import_react29.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_react28.useEffect)(() => {
++ (0, import_react29.useEffect)(() => {
const onDown = (event) => {
if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return;
onClose();
@@ -11811,7 +12037,7 @@ diff --git a/client.js b/client.js
};
}, [onClose]);
- (0, import_react18.useEffect)(() => {
-+ (0, import_react28.useEffect)(() => {
++ (0, import_react29.useEffect)(() => {
if (tab !== "upload") return void 0;
const onPaste = (event) => {
const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/"));
@@ -11820,24 +12046,24 @@ diff --git a/client.js b/client.js
const hasPhoto = Boolean(look.image);
const glide = useGlide(tab);
- return (0, import_react18.createElement)(
-+ return (0, import_react28.createElement)(
++ return (0, import_react29.createElement)(
"div",
{ ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") },
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
"div",
{ className: "bt-avedit-top" },
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react28.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label)))
++ (0, import_react29.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react29.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_react28.createElement)("button", {
++ (0, import_react29.createElement)("button", {
type: "button",
className: "bt-avedit-tab bt-avedit-reset",
title: t("Reset character to default"),
@@ -11845,14 +12071,14 @@ diff --git a/client.js b/client.js
}, t("Reset"))
),
- tab === "bot" ? (0, import_react18.createElement)(CharacterGrid, {
-+ tab === "bot" ? (0, import_react28.createElement)(CharacterGrid, {
++ tab === "bot" ? (0, import_react29.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_react28.createElement)(
++ tab === "upload" ? (0, import_react29.createElement)(
"div",
{
className: "bt-dropzone",
@@ -11863,18 +12089,18 @@ diff --git a/client.js b/client.js
- hasPhoto ? (0, import_react18.createElement)("img", { src: look.image, alt: "" }) : null,
- (0, import_react18.createElement)("div", null, t("Drag, drop, or paste an image")),
- (0, import_react18.createElement)(
-+ hasPhoto ? (0, import_react28.createElement)("img", { src: look.image, alt: "" }) : null,
-+ (0, import_react28.createElement)("div", null, t("Drag, drop, or paste an image")),
-+ (0, import_react28.createElement)(
++ hasPhoto ? (0, import_react29.createElement)("img", { src: look.image, alt: "" }) : null,
++ (0, import_react29.createElement)("div", null, t("Drag, drop, or paste an image")),
++ (0, import_react29.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_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")),
-+ hasPhoto ? (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null
++ (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")),
++ hasPhoto ? (0, import_react29.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_react28.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
++ (0, import_react29.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
const file = event.target.files?.[0];
event.target.value = "";
upload(file);
@@ -11883,17 +12109,17 @@ diff --git a/client.js b/client.js
const count = useMemoryCount(bot.id, actions);
const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count });
- return (0, import_react18.createElement)(
-+ return (0, import_react28.createElement)(
++ return (0, import_react29.createElement)(
Card,
{ title: t("Memory") },
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, label)),
-+ (0, import_react28.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react28.createElement)(ChevronRightIcon))
++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label)),
++ (0, import_react29.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react29.createElement)(ChevronRightIcon))
)
);
}
@@ -11902,10 +12128,10 @@ diff --git a/client.js b/client.js
- const [confirming, setConfirming] = (0, import_react18.useState)(false);
- const closeEditor = (0, import_react18.useCallback)(() => setEditing(false), []);
- (0, import_react18.useEffect)(() => {
-+ const [editing, setEditing] = (0, import_react28.useState)(false);
-+ const [confirming, setConfirming] = (0, import_react28.useState)(false);
-+ const closeEditor = (0, import_react28.useCallback)(() => setEditing(false), []);
-+ (0, import_react28.useEffect)(() => {
++ const [editing, setEditing] = (0, import_react29.useState)(false);
++ const [confirming, setConfirming] = (0, import_react29.useState)(false);
++ const closeEditor = (0, import_react29.useCallback)(() => setEditing(false), []);
++ (0, import_react29.useEffect)(() => {
if (!confirming) return void 0;
const timer = setTimeout(() => setConfirming(false), 4e3);
return () => clearTimeout(timer);
@@ -11919,92 +12145,92 @@ diff --git a/client.js b/client.js
else setConfirming(true);
};
- (0, import_react18.useEffect)(() => {
-+ (0, import_react28.useEffect)(() => {
++ (0, import_react29.useEffect)(() => {
void actions.refreshModels?.();
}, [bot.id]);
- return (0, import_react18.createElement)(
-+ return (0, import_react28.createElement)(
++ return (0, import_react29.createElement)(
"div",
{ className: "bt-bs" },
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
"div",
{ className: "bt-bs-hero" },
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
"div",
{ className: "bt-avatar-anchor" },
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)(BotMark, { bot, size: 64, live: true }),
-+ (0, import_react28.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react28.createElement)(PencilIcon))
++ (0, import_react29.createElement)(BotMark, { bot, size: 64, live: true }),
++ (0, import_react29.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react29.createElement)(PencilIcon))
),
- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
-+ editing ? (0, import_react28.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
++ editing ? (0, import_react29.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null
),
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
"div",
{ className: "bt-bs-who" },
- (0, import_react18.createElement)("span", { className: "bt-bs-name" }, bot.name),
- (0, import_react18.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
-+ (0, import_react28.createElement)("span", { className: "bt-bs-name" }, bot.name),
-+ (0, import_react28.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
++ (0, import_react29.createElement)("span", { className: "bt-bs-name" }, bot.name),
++ (0, import_react29.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
)
),
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
Card,
{ title: t("Profile") },
- (0, import_react18.createElement)(Row, { label: t("Name") }, (0, import_react18.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })),
- (0, import_react18.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react18.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) }))
-+ (0, import_react28.createElement)(Row, { label: t("Name") }, (0, import_react28.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })),
-+ (0, import_react28.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react28.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) }))
++ (0, import_react29.createElement)(Row, { label: t("Name") }, (0, import_react29.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })),
++ (0, import_react29.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react29.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) }))
),
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
Card,
{ title: t("Model") },
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Model")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))),
- (0, import_react18.createElement)(ModelMenu, { bot, roster, actions, run })
- )
-+ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))),
-+ (0, import_react28.createElement)(ModelMenu, { bot, roster, actions, run })
++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))),
++ (0, import_react29.createElement)(ModelMenu, { bot, roster, actions, run })
+ ),
-+ bot.model ? (0, import_react28.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null
++ bot.model ? (0, import_react29.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null
),
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
Card,
{ title: t("Working folder") },
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
- (0, import_react18.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) })
-+ (0, import_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
-+ (0, import_react28.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) })
++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
++ (0, import_react29.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) })
)
),
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
Card,
{ title: t("Instructions") },
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) })
++ (0, import_react29.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 }),
@@ -12012,71 +12238,71 @@ diff --git a/client.js b/client.js
- (0, import_react18.createElement)(BotSchedules, { bot, roster, actions }),
- (0, import_react18.createElement)(
+ // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406).
-+ (0, import_react28.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react28.createElement)(Card, { title: t("Sharing") }, children) }),
-+ (0, import_react28.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }),
-+ (0, import_react28.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }),
-+ roster.memory === false ? null : (0, import_react28.createElement)(MemoryCard, { bot, actions }),
-+ (0, import_react28.createElement)(BotSchedules, { bot, roster, actions }),
-+ (0, import_react28.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }),
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react29.createElement)(Card, { title: t("Sharing") }, children) }),
++ (0, import_react29.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }),
++ (0, import_react29.createElement)(BotConnectorsCard, { bot, roster, actions, run, Card, SwitchRow }),
++ roster.memory === false ? null : (0, import_react29.createElement)(MemoryCard, { bot, actions }),
++ (0, import_react29.createElement)(BotSchedules, { bot, roster, actions }),
++ (0, import_react29.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }),
++ (0, import_react29.createElement)(
Card,
{ title: t("Main Bot") },
- onlyMain ? (0, import_react18.createElement)(
-+ onlyMain ? (0, import_react28.createElement)(
++ onlyMain ? (0, import_react29.createElement)(
"div",
{ className: "bt-set-row bt-bs-row" },
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page."))
++ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page."))
),
- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change"))
- ) : (0, import_react18.createElement)(SwitchRow, { label: t("Main Bot"), hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) })
-+ (0, import_react28.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change"))
-+ ) : (0, import_react28.createElement)(SwitchRow, { label: t("Main Bot"), showLabel: false, hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) })
++ (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change"))
++ ) : (0, import_react29.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_react28.createElement)(
++ isTile ? null : (0, import_react29.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_react28.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }),
-+ isMainBot ? null : (0, import_react28.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) })
++ (0, import_react29.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_react29.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) })
),
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.createElement)(
Card,
- { title: t("Manage") },
- (0, import_react18.createElement)(
+ { title: t("More") },
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)("span", { className: "bt-set-copy" }, (0, import_react28.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react28.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))),
-+ (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon))
++ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))),
++ (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon))
),
- isMainBot ? null : (0, import_react18.createElement)(
-+ isMainBot ? null : (0, import_react28.createElement)(
++ isMainBot ? null : (0, import_react29.createElement)(
"button",
{ type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove },
- (0, import_react18.createElement)(
-+ (0, import_react28.createElement)(
++ (0, import_react29.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_react28.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")),
-+ (0, import_react28.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team."))
++ (0, import_react29.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")),
++ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team."))
),
- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon))
-+ (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon))
++ (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon))
)
)
);
@@ -12111,10 +12337,10 @@ diff --git a/client.js b/client.js
+ .bt-bs-row>.bt-set-copy{flex:1 1 100%;min-width:0}
+ .bt-bs-input{flex:1 1 100%;width:auto;min-width:0;margin:0;text-align:left}
+}
- `;
-
++`;
++
+// src/client/group-settings.js
-+var import_react29 = require("react");
++var import_react30 = 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."]
@@ -12146,9 +12372,9 @@ diff --git a/client.js b/client.js
+ for (const seat of person?.seats ?? []) {
+ const bot = faceSeatBot(seat, roster);
+ if (!bot) continue;
-+ marks.push((0, import_react29.createElement)(BotMark, { key: seat.key, bot, size, live: false }));
++ marks.push((0, import_react30.createElement)(BotMark, { key: seat.key, bot, size, live: false }));
+ }
-+ if (marks.length === 0) return [(0, import_react29.createElement)(PersonMark, { key: "person", size, name: person?.name })];
++ if (marks.length === 0) return [(0, import_react30.createElement)(PersonMark, { key: "person", size, name: person?.name })];
+ return marks;
+}
+function faceRoomCount(room2, roster) {
@@ -12162,31 +12388,31 @@ diff --git a/client.js b/client.js
+var isLeadSeat = (seat, lead) => Boolean(handleKey(lead) && handleKey(seat.record?.handle) === handleKey(lead));
+function FacePersonRow({ person, onOpenBot, lead, roster, className = "bt-set-row bt-bs-row bt-gs-member", size = 26 }) {
+ const bots = person.seats.flatMap((seat, index) => {
-+ const name = seat.local && onOpenBot ? (0, import_react29.createElement)("button", { key: seat.key, type: "button", className: "bt-face-bot", title: t("Open {name}'s chat", { name: seat.bot }), onClick: () => onOpenBot(seat.local.id) }, seat.bot) : (0, import_react29.createElement)("span", { key: seat.key, className: "bt-face-bot", title: seat.record?.handle || void 0 }, seat.bot);
-+ const marked = isLeadSeat(seat, lead) ? [name, (0, import_react29.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name];
-+ return index === 0 ? marked : [(0, import_react29.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked];
++ const name = seat.local && onOpenBot ? (0, import_react30.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_react30.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_react30.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name];
++ return index === 0 ? marked : [(0, import_react30.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked];
+ });
-+ return (0, import_react29.createElement)(
++ return (0, import_react30.createElement)(
+ "div",
+ { className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "span",
+ { className: "bt-gs-who bt-face-person" },
-+ (0, import_react29.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)("span", { className: "bt-face-marks", "data-flybot": "face-person-avatars" }, faceMarks(person, roster, size)),
++ (0, import_react30.createElement)(
+ "span",
+ { className: "bt-face-person-text" },
-+ (0, import_react29.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
-+ (0, import_react29.createElement)("span", { className: "bt-face-bots" }, bots)
++ (0, import_react30.createElement)("span", { className: "bt-row-title" }, facePersonLabel(person)),
++ (0, import_react30.createElement)("span", { className: "bt-face-bots" }, bots)
+ )
+ )
+ );
+}
+function FaceLead({ room: room2, roster, pick: pick2 }) {
+ const lead = room2.face?.lead;
-+ const [open, setOpen] = (0, import_react29.useState)(false);
-+ const [busy, setBusy] = (0, import_react29.useState)(false);
-+ const [error, setError] = (0, import_react29.useState)("");
++ const [open, setOpen] = (0, import_react30.useState)(false);
++ const [busy, setBusy] = (0, import_react30.useState)(false);
++ const [error, setError] = (0, import_react30.useState)("");
+ const seats = faceSeats(room2, roster);
+ const current = seats.find((seat) => isLeadSeat(seat, lead?.handle));
+ if (!current) return null;
@@ -12203,30 +12429,30 @@ diff --git a/client.js b/client.js
+ setBusy(false);
+ }
+ };
-+ return (0, import_react29.createElement)(
++ return (0, import_react30.createElement)(
+ "div",
+ { className: "bt-face-lead", "data-flybot": "face-lead" },
-+ (0, import_react29.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)("div", { className: "bt-section-title" }, t("Room lead")),
++ (0, import_react30.createElement)(
+ "div",
+ { className: "bt-set-row" },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react29.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)),
-+ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, lead.picked ? t("Writes the plan and gives each Bot its part.") : t("Default: the first Bot of the room creator that joined."))
++ (0, import_react30.createElement)("span", { className: "bt-row-title" }, faceSeatLabel(current)),
++ (0, import_react30.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_react29.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null
++ canPick ? (0, import_react30.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_react29.createElement)(
++ open && canPick ? seats.map((seat) => (0, import_react30.createElement)(
+ "div",
+ { key: seat.key, className: "bt-set-row" },
-+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)),
-+ seat === current ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", disabled: busy, onClick: () => void choose(seat.record?.handle) }, t("Make lead"))
++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, faceSeatLabel(seat)),
++ seat === current ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Lead")) : (0, import_react30.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_react29.createElement)("button", { type: "button", className: "bt-more", disabled: busy, onClick: () => void choose(null) }, t("Use the default lead")) : null,
-+ canPick || !lead.supported ? null : (0, import_react29.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")),
-+ error ? (0, import_react29.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null
++ open && canPick && lead.picked ? (0, import_react30.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_react30.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")),
++ error ? (0, import_react30.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null
+ );
+}
+function seatedCount(room2) {
@@ -12242,9 +12468,9 @@ diff --git a/client.js b/client.js
+ return [t(count === 1 ? "{count} member" : "{count} members", { count }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · ");
+}
+function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
-+ const [adding, setAdding] = (0, import_react29.useState)(false);
-+ const [confirming, setConfirming] = (0, import_react29.useState)(false);
-+ (0, import_react29.useEffect)(() => {
++ const [adding, setAdding] = (0, import_react30.useState)(false);
++ const [confirming, setConfirming] = (0, import_react30.useState)(false);
++ (0, import_react30.useEffect)(() => {
+ if (!confirming) return void 0;
+ const timer = setTimeout(() => setConfirming(false), 4e3);
+ return () => clearTimeout(timer);
@@ -12282,41 +12508,41 @@ diff --git a/client.js b/client.js
+ if (done) onDeleted?.();
+ });
+ };
-+ return (0, import_react29.createElement)(
++ return (0, import_react30.createElement)(
+ "div",
+ { className: "bt-bs" },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "div",
+ { className: "bt-bs-hero" },
-+ (0, import_react29.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
++ (0, import_react30.createElement)(
+ "div",
+ { className: "bt-bs-who" },
-+ (0, import_react29.createElement)("span", { className: "bt-bs-name" }, room2.name),
-+ (0, import_react29.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
++ (0, import_react30.createElement)("span", { className: "bt-bs-name" }, room2.name),
++ (0, import_react30.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
+ )
+ ),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ Card,
+ { title: t("Profile") },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ Row,
+ { label: t("Name"), hint: t("Leave it empty to name the group after its members.") },
-+ (0, import_react29.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) })
++ (0, import_react30.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) })
+ ),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ Row,
+ { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true },
-+ (0, import_react29.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) })
++ (0, import_react30.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) })
+ )
+ ),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ Card,
+ { title: t("Who answers") },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row bt-bs-stack" },
-+ (0, import_react29.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react29.createElement)("button", {
++ (0, import_react30.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react30.createElement)("button", {
+ key: id,
+ type: "button",
+ "aria-pressed": mode === id,
@@ -12326,108 +12552,108 @@ diff --git a/client.js b/client.js
+ if (mode !== id) void update({ mode: id });
+ }
+ }, t(label)))),
-+ (0, import_react29.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
++ (0, import_react30.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`)
+ )
+ ),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ Card,
+ { title: t("Working folder") },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row" },
-+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
-+ (0, import_react29.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) })
++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
++ (0, import_react30.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) })
+ )
+ ),
-+ face ? (0, import_react29.createElement)(
++ face ? (0, import_react30.createElement)(
+ Card,
+ { title: t("Members · {count}", { count: people2.length }) },
-+ people2.map((person) => (0, import_react29.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
++ people2.map((person) => (0, import_react30.createElement)(FacePersonRow, { key: person.key, person, roster, onOpenBot: openBot, lead: room2.face?.lead?.handle }))
+ ) : null,
-+ face ? null : (0, import_react29.createElement)(
++ face ? null : (0, import_react30.createElement)(
+ Card,
+ { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) },
-+ humans.map((person) => (0, import_react29.createElement)(
++ humans.map((person) => (0, import_react30.createElement)(
+ "div",
+ { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "span",
+ { className: "bt-gs-who" },
-+ (0, import_react29.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id),
-+ (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Human")),
-+ person.role === "admin" ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Admin")) : null
++ (0, import_react30.createElement)("span", { className: "bt-row-title" }, person.displayName || person.id),
++ (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Human")),
++ person.role === "admin" ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Admin")) : null
+ ),
-+ humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null
++ humans.length + members.length + remotes.length > 2 && person.id !== roster.self?.memberId ? (0, import_react30.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: person.displayName || person.id }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "human", id: person.id }] }) }, "×") : null
+ )),
+ members.map((member) => {
+ const leads = room2.admin === member.id;
+ const boardHere = room2.origin !== "face-to-face" && room2.origin !== "inbound-share";
+ const taskLead = room2.taskLead === member.id;
-+ return (0, import_react29.createElement)(
++ return (0, import_react30.createElement)(
+ "div",
+ { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "button",
+ { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) },
-+ (0, import_react29.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
-+ (0, import_react29.createElement)("span", { className: "bt-row-title" }, member.name),
-+ leads ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Admin")) : null,
-+ taskLead ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null,
-+ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react29.createElement)("span", { className: "bt-tag" }, t("Chief")) : null
++ (0, import_react30.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }),
++ (0, import_react30.createElement)("span", { className: "bt-row-title" }, member.name),
++ leads ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Admin")) : null,
++ taskLead ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Task lead")) : null,
++ (roster.humans ?? []).some((person) => person.designatedChiefId === member.id) ? (0, import_react30.createElement)("span", { className: "bt-tag" }, t("Chief")) : null
+ ),
-+ leads ? (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")),
-+ boardHere ? taskLead ? (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null,
-+ members.length > 2 ? (0, import_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null
++ leads ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")),
++ boardHere ? taskLead ? (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: null }) }, t("Use the admin")) : (0, import_react30.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ taskLead: member.id }) }, t("Make task lead")) : null,
++ members.length > 2 ? (0, import_react30.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_react29.createElement)(
++ remotes.map((person) => (0, import_react30.createElement)(
+ "div",
+ { key: person.handle || person.id, className: "bt-set-row bt-bs-row bt-gs-member" },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "span",
+ { className: "bt-gs-who" },
-+ (0, import_react29.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
-+ (0, import_react29.createElement)("span", { className: "bt-row-title", title: person.handle || void 0 }, person.label || remoteBotTitle(person)),
++ (0, import_react30.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
++ (0, import_react30.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_react29.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_react30.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_react29.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: remoteBotTitle(person) }), title: t("Remove from group"), onClick: () => update({ removeParticipants: [{ kind: "bot", id: person.handle || person.id, remote: true }] }) }, "×")
++ room2.origin === "face-to-face" || room2.origin === "inbound-share" || members.length + remotes.length <= 2 ? null : (0, import_react30.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_react29.createElement)(
++ ...outsidePeople.map((person) => (0, import_react30.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_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)),
-+ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add person"))
++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, person.displayName || person.id)),
++ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add person"))
+ )),
-+ ...outside.map((bot) => (0, import_react29.createElement)(
++ ...outside.map((bot) => (0, import_react30.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_react29.createElement)(BotMark, { bot, size: 22 }),
-+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, bot.name)),
-+ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add"))
++ (0, import_react30.createElement)(BotMark, { bot, size: 22 }),
++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, bot.name)),
++ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add"))
+ )),
-+ ...outsideRemotes.map((bot) => (0, import_react29.createElement)(
++ ...outsideRemotes.map((bot) => (0, import_react30.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_react29.createElement)(BotAvatar, { bot: lookBotForRemote({ handle: bot.handle, displayName: bot.name, avatar: bot.avatar, color: bot.color }, roster), size: 22, badge: false, live: false }),
-+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))),
-+ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot"))
++ (0, import_react30.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_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, remoteBotTitle({ displayName: bot.name, ownerName: bot.homeName || bot.owner, handle: bot.handle, isChief: bot.isChief }))),
++ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, t("Add friend's Bot"))
+ )),
-+ (0, import_react29.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel"))
-+ ] : outside.length + outsidePeople.length + outsideRemotes.length > 0 ? (0, import_react29.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react29.createElement)(PlusIcon), t("Add member")) : null,
-+ (0, import_react29.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react29.createElement)(
++ (0, import_react30.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_react30.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react30.createElement)(PlusIcon), t("Add member")) : null,
++ (0, import_react30.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react30.createElement)(
+ "span",
+ { className: "bt-set-hint" },
+ // The host's two-Bot floor guards membership changes; this hint warns before one.
@@ -12435,37 +12661,37 @@ diff --git a/client.js b/client.js
+ )),
+ // #876: a plugin's rows for this room (Invite by link), only in a group that lives here. Last, after
+ // the note on who leads, so that note stays with the members it is about (#892).
-+ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react29.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
++ room2.origin === "face-to-face" || room2.origin === "inbound-share" ? null : (0, import_react30.createElement)(FlyBotRoomMembers, { room: room2, roster, actions })
+ ),
-+ (0, import_react29.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }),
++ (0, import_react30.createElement)(
+ Card,
+ { title: t("Sidebar") },
-+ (0, import_react29.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }),
-+ (0, import_react29.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) })
++ (0, import_react30.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_react30.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) })
+ ),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ Card,
+ { title: t("More") },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "button",
+ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react29.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")),
-+ (0, import_react29.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team."))
++ (0, import_react30.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")),
++ (0, import_react30.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team."))
+ ),
-+ (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon))
++ (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon))
+ )
+ )
+ );
+}
+function NewGroupForm({ roster, actions, run, onCreated, onCancel }) {
-+ const [admin, setAdmin] = (0, import_react29.useState)(null);
-+ const [members, setMembers] = (0, import_react29.useState)([]);
-+ const [name, setName] = (0, import_react29.useState)("");
-+ const [busy, setBusy] = (0, import_react29.useState)(false);
++ const [admin, setAdmin] = (0, import_react30.useState)(null);
++ const [members, setMembers] = (0, import_react30.useState)([]);
++ const [name, setName] = (0, import_react30.useState)("");
++ const [busy, setBusy] = (0, import_react30.useState)(false);
+ const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]);
+ const pickAdmin = (id) => {
+ setAdmin(id);
@@ -12481,48 +12707,48 @@ diff --git a/client.js b/client.js
+ if (done && room2) onCreated(room2);
+ else setBusy(false);
+ };
-+ const chip = (bot, on, onClick, role) => (0, import_react29.createElement)("button", {
++ const chip = (bot, on, onClick, role) => (0, import_react30.createElement)("button", {
+ key: bot.id,
+ type: "button",
+ role,
+ className: "bt-ng-chip",
+ "aria-checked": on,
+ onClick
-+ }, (0, import_react29.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react29.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react29.createElement)(AdminStar) : null);
-+ return (0, import_react29.createElement)(
++ }, (0, import_react30.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react30.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react30.createElement)(AdminStar) : null);
++ return (0, import_react30.createElement)(
+ "div",
+ { className: "bt-set-card bt-ng" },
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row bt-bs-stack" },
-+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react29.createElement)(AdminStar), t("Choose the group admin"))),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react30.createElement)(AdminStar), t("Choose the group admin"))),
++ (0, import_react30.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_react29.createElement)(
++ admin === null ? null : (0, import_react30.createElement)(
+ "div",
+ { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" },
-+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))),
++ (0, import_react30.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_react29.createElement)(
++ admin === null || members.length === 0 ? null : (0, import_react30.createElement)(
+ "label",
+ { className: "bt-set-row bt-bs-row bt-ng-step" },
-+ (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
-+ (0, import_react29.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) })
++ (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Name"))),
++ (0, import_react30.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) })
+ ),
-+ (0, import_react29.createElement)(
++ (0, import_react30.createElement)(
+ "div",
+ { className: "bt-set-row bt-task-foot" },
-+ (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
-+ (0, import_react29.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat"))
++ (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")),
++ (0, import_react30.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat"))
+ )
+ );
+}
@@ -12555,17 +12781,17 @@ diff --git a/client.js b/client.js
+.bt-gs-pick{gap:10px;animation:bt-rise .16s ease-out}
+.bt-bs-stack .bt-seg{align-self:stretch}
+@media (prefers-reduced-motion:reduce){.bt-gs-pick{animation:none}}
-+`;
-+
+ `;
+
+// src/client/palette.js
-+var import_react30 = require("react");
++var import_react31 = require("react");
+var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
+var paletteText = (raw, names) => readableLine(String(raw ?? ""), names);
+var PALETTE_EXIT_MS = 160;
+function CommandPalette({ start, token, roster, actions, useSessions }) {
-+ const [query, setQuery] = (0, import_react30.useState)(start ?? "");
-+ const [cursor, setCursor] = (0, import_react30.useState)(0);
-+ const [leaving, setLeaving] = (0, import_react30.useState)(false);
++ const [query, setQuery] = (0, import_react31.useState)(start ?? "");
++ const [cursor, setCursor] = (0, import_react31.useState)(0);
++ const [leaving, setLeaving] = (0, import_react31.useState)(false);
+ const wanted = query.trim().toLowerCase();
+ const close = () => {
+ if (leaving) return;
@@ -12602,7 +12828,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_react30.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }),
++ avatar: (0, import_react31.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,
@@ -12613,7 +12839,7 @@ diff --git a/client.js b/client.js
+ })),
+ ...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({
+ key: room2.id,
-+ avatar: (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
++ avatar: (0, import_react31.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,
@@ -12626,7 +12852,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_react30.createElement)("span", { className: "bt-palette-agent" }, (0, import_react30.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
++ avatar: (0, import_react31.createElement)("span", { className: "bt-palette-agent" }, (0, import_react31.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })),
+ title: "DSH Agent",
+ subtitle: t("Plain dsh session"),
+ run: () => actions.openAgent(agent.id)
@@ -12639,16 +12865,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_react30.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react30.createElement)(RoomAvatar, { room: room2, roster, size: 24 }),
++ avatar: bot ? (0, import_react31.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react31.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_react30.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") },
-+ { key: "cmd:bot", icon: (0, import_react30.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") },
-+ { key: "cmd:group", icon: (0, import_react30.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") },
-+ { key: "cmd:settings", icon: (0, import_react30.createElement)(GearIcon), title: t("Settings"), run: () => actions.openSettings("general") }
++ { key: "cmd:new", icon: (0, import_react31.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") },
++ { key: "cmd:bot", icon: (0, import_react31.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") },
++ { key: "cmd:group", icon: (0, import_react31.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") },
++ { key: "cmd:settings", icon: (0, import_react31.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);
@@ -12658,7 +12884,7 @@ diff --git a/client.js b/client.js
+${shown}`, { axis: "y", ...QUICK_EDGES });
+ const listRef = glide.box;
+ useFlip(listRef, shown, "[data-flip]");
-+ (0, import_react30.useEffect)(() => {
++ (0, import_react31.useEffect)(() => {
+ listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" });
+ }, [active, query]);
+ const choose = (row) => {
@@ -12667,18 +12893,18 @@ diff --git a/client.js b/client.js
+ close();
+ };
+ let index = -1;
-+ return (0, import_react30.createElement)(
++ return (0, import_react31.createElement)(
+ "div",
+ { className: "bt-palette-layer", "data-leaving": leaving || void 0 },
-+ (0, import_react30.createElement)("div", { className: "bt-backdrop", onMouseDown: close }),
-+ (0, import_react30.createElement)(
++ (0, import_react31.createElement)("div", { className: "bt-backdrop", onMouseDown: close }),
++ (0, import_react31.createElement)(
+ "div",
+ { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true },
-+ (0, import_react30.createElement)(
++ (0, import_react31.createElement)(
+ "div",
+ { className: "bt-palette-head" },
-+ (0, import_react30.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react30.createElement)(SearchIcon)),
-+ (0, import_react30.createElement)("input", {
++ (0, import_react31.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react31.createElement)(SearchIcon)),
++ (0, import_react31.createElement)("input", {
+ autoFocus: true,
+ value: query,
+ placeholder: t("Search Bots, chats and messages"),
@@ -12704,29 +12930,29 @@ diff --git a/client.js b/client.js
+ }
+ }
+ }),
-+ query ? (0, import_react30.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
++ query ? (0, import_react31.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => {
+ setQuery("");
+ setCursor(0);
-+ } }, (0, import_react30.createElement)(CloseIcon)) : null
++ } }, (0, import_react31.createElement)(CloseIcon)) : null
+ ),
-+ (0, import_react30.createElement)(
++ (0, import_react31.createElement)(
+ "div",
+ { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") },
-+ (0, import_react30.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ rows.length === 0 ? (0, import_react30.createElement)(
++ (0, import_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ rows.length === 0 ? (0, import_react31.createElement)(
+ "div",
+ { className: "bt-palette-empty" },
-+ (0, import_react30.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react30.createElement)(SearchIcon)),
-+ (0, import_react30.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")),
-+ (0, import_react30.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message"))
-+ ) : sections.map(([title, items]) => (0, import_react30.createElement)(
-+ import_react30.Fragment,
++ (0, import_react31.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react31.createElement)(SearchIcon)),
++ (0, import_react31.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")),
++ (0, import_react31.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message"))
++ ) : sections.map(([title, items]) => (0, import_react31.createElement)(
++ import_react31.Fragment,
+ { key: title },
-+ (0, import_react30.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
++ (0, import_react31.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title),
+ items.map((row) => {
+ index += 1;
+ const at = index;
-+ return (0, import_react30.createElement)(
++ return (0, import_react31.createElement)(
+ "button",
+ {
+ key: row.key,
@@ -12740,14 +12966,14 @@ diff --git a/client.js b/client.js
+ },
+ onClick: () => choose(row)
+ },
-+ (0, import_react30.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react30.createElement)("span", { className: "bt-palette-cmd" }, row.icon)),
-+ (0, import_react30.createElement)(
++ (0, import_react31.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react31.createElement)("span", { className: "bt-palette-cmd" }, row.icon)),
++ (0, import_react31.createElement)(
+ "span",
+ { className: "bt-palette-text" },
-+ (0, import_react30.createElement)("span", { className: "bt-palette-title" }, row.title),
-+ row.subtitle ? (0, import_react30.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null
++ (0, import_react31.createElement)("span", { className: "bt-palette-title" }, row.title),
++ row.subtitle ? (0, import_react31.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null
+ ),
-+ row.badge ? (0, import_react30.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null
++ row.badge ? (0, import_react31.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null
+ );
+ })
+ ))
@@ -12790,7 +13016,7 @@ diff --git a/client.js b/client.js
// src/client/create-bot.js
-var import_react19 = require("react");
-+var import_react31 = require("react");
++var import_react32 = require("react");
var pick = (list) => list[Math.floor(Math.random() * list.length)];
var randomLook = (avoid = {}) => {
const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape);
@@ -12808,18 +13034,18 @@ diff --git a/client.js b/client.js
- const fileRef = (0, import_react19.useRef)(null);
- const frame = (0, import_react19.useRef)(null);
+function CreateBot({ roster, actions, start, onBack, canGoBack, onCreated, inline = false }) {
-+ const [look, setLook] = (0, import_react31.useState)(() => randomLook());
-+ const [image, setImage] = (0, import_react31.useState)(null);
-+ const [name, setName] = (0, import_react31.useState)(start?.name ?? "");
-+ const [model, setModel] = (0, import_react31.useState)(() => startModel(roster));
-+ const [effort, setEffort] = (0, import_react31.useState)(void 0);
-+ const [busy, setBusy] = (0, import_react31.useState)(false);
-+ const [error, setError] = (0, import_react31.useState)("");
-+ const fileRef = (0, import_react31.useRef)(null);
-+ const frame = (0, import_react31.useRef)(null);
++ const [look, setLook] = (0, import_react32.useState)(() => randomLook());
++ const [image, setImage] = (0, import_react32.useState)(null);
++ const [name, setName] = (0, import_react32.useState)(start?.name ?? "");
++ const [model, setModel] = (0, import_react32.useState)(() => startModel(roster));
++ const [effort, setEffort] = (0, import_react32.useState)(void 0);
++ const [busy, setBusy] = (0, import_react32.useState)(false);
++ const [error, setError] = (0, import_react32.useState)("");
++ const fileRef = (0, import_react32.useRef)(null);
++ const frame = (0, import_react32.useRef)(null);
useEscape(onBack);
- (0, import_react19.useEffect)(() => {
-+ (0, import_react31.useEffect)(() => {
++ (0, import_react32.useEffect)(() => {
if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" });
}, []);
const fallbackName = freshBotName(roster, t("New Bot"));
@@ -12839,32 +13065,32 @@ diff --git a/client.js b/client.js
};
const previewLook = image ? { ...look, image } : look;
- const card = (0, import_react19.createElement)(
-+ const card = (0, import_react31.createElement)(
++ const card = (0, import_react32.createElement)(
"div",
{ className: "bt-create-card" },
- (0, import_react19.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.createElement)(
"div",
{ className: "bt-create-preview" },
// The key remounts the mark, so every new pick plays the pop.
- (0, import_react19.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)(BotMark, { look: previewLook, bot: { id: "new-bot" }, size: 88, live: true, gaze: true, pokeable: true })
++ (0, import_react32.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_react31.createElement)("span", { className: "bt-create-name" }, finalName)
++ (0, import_react32.createElement)("span", { className: "bt-create-name" }, finalName)
),
- (0, import_react19.createElement)("div", { className: "bt-create-step" }, t("Look")),
- (0, import_react19.createElement)(
-+ (0, import_react31.createElement)("div", { className: "bt-create-step" }, t("Look")),
-+ (0, import_react31.createElement)(
++ (0, import_react32.createElement)("div", { className: "bt-create-step" }, t("Look")),
++ (0, import_react32.createElement)(
"div",
{ className: "bt-create-looks" },
- (0, import_react19.createElement)(CharacterGrid, {
-+ (0, import_react31.createElement)(CharacterGrid, {
++ (0, import_react32.createElement)(CharacterGrid, {
look,
hasPhoto: Boolean(image),
onShape: (shape) => {
@@ -12873,20 +13099,20 @@ diff --git a/client.js b/client.js
}
}),
- (0, import_react19.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.createElement)(
"div",
{ className: "bt-actions bt-create-lookbar" },
- (0, import_react19.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
-+ (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
++ (0, import_react32.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_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")),
-+ image ? (0, import_react31.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null,
-+ (0, import_react31.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => {
++ (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, image ? t("Replace photo") : t("Upload photo")),
++ image ? (0, import_react32.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setImage(null) }, t("Remove photo")) : null,
++ (0, import_react32.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);
@@ -12894,13 +13120,13 @@ diff --git a/client.js b/client.js
)
),
- (0, import_react19.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.createElement)(
"label",
{ className: "bt-field" },
- (0, import_react19.createElement)("span", { className: "bt-field-label" }, t("Name")),
- (0, import_react19.createElement)("input", {
-+ (0, import_react31.createElement)("span", { className: "bt-field-label" }, t("Name")),
-+ (0, import_react31.createElement)("input", {
++ (0, import_react32.createElement)("span", { className: "bt-field-label" }, t("Name")),
++ (0, import_react32.createElement)("input", {
className: "bt-input",
autoFocus: !inline,
value: name,
@@ -12909,61 +13135,61 @@ diff --git a/client.js b/client.js
}
}),
- problem ? (0, import_react19.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react19.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null
-+ problem ? (0, import_react31.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react31.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null
++ problem ? (0, import_react32.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react32.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null
),
- (0, import_react19.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)("span", { className: "bt-field-label" }, t("Model")),
-+ (0, import_react31.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => {
++ (0, import_react32.createElement)("span", { className: "bt-field-label" }, t("Model")),
++ (0, import_react32.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_react31.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later."))
++ (0, import_react32.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_react31.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
-+ (0, import_react31.createElement)(
++ error ? (0, import_react32.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
++ (0, import_react32.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_react31.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null,
-+ (0, import_react31.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create"))
++ inline ? (0, import_react32.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")) : null,
++ (0, import_react32.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_react31.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card);
-+ return (0, import_react31.createElement)(
++ if (inline) return (0, import_react32.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card);
++ return (0, import_react32.createElement)(
"div",
{ className: "bt-create", role: "dialog", "aria-label": t("Create Bot") },
- (0, import_react19.createElement)(
-+ (0, import_react31.createElement)(
++ (0, import_react32.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_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react31.createElement)(ChevronLeftIcon)) : (0, import_react31.createElement)("span", { className: "bt-create-gap" }),
-+ (0, import_react31.createElement)("span", { className: "bt-create-title" }, t("Create Bot")),
-+ (0, import_react31.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react31.createElement)(CloseIcon))
++ canGoBack ? (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to new chat"), onClick: onBack }, (0, import_react32.createElement)(ChevronLeftIcon)) : (0, import_react32.createElement)("span", { className: "bt-create-gap" }),
++ (0, import_react32.createElement)("span", { className: "bt-create-title" }, t("Create Bot")),
++ (0, import_react32.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close"), onClick: () => actions.closeOverlay() }, (0, import_react32.createElement)(CloseIcon))
),
- (0, import_react19.createElement)("div", { className: "bt-create-body" }, card)
-+ (0, import_react31.createElement)("div", { className: "bt-create-body" }, card)
++ (0, import_react32.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_react32 = require("react");
++var import_react33 = require("react");
function NewChat({ mode, roster, actions }) {
- const [query, setQuery] = (0, import_react20.useState)("");
- const [picked, setPicked] = (0, import_react20.useState)([]);
@@ -12975,20 +13201,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_react32.useState)("");
-+ const [picked, setPicked] = (0, import_react32.useState)([]);
-+ const [group, setGroup] = (0, import_react32.useState)(mode === "group");
-+ const [compose, setCompose] = (0, import_react32.useState)(mode !== "new");
-+ const [admin, setAdmin] = (0, import_react32.useState)(null);
-+ const [message, setMessage] = (0, import_react32.useState)("");
-+ const [busy, setBusy] = (0, import_react32.useState)(false);
-+ const [error, setError] = (0, import_react32.useState)("");
-+ const [fix, setFix] = (0, import_react32.useState)(null);
-+ const [fixNote, setFixNote] = (0, import_react32.useState)("");
-+ const [inviteLink, setInviteLink] = (0, import_react32.useState)("");
-+ const [cursor, setCursor] = (0, import_react32.useState)(0);
-+ const [draft, setDraft] = (0, import_react32.useState)(mode === "create" ? { name: "", fromList: false } : null);
-+ const inputRef = (0, import_react32.useRef)(null);
++ const [query, setQuery] = (0, import_react33.useState)("");
++ const [picked, setPicked] = (0, import_react33.useState)([]);
++ const [group, setGroup] = (0, import_react33.useState)(mode === "group");
++ const [compose, setCompose] = (0, import_react33.useState)(mode !== "new");
++ const [admin, setAdmin] = (0, import_react33.useState)(null);
++ const [message, setMessage] = (0, import_react33.useState)("");
++ const [busy, setBusy] = (0, import_react33.useState)(false);
++ const [error, setError] = (0, import_react33.useState)("");
++ const [fix, setFix] = (0, import_react33.useState)(null);
++ const [fixNote, setFixNote] = (0, import_react33.useState)("");
++ const [inviteLink, setInviteLink] = (0, import_react33.useState)("");
++ const [cursor, setCursor] = (0, import_react33.useState)(0);
++ const [draft, setDraft] = (0, import_react33.useState)(mode === "create" ? { name: "", fromList: false } : null);
++ const inputRef = (0, import_react33.useRef)(null);
const close = () => actions.closeOverlay();
useEscape(close);
const paneLeft = usePaneLeft();
@@ -13107,14 +13333,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_react32.createElement)(ChatIcon), label: t("New chat"), hint: t("Message one of your Bots"), run: () => {
++ options.push({ key: "chat", icon: (0, import_react33.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_react32.createElement)(BotIcon), label: t("New Bot"), hint: t("Set up a Bot with its own role"), run: createBot });
-+ options.push({ key: "group", icon: (0, import_react32.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => {
++ options.push({ key: "new", icon: (0, import_react33.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_react33.createElement)(PeopleIcon), label: t("New group chat"), hint: t("Chat with several of your Bots"), run: () => {
setGroup(true);
+ setCompose(true);
setCursor(0);
@@ -13123,7 +13349,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_react32.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react32.createElement)(PeopleIcon) }) : (0, import_react32.createElement)(PeopleIcon),
++ icon: action.icon ? (0, import_react33.createElement)(SlotIcon, { id: action.key, icon: action.icon, fallback: (0, import_react33.createElement)(PeopleIcon) }) : (0, import_react33.createElement)(PeopleIcon),
+ label: t(action.label),
+ hint: action.hint ? t(action.hint) : void 0,
+ run: () => {
@@ -13136,7 +13362,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_react32.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_react33.createElement)(TerminalIcon), label: t("Add coding agent"), hint: t("A coding session without a Bot"), run: () => run(async () => {
await actions.addAgent();
close();
}) });
@@ -13150,22 +13376,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_react32.createElement)(PlusIcon), label: t("New Bot “{name}”", { name: query.trim() }), run: createBot });
++ if (wanted !== "" && !exact && mode !== "search") options.push({ key: "create", icon: (0, import_react33.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_react32.useRef)(null);
++ const drop = (0, import_react33.useRef)(null);
useMorph(drop, listed);
if (draft) {
- return (0, import_react20.createElement)(
-+ return (0, import_react32.createElement)(
++ return (0, import_react33.createElement)(
"div",
{ className: "bt-pane", style: { left: paneLeft } },
- (0, import_react20.createElement)(CreateBot, {
-+ (0, import_react32.createElement)(CreateBot, {
++ (0, import_react33.createElement)(CreateBot, {
roster,
actions,
start: draft,
@@ -13178,7 +13404,7 @@ diff --git a/client.js b/client.js
else if (admin !== null) setAdmin(null);
};
- return (0, import_react20.createElement)(
-+ return (0, import_react32.createElement)(
++ return (0, import_react33.createElement)(
"div",
- { className: "bt-pane", style: { left: paneLeft } },
- (0, import_react20.createElement)(
@@ -13190,43 +13416,43 @@ diff --git a/client.js b/client.js
+ if (event.target === event.currentTarget) close();
+ } : void 0
+ },
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)(
++ menuOnly ? null : (0, import_react33.createElement)(
"div",
{ className: "bt-to" },
- (0, import_react20.createElement)("span", { className: "bt-to-label" }, t("To:")),
- adminBot ? (0, import_react20.createElement)(
-+ (0, import_react32.createElement)("span", { className: "bt-to-label" }, t("To:")),
-+ adminBot ? (0, import_react32.createElement)(
++ (0, import_react33.createElement)("span", { className: "bt-to-label" }, t("To:")),
++ adminBot ? (0, import_react33.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_react32.createElement)(BotMark, { bot: adminBot, size: 16 }),
++ (0, import_react33.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_react32.createElement)(AdminStar),
-+ (0, import_react32.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: adminBot.name }), onClick: () => setAdmin(null) }, "×")
++ (0, import_react33.createElement)(AdminStar),
++ (0, import_react33.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_react32.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react32.createElement)(AdminStar)) : null,
-+ pickedBots.map((bot) => (0, import_react32.createElement)(
++ choosingAdmin ? (0, import_react33.createElement)("span", { className: "bt-to-star", "aria-hidden": true }, (0, import_react33.createElement)(AdminStar)) : null,
++ pickedBots.map((bot) => (0, import_react33.createElement)(
"span",
{ key: bot.id, className: "bt-token" },
- (0, import_react20.createElement)(BotMark, { bot, size: 16 }),
-+ (0, import_react32.createElement)(BotMark, { bot, size: 16 }),
++ (0, import_react33.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_react32.createElement)("button", { type: "button", "aria-label": t("Remove {name}", { name: bot.name }), onClick: () => setPicked((current) => current.filter((id) => id !== bot.id)) }, "×")
++ (0, import_react33.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_react32.createElement)("input", {
++ (0, import_react33.createElement)("input", {
ref: inputRef,
autoFocus: true,
value: query,
@@ -13235,7 +13461,7 @@ diff --git a/client.js b/client.js
}
}),
- group && admin !== null ? (0, import_react20.createElement)("button", {
-+ group && admin !== null ? (0, import_react32.createElement)("button", {
++ group && admin !== null ? (0, import_react33.createElement)("button", {
type: "button",
className: "bt-send bt-to-create",
disabled: busy || picked.length === 0,
@@ -13243,16 +13469,16 @@ diff --git a/client.js b/client.js
onClick: createGroup
}, t("Create group chat")) : null,
- (0, import_react20.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×")
-+ (0, import_react32.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Close new chat"), onClick: close }, "×")
++ (0, import_react33.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_react32.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react32.createElement)(
++ (0, import_react33.createElement)("div", { ref: drop, className: "bt-drop" }, (0, import_react33.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_react32.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ options.length === 0 ? (0, import_react32.createElement)("div", { className: "bt-hint" }, mode === "search" ? t("No Bots match") : t("Type a name to create a Bot")) : options.map((option, index) => (0, import_react32.createElement)(
++ (0, import_react33.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ options.length === 0 ? (0, import_react33.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_react33.createElement)(
"button",
{
key: option.key,
@@ -13263,42 +13489,42 @@ diff --git a/client.js b/client.js
- option.bot ? (0, import_react20.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react20.createElement)("span", { className: "bt-option-icon" }, option.icon),
- (0, import_react20.createElement)("span", { className: "bt-option-label" }, option.label),
- option.hint && index === active ? (0, import_react20.createElement)("span", { className: "bt-option-hint" }, option.hint) : null
-+ option.bot ? (0, import_react32.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react32.createElement)("span", { className: "bt-option-icon" }, option.icon),
++ option.bot ? (0, import_react33.createElement)(BotMark, { bot: option.bot, size: 22 }) : (0, import_react33.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_react32.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react32.createElement)("span", { className: "bt-option-copy" }, (0, import_react32.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react32.createElement)("span", { className: "bt-option-sub" }, option.hint)),
-+ option.bot && option.hint && index === active ? (0, import_react32.createElement)("span", { className: "bt-option-hint" }, option.hint) : null
++ option.bot || !option.hint ? (0, import_react33.createElement)("span", { className: "bt-option-label" }, option.label) : (0, import_react33.createElement)("span", { className: "bt-option-copy" }, (0, import_react33.createElement)("span", { className: "bt-option-label" }, option.label), (0, import_react33.createElement)("span", { className: "bt-option-sub" }, option.hint)),
++ option.bot && option.hint && index === active ? (0, import_react33.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_react32.createElement)("div", { className: "bt-nc-spacer" }),
-+ error && fix ? (0, import_react32.createElement)(
- "div",
++ menuOnly ? null : (0, import_react33.createElement)("div", { className: "bt-nc-spacer" }),
++ error && fix ? (0, import_react33.createElement)(
++ "div",
+ { className: "bt-error bt-fix", role: "alert", "data-fix": "ungranted-local" },
-+ (0, import_react32.createElement)("div", null, fix.bots.length === 1 ? t("{name} is not shared with {friend}, so it cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots[0].name, friend: fix.friends.join(", ") || t("that friend") }) : t("{name} are not shared with {friend}, so they cannot be in a group with their Bot: the group's messages go to their home.", { name: fix.bots.map((bot) => bot.name).join(", "), friend: fix.friends.join(", ") || t("that friend") })),
-+ (0, import_react32.createElement)("div", { className: "bt-fix-actions" }, fix.bots.flatMap((bot) => [
-+ typeof globalThis.__flybot__?.social?.createInvite === "function" && globalThis.__flybot__.social.canInvite?.(bot.id) !== false ? (0, import_react32.createElement)("button", { key: `invite-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "invite", onClick: () => {
++ (0, import_react33.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_react33.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_react33.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_react32.createElement)("button", { key: `out-${bot.id}`, type: "button", className: "bt-fix-btn", "data-fix-action": "leave-out", onClick: () => leaveOut(bot.id) }, t("Leave {name} out", { name: bot.name }))
++ (0, import_react33.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_react32.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null,
-+ inviteLink ? (0, import_react32.createElement)("input", { className: "bt-input bt-fix-link", type: "text", readOnly: true, value: inviteLink, "aria-label": t("Invite link"), "data-fix-link": "", autoFocus: true, onFocus: (event) => event.target.select(), onClick: (event) => event.target.select() }) : null
-+ ) : error ? (0, import_react32.createElement)("div", { className: "bt-error" }, error) : null,
-+ menuOnly ? null : (0, import_react32.createElement)(
-+ "div",
++ fixNote ? (0, import_react33.createElement)("div", { className: "bt-note", role: "status" }, fixNote) : null,
++ inviteLink ? (0, import_react33.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_react33.createElement)("div", { className: "bt-error" }, error) : null,
++ menuOnly ? null : (0, import_react33.createElement)(
+ "div",
{ className: "bt-nc-compose" },
- (0, import_react20.createElement)(
-+ (0, import_react32.createElement)(
++ (0, import_react33.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_react32.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react32.createElement)(PlusIcon)),
-+ (0, import_react32.createElement)("textarea", {
++ (0, import_react33.createElement)("span", { className: "bt-nc-plus", "aria-hidden": true }, (0, import_react33.createElement)(PlusIcon)),
++ (0, import_react33.createElement)("textarea", {
rows: 1,
value: message,
placeholder,
@@ -13307,7 +13533,7 @@ diff --git a/client.js b/client.js
}
}),
- (0, import_react20.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react20.createElement)(ArrowUpIcon))
-+ (0, import_react32.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react32.createElement)(ArrowUpIcon))
++ (0, import_react33.createElement)("button", { type: "button", className: "bt-nc-send", "aria-label": t("Send message"), disabled: busy || message.trim() === "" || picked.length === 0, onClick: sendMessage }, (0, import_react33.createElement)(ArrowUpIcon))
)
)
)
@@ -13316,7 +13542,7 @@ diff --git a/client.js b/client.js
// src/client/exchange.js
-var import_react21 = require("react");
-+var import_react33 = require("react");
++var import_react34 = require("react");
var SHEET_EXIT_MS = 220;
+function exchangeText(text2) {
+ if (typeof text2 !== "string") return "";
@@ -13334,18 +13560,18 @@ diff --git a/client.js b/client.js
+}
+function ExchangeLine({ entry, roster, actions }) {
+ const bot = roster.byId[entry.from] ?? { id: entry.from, name: t("You"), color: "gray" };
-+ return (0, import_react33.createElement)(
++ return (0, import_react34.createElement)(
+ "div",
+ { className: "bt-group-msg" },
-+ (0, import_react33.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name),
-+ (0, import_react33.createElement)(Bubbles, { texts: [exchangeText(entry.text)], roster, selfId: bot.id, actions, lead: (0, import_react33.createElement)("span", { className: "bt-lead" }, (0, import_react33.createElement)(BotMark, { bot, size: 22 })) })
++ (0, import_react34.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, bot.name),
++ (0, import_react34.createElement)(Bubbles, { texts: [exchangeText(entry.text)], roster, selfId: bot.id, actions, lead: (0, import_react34.createElement)("span", { className: "bt-lead" }, (0, import_react34.createElement)(BotMark, { bot, size: 22 })) })
+ );
+}
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_react33.useState)(null);
-+ const [leaving, setLeaving] = (0, import_react33.useState)(false);
++ const [entries2, setEntries] = (0, import_react34.useState)(null);
++ const [leaving, setLeaving] = (0, import_react34.useState)(false);
const close = () => {
if (leaving) return;
setLeaving(true);
@@ -13353,7 +13579,7 @@ diff --git a/client.js b/client.js
};
useEscape(close);
- (0, import_react21.useEffect)(() => {
-+ (0, import_react33.useEffect)(() => {
++ (0, import_react34.useEffect)(() => {
let stop = false;
let timer;
let seen = null;
@@ -13362,39 +13588,39 @@ diff --git a/client.js b/client.js
let lastDay = 0;
const paneLeft = usePaneLeft();
- return (0, import_react21.createElement)(
-+ return (0, import_react33.createElement)(
++ return (0, import_react34.createElement)(
"div",
{ className: "bt-pane bt-exchange", style: { left: paneLeft }, "data-leaving": leaving || void 0 },
- (0, import_react21.createElement)(
-+ (0, import_react33.createElement)(
++ (0, import_react34.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_react33.createElement)("div", { className: "bt-head" }, (0, import_react33.createElement)(
++ (0, import_react34.createElement)("div", { className: "bt-head" }, (0, import_react34.createElement)(
"span",
{ className: "bt-pill", style: { cursor: "default" } },
- left ? (0, import_react21.createElement)(BotMark, { bot: left, size: 22 }) : null,
-+ left ? (0, import_react33.createElement)(BotMark, { bot: left, size: 22 }) : null,
++ left ? (0, import_react34.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_react33.createElement)("span", { className: "bt-swap" }, "⇄"),
-+ right ? (0, import_react33.createElement)(BotMark, { bot: right, size: 22 }) : null,
++ (0, import_react34.createElement)("span", { className: "bt-swap" }, "⇄"),
++ right ? (0, import_react34.createElement)(BotMark, { bot: right, size: 22 }) : null,
right?.name ?? "?"
)),
- (0, import_react21.createElement)(
-+ (0, import_react33.createElement)(
++ (0, import_react34.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) => {
- const bot = roster.byId[entry.from] ?? { id: entry.from, name: t("You"), color: "gray" };
-+ entries2 === null ? (0, import_react33.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react33.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => {
++ entries2 === null ? (0, import_react34.createElement)("div", { className: "bt-hint" }, t("Loading…")) : entries2.length === 0 ? (0, import_react34.createElement)("div", { className: "bt-hint" }, t("No messages yet")) : entries2.map((entry) => {
const showDay = entry.time - lastDay > 30 * 60 * 1e3;
lastDay = entry.time;
- return (0, import_react21.createElement)(
- import_react21.Fragment,
-+ return (0, import_react33.createElement)(
-+ import_react33.Fragment,
++ return (0, import_react34.createElement)(
++ import_react34.Fragment,
{ key: entry.id },
- showDay ? (0, import_react21.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
- (0, import_react21.createElement)(
@@ -13403,13 +13629,13 @@ diff --git a/client.js b/client.js
- (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 })) })
- )
-+ showDay ? (0, import_react33.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
-+ (0, import_react33.createElement)(ExchangeLine, { entry, roster, actions })
++ showDay ? (0, import_react34.createElement)("time", { className: "bt-time" }, dayLabel(entry.time)) : null,
++ (0, import_react34.createElement)(ExchangeLine, { entry, roster, actions })
);
})
),
- (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_react33.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react33.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat")))
++ (0, import_react34.createElement)("div", { className: "bt-dialog-foot" }, (0, import_react34.createElement)("button", { type: "button", className: "bt-soft", onClick: close }, t("Close Chat")))
)
);
}
@@ -13417,7 +13643,7 @@ diff --git a/client.js b/client.js
// src/client/voice.js
-var import_react22 = require("react");
-var plainSpeech = (text) => stripHeader(text).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim();
-+var import_react34 = require("react");
++var import_react35 = require("react");
var VOICE_LABELS = {
listening: "Listening",
thinking: "Thinking",
@@ -13427,8 +13653,8 @@ diff --git a/client.js b/client.js
function useMicLevel(enabled) {
- const [level, setLevel] = (0, import_react22.useState)(0);
- (0, import_react22.useEffect)(() => {
-+ const [level, setLevel] = (0, import_react34.useState)(0);
-+ (0, import_react34.useEffect)(() => {
++ const [level, setLevel] = (0, import_react35.useState)(0);
++ (0, import_react35.useEffect)(() => {
if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0;
let stream = null;
let audio = null;
@@ -13453,19 +13679,19 @@ diff --git a/client.js b/client.js
- const [cycle, setCycle] = (0, import_react22.useState)(0);
- const waiting = (0, import_react22.useRef)(null);
- const reply = (0, import_react22.useRef)(lastReply);
-+ const [speech] = (0, import_react34.useState)(() => actions.speech());
-+ const [phase, setPhase] = (0, import_react34.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
-+ const [catalog, setCatalog] = (0, import_react34.useState)(null);
-+ const [heard, setHeard] = (0, import_react34.useState)("");
-+ const [said, setSaid] = (0, import_react34.useState)("");
-+ const [note, setNote] = (0, import_react34.useState)("");
-+ const [serviceLevel, setServiceLevel] = (0, import_react34.useState)(0);
-+ const [cycle, setCycle] = (0, import_react34.useState)(0);
-+ const waiting = (0, import_react34.useRef)(null);
-+ const reply = (0, import_react34.useRef)(lastReply);
++ const [speech] = (0, import_react35.useState)(() => actions.speech());
++ const [phase, setPhase] = (0, import_react35.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
++ const [catalog, setCatalog] = (0, import_react35.useState)(null);
++ const [heard, setHeard] = (0, import_react35.useState)("");
++ const [said, setSaid] = (0, import_react35.useState)("");
++ const [note, setNote] = (0, import_react35.useState)("");
++ const [serviceLevel, setServiceLevel] = (0, import_react35.useState)(0);
++ const [cycle, setCycle] = (0, import_react35.useState)(0);
++ const waiting = (0, import_react35.useRef)(null);
++ const reply = (0, import_react35.useRef)(lastReply);
reply.current = lastReply;
- const turn = (0, import_react22.useRef)(0);
-+ const turn = (0, import_react34.useRef)(0);
++ const turn = (0, import_react35.useRef)(0);
const paneLeft = usePaneLeft();
const close = () => {
turn.current += 1;
@@ -13492,13 +13718,13 @@ diff --git a/client.js b/client.js
+ submit(text2);
});
- (0, import_react22.useEffect)(() => {
-+ (0, import_react34.useEffect)(() => {
++ (0, import_react35.useEffect)(() => {
if (!note) return void 0;
const timer = setTimeout(() => setNote(""), 2600);
return () => clearTimeout(timer);
}, [note]);
- (0, import_react22.useEffect)(() => {
-+ (0, import_react34.useEffect)(() => {
++ (0, import_react35.useEffect)(() => {
if (!speech) return void 0;
let live = true;
speechCatalog(speech).then(({ catalog: next, usable }) => {
@@ -13507,18 +13733,18 @@ diff --git a/client.js b/client.js
}, [speech]);
const listen = phase === "listening";
- (0, import_react22.useEffect)(() => {
-+ (0, import_react34.useEffect)(() => {
++ (0, import_react35.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_react34.useEffect)(() => {
++ (0, import_react35.useEffect)(() => {
if (phase !== "listening") dictation.stop();
}, [phase]);
- (0, import_react22.useEffect)(() => {
-+ (0, import_react34.useEffect)(() => {
++ (0, import_react35.useEffect)(() => {
if (!speech || !catalog || !listen) return void 0;
const own = turn.current;
let capture = null;
@@ -13539,7 +13765,7 @@ diff --git a/client.js b/client.js
};
}, [speech, catalog, listen, cycle]);
- (0, import_react22.useEffect)(() => {
-+ (0, import_react34.useEffect)(() => {
++ (0, import_react35.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;
@@ -13556,7 +13782,7 @@ diff --git a/client.js b/client.js
window.speechSynthesis.speak(utterance);
}, [phase, running, lastReply]);
- (0, import_react22.useEffect)(() => () => window.speechSynthesis?.cancel(), []);
-+ (0, import_react34.useEffect)(() => () => window.speechSynthesis?.cancel(), []);
++ (0, import_react35.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";
@@ -13565,49 +13791,49 @@ diff --git a/client.js b/client.js
const caption = phase === "speaking" ? said : heard;
const shownNote = note || dictation.note;
- return (0, import_react22.createElement)(
-+ return (0, import_react34.createElement)(
++ return (0, import_react35.createElement)(
"div",
{ className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
- (0, import_react22.createElement)(
-+ (0, import_react34.createElement)(
++ (0, import_react35.createElement)(
"div",
{ className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase },
- (0, import_react22.createElement)(
-+ (0, import_react34.createElement)(
++ (0, import_react35.createElement)(
"div",
{ className: "bt-voice-orb", style: { "--bt-level": level } },
- (0, import_react22.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
- (0, import_react22.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
- bot ? (0, import_react22.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react22.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
-+ (0, import_react34.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
-+ (0, import_react34.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
-+ bot ? (0, import_react34.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react34.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
++ (0, import_react35.createElement)("span", { className: "bt-voice-ring", "aria-hidden": true }),
++ (0, import_react35.createElement)("span", { className: "bt-voice-ring bt-voice-ring-2", "aria-hidden": true }),
++ bot ? (0, import_react35.createElement)(BotMark, { bot, size: 112, state: markState, live: true, gaze: phase === "listening" }) : room2 ? (0, import_react35.createElement)(RoomAvatar, { room: room2, roster, size: 112 }) : null
),
- (0, import_react22.createElement)("div", { className: "bt-voice-name" }, name),
- (0, import_react22.createElement)(
-+ (0, import_react34.createElement)("div", { className: "bt-voice-name" }, name),
-+ (0, import_react34.createElement)(
++ (0, import_react35.createElement)("div", { className: "bt-voice-name" }, name),
++ (0, import_react35.createElement)(
"div",
{ className: "bt-voice-status", role: "status" },
- phase === "thinking" ? (0, import_react22.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react22.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
-+ phase === "thinking" ? (0, import_react34.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react34.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
++ phase === "thinking" ? (0, import_react35.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react35.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_react34.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
-+ openSetup ? (0, import_react34.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
++ (0, import_react35.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
++ openSetup ? (0, import_react35.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
close();
openSetup();
} }, t("Set up voice input")) : null,
- shownNote ? (0, import_react22.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
-+ shownNote ? (0, import_react34.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
++ shownNote ? (0, import_react35.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
),
- (0, import_react22.createElement)(
-+ (0, import_react34.createElement)(
++ (0, import_react35.createElement)(
"div",
{ className: "bt-voice-bar" },
- (0, import_react22.createElement)("button", {
-+ (0, import_react34.createElement)("button", {
++ (0, import_react35.createElement)("button", {
type: "button",
className: "bt-voice-btn",
"aria-pressed": phase === "paused",
@@ -13616,8 +13842,8 @@ diff --git a/client.js b/client.js
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_react34.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react34.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
-+ (0, import_react34.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react34.createElement)(CloseIcon))
++ }, (0, import_react35.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react35.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
++ (0, import_react35.createElement)("button", { type: "button", className: "bt-voice-btn bt-voice-end", "aria-label": t("End voice chat"), onClick: close }, (0, import_react35.createElement)(CloseIcon))
)
);
}
@@ -13633,7 +13859,7 @@ diff --git a/client.js b/client.js
- const admin = roster.byId[room2.admin];
- return [t("{count} members", { count: room2.members.length }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · ");
+// src/client/workflows.js
-+var import_react35 = require("react");
++var import_react36 = require("react");
+var READ_MS = 8e3;
+var WORKFLOW_PLUGIN_HINT = "The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow.";
+var COLORS = { Planner: "#8b5cf6", Writer: "#f0509a", Coder: "#1fbf6a", Reviewer: "#f5a524", Tester: "#14b8a6", Deployer: "#2f7cf6" };
@@ -13641,24 +13867,7 @@ diff --git a/client.js b/client.js
+ const code = failure && typeof failure === "object" ? String(failure.code ?? "") : "";
+ const message = typeof failure === "string" ? failure : String(failure?.message ?? "");
+ return code === "bot/not-found" || /Unknown FlyBot endpoint workflow\//i.test(message);
- }
--function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
-- const [adding, setAdding] = (0, import_react23.useState)(false);
-- const [confirming, setConfirming] = (0, import_react23.useState)(false);
-- (0, import_react23.useEffect)(() => {
-- if (!confirming) return void 0;
-- const timer = setTimeout(() => setConfirming(false), 4e3);
-- return () => clearTimeout(timer);
-- }, [confirming]);
-- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean);
-- const outside = roster.bots.filter((bot) => !room2.members.includes(bot.id));
-- const admin = roster.byId[room2.admin];
-- const mode = shownMode(room2, roster);
-- const modeHint = ROOM_MODES.find(([id]) => id === mode)?.[2] ?? "";
-- const update = (patch) => run(actions.updateRoom(room2.id, patch));
-- const openBot = (id) => {
-- actions.closeSettings();
-- actions.openSession(id);
++}
+function workflowCallError(failure, fallback = "Could not read workflows") {
+ if (workflowCallMissing(failure)) return WORKFLOW_PLUGIN_HINT;
+ if (typeof failure === "string" && failure.trim()) return failure.trim();
@@ -13677,7 +13886,7 @@ diff --git a/client.js b/client.js
+ if (templates.length === 0 && workflows.length === 0) return { phase: "empty", error: "", pack };
+ return { phase: "ready", error: "", pack };
+}
-+function readWorkflows(call4, { timeoutMs = READ_MS } = {}) {
++function readWorkflows(call5, { timeoutMs = READ_MS } = {}) {
+ return new Promise((resolve) => {
+ let settled = false;
+ const finish = (value) => {
@@ -13687,9 +13896,26 @@ diff --git a/client.js b/client.js
+ resolve(value);
+ };
+ const timer = setTimeout(() => finish(settleWorkflowRead(null)), timeoutMs);
-+ Promise.resolve().then(() => call4("workflow/state")).then((value) => finish(settleWorkflowRead(value))).catch((failure) => finish({ phase: "error", error: workflowCallError(failure), pack: null }));
++ Promise.resolve().then(() => call5("workflow/state")).then((value) => finish(settleWorkflowRead(value))).catch((failure) => finish({ phase: "error", error: workflowCallError(failure), pack: null }));
+ });
-+}
+ }
+-function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
+- const [adding, setAdding] = (0, import_react23.useState)(false);
+- const [confirming, setConfirming] = (0, import_react23.useState)(false);
+- (0, import_react23.useEffect)(() => {
+- if (!confirming) return void 0;
+- const timer = setTimeout(() => setConfirming(false), 4e3);
+- return () => clearTimeout(timer);
+- }, [confirming]);
+- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean);
+- const outside = roster.bots.filter((bot) => !room2.members.includes(bot.id));
+- const admin = roster.byId[room2.admin];
+- const mode = shownMode(room2, roster);
+- const modeHint = ROOM_MODES.find(([id]) => id === mode)?.[2] ?? "";
+- const update = (patch) => run(actions.updateRoom(room2.id, patch));
+- const openBot = (id) => {
+- actions.closeSettings();
+- actions.openSession(id);
+function moveNode(nodes, id, x, y) {
+ return nodes.map((node) => node.id === id ? { ...node, x, y } : node);
+}
@@ -13779,9 +14005,9 @@ diff --git a/client.js b/client.js
+ return Number.isFinite(width) && width > 0 ? width : 0;
+}
+function GraphCanvas({ graph, editing, roster, onCommit, width: knownWidth = 0 }) {
-+ const gesture = (0, import_react35.useRef)(null);
-+ const canvasRef = (0, import_react35.useRef)(null);
-+ const [measuredWidth, setCanvasWidth] = (0, import_react35.useState)(0);
++ const gesture = (0, import_react36.useRef)(null);
++ const canvasRef = (0, import_react36.useRef)(null);
++ const [measuredWidth, setCanvasWidth] = (0, import_react36.useState)(0);
+ const nodes = graph.nodes ?? [];
+ const edges = graph.edges ?? [];
+ const status = graph.run?.nodeStatus ?? null;
@@ -13790,7 +14016,7 @@ diff --git a/client.js b/client.js
+ const canvasWidth = measuredWidth || knownWidth;
+ const { scale, pad: pad2 } = fitLayout(nodes, canvasWidth);
+ const drawnHeight = Math.round(height * scale);
-+ (0, import_react35.useEffect)(() => {
++ (0, import_react36.useEffect)(() => {
+ const el = canvasRef.current;
+ const measure = () => {
+ const width = measureCanvasWidth(el);
@@ -13823,19 +14049,19 @@ diff --git a/client.js b/client.js
- });
};
- return (0, import_react23.createElement)(
-+ return (0, import_react35.createElement)(
++ return (0, import_react36.createElement)(
"div",
- { className: "bt-bs" },
- (0, import_react23.createElement)(
+ { className: "bt-wf-canvas", ref: canvasRef, onPointerUp: end },
-+ editing ? (0, import_react35.createElement)(
++ editing ? (0, import_react36.createElement)(
"div",
- { className: "bt-bs-hero" },
- (0, import_react23.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
- (0, import_react23.createElement)(
+ { className: "bt-wf-palette" },
-+ (0, import_react35.createElement)("span", { className: "bt-wf-hint" }, t("Drag a Bot here")),
-+ locals.map((bot) => (0, import_react35.createElement)("button", {
++ (0, import_react36.createElement)("span", { className: "bt-wf-hint" }, t("Drag a Bot here")),
++ locals.map((bot) => (0, import_react36.createElement)("button", {
+ key: bot.id,
+ type: "button",
+ "data-bot": bot.id,
@@ -13848,11 +14074,11 @@ diff --git a/client.js b/client.js
+ // A node's system prompt is the run Bot's prompt (plugins/workflow
+ // nodeConfig). The sidebar panel has this field, so the Settings editor
+ // writes the same graph instead of only drawing it (#1393).
-+ editing ? (0, import_react35.createElement)("div", { className: "bt-wf-prompts" }, nodes.map((node) => (0, import_react35.createElement)(
++ editing ? (0, import_react36.createElement)("div", { className: "bt-wf-prompts" }, nodes.map((node) => (0, import_react36.createElement)(
+ "label",
+ { key: node.id, className: "bt-wf-prompt" },
-+ (0, import_react35.createElement)("span", null, `${node.name} · ${t("System prompt")}`),
-+ (0, import_react35.createElement)("textarea", {
++ (0, import_react36.createElement)("span", null, `${node.name} · ${t("System prompt")}`),
++ (0, import_react36.createElement)("textarea", {
+ "data-prompt": node.id,
+ rows: 3,
+ defaultValue: node.prompt || "",
@@ -13864,19 +14090,19 @@ diff --git a/client.js b/client.js
+ }
+ })
+ ))) : null,
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
+ "div",
+ { className: "bt-wf-stage", style: pad2 > 0 ? { height: drawnHeight, paddingLeft: pad2 } : { height: drawnHeight } },
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
"div",
- { className: "bt-bs-who" },
- (0, import_react23.createElement)("span", { className: "bt-bs-name" }, room2.name),
- (0, import_react23.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
+ { className: "bt-wf-fit", style: { height, transform: `scale(${scale})` } },
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
+ "div",
+ { className: "bt-wf-plane", "data-plane": "1", style: { height, "--bt-wf-wire-span": `${WIRE_SPAN}px` } },
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
+ "svg",
+ {
+ className: "bt-wf-wires",
@@ -13885,10 +14111,10 @@ diff --git a/client.js b/client.js
+ height,
+ viewBox: `0 0 ${WIRE_SPAN * 2} ${height}`
+ },
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
+ "defs",
+ null,
-+ (0, import_react35.createElement)("marker", {
++ (0, import_react36.createElement)("marker", {
+ id: "bt-wf-arrow",
+ viewBox: "0 0 10 10",
+ refX: "8",
@@ -13896,13 +14122,13 @@ diff --git a/client.js b/client.js
+ markerWidth: "6",
+ markerHeight: "6",
+ orient: "auto"
-+ }, (0, import_react35.createElement)("path", { d: "M0 0 10 5 0 10z" }))
++ }, (0, import_react36.createElement)("path", { d: "M0 0 10 5 0 10z" }))
+ ),
+ edges.map((edge) => {
+ const from = nodes.find((node) => node.id === edge.from);
+ const to = nodes.find((node) => node.id === edge.to);
+ if (!from || !to) return null;
-+ return (0, import_react35.createElement)("path", { key: `${edge.from}>${edge.to}`, d: wirePath(from, to), markerEnd: "url(#bt-wf-arrow)" });
++ return (0, import_react36.createElement)("path", { key: `${edge.from}>${edge.to}`, d: wirePath(from, to), markerEnd: "url(#bt-wf-arrow)" });
+ })
+ ),
+ (graph.layers ?? []).filter((layer) => layer.length > 1).map((layer) => {
@@ -13912,15 +14138,15 @@ diff --git a/client.js b/client.js
+ const right = Math.max(...members.map((node) => node.x ?? 0)) + FRAME_SIDE;
+ const top = Math.min(...members.map((node) => node.y ?? 0)) - FRAME_LABEL;
+ const bottom = Math.max(...members.map((node) => node.y ?? 0)) + CARD_H + FRAME_FOOT;
-+ return (0, import_react35.createElement)("div", {
++ return (0, import_react36.createElement)("div", {
+ key: layer.join("+"),
+ className: "bt-wf-par",
+ style: { left, top, width: right - left, height: bottom - top }
-+ }, (0, import_react35.createElement)("div", { className: "bt-wf-plabel" }, t("Parallel branch, then join")));
++ }, (0, import_react36.createElement)("div", { className: "bt-wf-plabel" }, t("Parallel branch, then join")));
+ }),
+ nodes.map((node) => {
+ const state = status ? status[node.id] ?? "queued" : "idle";
-+ return (0, import_react35.createElement)(
++ return (0, import_react36.createElement)(
+ "div",
+ {
+ key: node.id,
@@ -13932,7 +14158,7 @@ diff --git a/client.js b/client.js
+ gesture.current = { kind: "move", id: node.id, ox: event.clientX, oy: event.clientY, x: node.x ?? 0, y: node.y ?? 0 };
+ }
+ },
-+ editing ? (0, import_react35.createElement)("button", {
++ editing ? (0, import_react36.createElement)("button", {
+ type: "button",
+ className: "bt-wf-port",
+ "data-port": "out",
@@ -13943,7 +14169,7 @@ diff --git a/client.js b/client.js
+ gesture.current = { kind: "wire", from: node.id };
+ }
+ }) : null,
-+ editing ? (0, import_react35.createElement)("button", {
++ editing ? (0, import_react36.createElement)("button", {
+ type: "button",
+ className: "bt-wf-remove",
+ "data-remove": node.id,
@@ -13954,13 +14180,13 @@ diff --git a/client.js b/client.js
+ onCommit?.(next);
+ }
+ }, "×") : null,
-+ (0, import_react35.createElement)("span", { className: `bt-wf-chip ${state}` }, statusLabel2(state)),
-+ (0, import_react35.createElement)("span", { className: "bt-wf-av", style: { background: COLORS[node.name] ?? "#4d6bfe" } }, (node.name ?? "?").slice(0, 1)),
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)("span", { className: `bt-wf-chip ${state}` }, statusLabel2(state)),
++ (0, import_react36.createElement)("span", { className: "bt-wf-av", style: { background: COLORS[node.name] ?? "#4d6bfe" } }, (node.name ?? "?").slice(0, 1)),
++ (0, import_react36.createElement)(
+ "span",
+ { className: "bt-wf-copy" },
-+ (0, import_react35.createElement)("span", { className: "bt-wf-name" }, node.name),
-+ (0, import_react35.createElement)("span", { className: "bt-wf-role" }, node.role ?? "")
++ (0, import_react36.createElement)("span", { className: "bt-wf-name" }, node.name),
++ (0, import_react36.createElement)("span", { className: "bt-wf-role" }, node.role ?? "")
+ )
+ );
+ })
@@ -13970,7 +14196,7 @@ diff --git a/client.js b/client.js
+ );
+}
+function Row2({ item, source, onOpen, onRename, onDelete }) {
-+ return (0, import_react35.createElement)(
++ return (0, import_react36.createElement)(
+ "button",
+ {
+ type: "button",
@@ -13979,11 +14205,11 @@ diff --git a/client.js b/client.js
+ "data-source": source,
+ onClick: () => onOpen?.(item)
+ },
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
+ "span",
+ { className: "bt-set-copy" },
-+ (0, import_react35.createElement)("span", null, item.name),
-+ (0, import_react35.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "")
++ (0, import_react36.createElement)("span", null, item.name),
++ (0, import_react36.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "")
),
- (0, import_react23.createElement)(
- Card,
@@ -13992,10 +14218,10 @@ diff --git a/client.js b/client.js
- Row,
- { label: t("Name"), hint: t("Leave it empty to name the group after its members.") },
- (0, import_react23.createElement)(InlineText, { label: t("Group name"), value: room2.named ? room2.name : "", placeholder: t("Name after members"), maxLength: 60, onSave: (name) => update({ name }) })
-+ source === "own" ? (0, import_react35.createElement)(
++ source === "own" ? (0, import_react36.createElement)(
+ "span",
+ { className: "bt-set-actions" },
-+ (0, import_react35.createElement)("button", {
++ (0, import_react36.createElement)("button", {
+ type: "button",
+ className: "bt-set-action",
+ "aria-label": t("Rename"),
@@ -14005,7 +14231,7 @@ diff --git a/client.js b/client.js
+ onRename?.(item);
+ }
+ }, "✎"),
-+ (0, import_react35.createElement)("button", {
++ (0, import_react36.createElement)("button", {
+ type: "button",
+ className: "bt-set-action",
+ "aria-label": t("Delete"),
@@ -14016,7 +14242,7 @@ diff --git a/client.js b/client.js
+ }
+ }, "×")
+ ) : null,
-+ (0, import_react35.createElement)("span", { className: "bt-chevron", "aria-hidden": true }, "›")
++ (0, import_react36.createElement)("span", { className: "bt-chevron", "aria-hidden": true }, "›")
+ );
+}
+function alertCopy(error) {
@@ -14029,33 +14255,33 @@ diff --git a/client.js b/client.js
+ const templateIds = new Set(templates.map((item) => item.id));
+ const yours = (pack?.workflows ?? []).filter((item) => !templateIds.has(item.id));
+ const showUse = opened?.source === "factory" || opened?.fromFactory !== void 0;
-+ return (0, import_react35.createElement)(
++ return (0, import_react36.createElement)(
+ "div",
+ { className: "bt-wf-page" },
-+ (0, import_react35.createElement)("p", { className: "bt-wf-hint" }, t("A workflow is a group of your Bots. Drag the flowchart to change who hands work to whom.")),
-+ error ? (0, import_react35.createElement)(
++ (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("A workflow is a group of your Bots. Drag the flowchart to change who hands work to whom.")),
++ error ? (0, import_react36.createElement)(
+ "div",
+ { className: "bt-error", role: "alert" },
+ alertCopy(error),
-+ phase === "error" ? (0, import_react35.createElement)("button", { type: "button", onClick: () => onRetry?.() }, t("Retry")) : null
++ phase === "error" ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onRetry?.() }, t("Retry")) : null
+ ) : null,
-+ phase === "loading" ? (0, import_react35.createElement)("p", { className: "bt-wf-status", role: "status" }, t("Reading workflows…")) : null,
-+ phase === "empty" ? (0, import_react35.createElement)("p", { className: "bt-wf-status" }, t("No workflows yet.")) : null,
-+ phase === "ready" && !graph ? (0, import_react35.createElement)(
++ phase === "loading" ? (0, import_react36.createElement)("p", { className: "bt-wf-status", role: "status" }, t("Reading workflows…")) : null,
++ phase === "empty" ? (0, import_react36.createElement)("p", { className: "bt-wf-status" }, t("No workflows yet.")) : null,
++ phase === "ready" && !graph ? (0, import_react36.createElement)(
+ "div",
+ { className: "bt-wf-lists" },
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
+ "section",
+ { className: "bt-set-section" },
-+ (0, import_react35.createElement)("h3", null, t("Factory presets")),
-+ (0, import_react35.createElement)("p", { className: "bt-wf-hint" }, t("Ready-made pipelines. Use one to start your own.")),
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)("h3", null, t("Factory presets")),
++ (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("Ready-made pipelines. Use one to start your own.")),
++ (0, import_react36.createElement)(
+ "div",
+ { className: "bt-set-card" },
-+ templates.length === 0 ? (0, import_react35.createElement)("div", { className: "bt-set-row" }, (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("No workflows yet."))) : templates.map((item) => {
++ templates.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No workflows yet."))) : templates.map((item) => {
+ const live = (pack.workflows ?? []).find((workflow) => workflow.id === item.id);
+ const row = live ? { ...item, name: live.name, preview: live.preview || chainOf(live) || chainOf(item) } : item;
-+ return (0, import_react35.createElement)(Row2, { key: item.id, item: row, source: "factory", onOpen: () => onOpen?.(item, "factory") });
++ return (0, import_react36.createElement)(Row2, { key: item.id, item: row, source: "factory", onOpen: () => onOpen?.(item, "factory") });
+ })
+ )
),
@@ -14063,15 +14289,15 @@ diff --git a/client.js b/client.js
- Row,
- { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true },
- (0, import_react23.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) })
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
+ "section",
+ { className: "bt-set-section" },
-+ (0, import_react35.createElement)("h3", null, t("Yours")),
-+ (0, import_react35.createElement)("p", { className: "bt-wf-hint" }, t("Workflows saved on this machine.")),
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)("h3", null, t("Yours")),
++ (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("Workflows saved on this machine.")),
++ (0, import_react36.createElement)(
+ "div",
+ { className: "bt-set-card" },
-+ yours.length === 0 ? (0, import_react35.createElement)("div", { className: "bt-set-row" }, (0, import_react35.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react35.createElement)(Row2, { key: item.id, item, source: "own", onOpen: () => onOpen?.(item, "own"), onRename, onDelete }))
++ yours.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react36.createElement)(Row2, { key: item.id, item, source: "own", onOpen: () => onOpen?.(item, "own"), onRename, onDelete }))
+ )
)
- ),
@@ -14080,10 +14306,10 @@ diff --git a/client.js b/client.js
- { title: t("Who answers") },
- (0, import_react23.createElement)(
+ ) : null,
-+ graph ? (0, import_react35.createElement)(
++ graph ? (0, import_react36.createElement)(
+ "div",
+ { className: "bt-wf-editor" },
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
"div",
- { className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react23.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react23.createElement)("button", {
@@ -14104,13 +14330,13 @@ diff --git a/client.js b/client.js
- { title: t("Working folder") },
- (0, import_react23.createElement)(
+ { className: "bt-wf-actions" },
-+ (0, import_react35.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")),
-+ showUse ? (0, import_react35.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null,
-+ opened?.source === "own" ? (0, import_react35.createElement)("button", { type: "button", onClick: () => onEditing?.(!editing) }, editing ? t("Finish editing") : t("Edit")) : null
++ (0, import_react36.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")),
++ showUse ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null,
++ opened?.source === "own" ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onEditing?.(!editing) }, editing ? t("Finish editing") : t("Edit")) : null
+ ),
-+ (0, import_react35.createElement)("h3", { className: "bt-wf-title" }, graph.name),
-+ graph.description ? (0, import_react35.createElement)("p", { className: "bt-wf-hint" }, graph.description) : null,
-+ (graph.nodes ?? []).length === 0 ? (0, import_react35.createElement)("p", { className: "bt-wf-status" }, t("This workflow has no flowchart yet.")) : (0, import_react35.createElement)(GraphCanvas, {
++ (0, import_react36.createElement)("h3", { className: "bt-wf-title" }, graph.name),
++ graph.description ? (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, graph.description) : null,
++ (graph.nodes ?? []).length === 0 ? (0, import_react36.createElement)("p", { className: "bt-wf-status" }, t("This workflow has no flowchart yet.")) : (0, import_react36.createElement)(GraphCanvas, {
+ graph,
+ roster,
+ onCommit,
@@ -14121,31 +14347,28 @@ diff --git a/client.js b/client.js
+ );
+}
+function RenameModal({ workflow, onClose, onConfirm }) {
-+ const [name, setName] = (0, import_react35.useState)(workflow?.name ?? "");
-+ return (0, import_react35.createElement)(
++ const [name, setName] = (0, import_react36.useState)(workflow?.name ?? "");
++ return (0, import_react36.createElement)(
+ "div",
+ { className: "bt-modal-mask", onClick: (event) => {
+ if (event.target === event.currentTarget) onClose?.();
+ } },
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
+ "div",
+ { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Rename workflow") },
-+ (0, import_react35.createElement)("h3", null, t("Rename workflow")),
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)("h3", null, t("Rename workflow")),
++ (0, import_react36.createElement)(
+ "label",
+ { className: "bt-field" },
+ t("Name"),
-+ (0, import_react35.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true })
++ (0, import_react36.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true })
+ ),
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)(
"div",
- { className: "bt-set-row bt-bs-row" },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Folder"))),
- (0, import_react23.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) })
-+ { className: "bt-modal-actions" },
-+ (0, import_react35.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")),
-+ (0, import_react35.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename"))
- )
+- )
- ),
- (0, import_react23.createElement)(
- Card,
@@ -14205,7 +14428,10 @@ diff --git a/client.js b/client.js
- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team."))
- ),
- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon))
-- )
++ { className: "bt-modal-actions" },
++ (0, import_react36.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")),
++ (0, import_react36.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename"))
+ )
)
);
}
@@ -14239,28 +14465,29 @@ diff --git a/client.js b/client.js
- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null);
- return (0, import_react23.createElement)(
+function DeleteModal({ workflow, onClose, onConfirm }) {
-+ return (0, import_react35.createElement)(
++ return (0, import_react36.createElement)(
"div",
- { className: "bt-set-card bt-ng" },
- (0, import_react23.createElement)(
-+ { className: "bt-modal-mask", onClick: (event) => {
-+ if (event.target === event.currentTarget) onClose?.();
-+ } },
-+ (0, import_react35.createElement)(
- "div",
+- "div",
- { className: "bt-set-row bt-bs-row bt-bs-stack" },
- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))),
- (0, import_react23.createElement)(
++ { className: "bt-modal-mask", onClick: (event) => {
++ if (event.target === event.currentTarget) onClose?.();
++ } },
++ (0, import_react36.createElement)(
++ "div",
+ { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") },
-+ (0, import_react35.createElement)("h3", null, t("Delete workflow")),
-+ (0, import_react35.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })),
-+ (0, import_react35.createElement)(
++ (0, import_react36.createElement)("h3", null, t("Delete workflow")),
++ (0, import_react36.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })),
++ (0, import_react36.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"))
+ { className: "bt-modal-actions" },
-+ (0, import_react35.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")),
-+ (0, import_react35.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete"))
++ (0, import_react36.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")),
++ (0, import_react36.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete"))
)
- ),
- admin === null ? null : (0, import_react23.createElement)(
@@ -14308,21 +14535,21 @@ 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}}
-`;
-+function WorkflowsPage({ call: call4, roster }) {
-+ const [phase, setPhase] = (0, import_react35.useState)("loading");
-+ const [pack, setPack] = (0, import_react35.useState)(null);
-+ const [error, setError] = (0, import_react35.useState)("");
-+ const [opened, setOpened] = (0, import_react35.useState)(null);
-+ const [editing, setEditing] = (0, import_react35.useState)(false);
-+ const [renaming, setRenaming] = (0, import_react35.useState)(null);
-+ const [deleting, setDeleting] = (0, import_react35.useState)(null);
-+ const invoke = typeof call4 === "function" ? call4 : () => Promise.reject(new Error("Could not read workflows"));
++function WorkflowsPage({ call: call5, roster }) {
++ const [phase, setPhase] = (0, import_react36.useState)("loading");
++ const [pack, setPack] = (0, import_react36.useState)(null);
++ const [error, setError] = (0, import_react36.useState)("");
++ const [opened, setOpened] = (0, import_react36.useState)(null);
++ const [editing, setEditing] = (0, import_react36.useState)(false);
++ const [renaming, setRenaming] = (0, import_react36.useState)(null);
++ const [deleting, setDeleting] = (0, import_react36.useState)(null);
++ const invoke = typeof call5 === "function" ? call5 : () => Promise.reject(new Error("Could not read workflows"));
+ const apply2 = (settled) => {
+ setPhase(settled.phase);
+ setPack(settled.pack);
+ setError(settled.error);
+ };
-+ (0, import_react35.useEffect)(() => {
++ (0, import_react36.useEffect)(() => {
+ let gone = false;
+ readWorkflows(invoke).then((settled) => {
+ if (!gone) apply2(settled);
@@ -14422,10 +14649,10 @@ diff --git a/client.js b/client.js
+ setError(workflowCallError(failure, t("Could not save this workflow")));
+ });
+ };
-+ return (0, import_react35.createElement)(
++ return (0, import_react36.createElement)(
+ "div",
+ null,
-+ (0, import_react35.createElement)(WorkflowsView, {
++ (0, import_react36.createElement)(WorkflowsView, {
+ phase,
+ pack,
+ error,
@@ -14444,12 +14671,12 @@ diff --git a/client.js b/client.js
+ onRename: rename,
+ onDelete: deleteWorkflow
+ }),
-+ renaming ? (0, import_react35.createElement)(RenameModal, {
++ renaming ? (0, import_react36.createElement)(RenameModal, {
+ workflow: renaming,
+ onClose: () => setRenaming(null),
+ onConfirm: (name) => confirmRename(renaming.id, name)
+ }) : null,
-+ deleting ? (0, import_react35.createElement)(DeleteModal, {
++ deleting ? (0, import_react36.createElement)(DeleteModal, {
+ workflow: deleting,
+ onClose: () => setDeleting(null),
+ onConfirm: () => confirmDelete(deleting.id)
@@ -14482,7 +14709,7 @@ diff --git a/client.js b/client.js
// src/client/usage-page.js
-var import_react24 = require("react");
-+var import_react36 = require("react");
++var import_react37 = require("react");
var HEAT_WEEKS = 53;
var TOP_SERIES = 7;
var RING_R = 42;
@@ -14498,9 +14725,9 @@ diff --git a/client.js b/client.js
- const [shown, setShown] = (0, import_react24.useState)(0);
- const from = (0, import_react24.useRef)(0);
- (0, import_react24.useEffect)(() => {
-+ const [shown, setShown] = (0, import_react36.useState)(0);
-+ const from = (0, import_react36.useRef)(0);
-+ (0, import_react36.useEffect)(() => {
++ const [shown, setShown] = (0, import_react37.useState)(0);
++ const from = (0, import_react37.useRef)(0);
++ (0, import_react37.useEffect)(() => {
if (reducedMotion() || typeof requestAnimationFrame !== "function") {
from.current = value;
setShown(value);
@@ -14509,10 +14736,10 @@ diff --git a/client.js b/client.js
return shown;
}
-var glyphBox = (size, icon2) => (0, import_react24.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react24.createElement)(icon2));
-+var glyphBox = (size, icon3) => (0, import_react36.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react36.createElement)(icon3));
++var glyphBox = (size, icon3) => (0, import_react37.createElement)("span", { className: "bt-us-glyph", style: { width: size, height: size } }, (0, import_react37.createElement)(icon3));
function ModelGlyph({ name, paint, size }) {
- return (0, import_react24.createElement)("span", {
-+ return (0, import_react36.createElement)("span", {
++ return (0, import_react37.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))` }
@@ -14521,7 +14748,7 @@ diff --git a/client.js b/client.js
paint: paintCss(colorOf(bot)),
stops: GRADIENTS[colorOf(bot)],
- glyph: (size) => (0, import_react24.createElement)(BotAvatar, { bot, size, badge: false, live: false })
-+ glyph: (size) => (0, import_react36.createElement)(BotAvatar, { bot, size, badge: false, live: false })
++ glyph: (size) => (0, import_react37.createElement)(BotAvatar, { bot, size, badge: false, live: false })
};
}
if (owner.kind === "room") {
@@ -14530,7 +14757,7 @@ diff --git a/client.js b/client.js
sub: owner.deleted ? t("Deleted") : t("Group chat"),
paint: "var(--bt-us-rest)",
- glyph: (size) => room2 ? (0, import_react24.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
-+ glyph: (size) => room2 ? (0, import_react36.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
++ glyph: (size) => room2 ? (0, import_react37.createElement)(RoomAvatar, { room: room2, roster, size }) : glyphBox(size, ChatsIcon)
};
}
if (owner.kind === "agent") {
@@ -14543,13 +14770,13 @@ diff --git a/client.js b/client.js
const glide = useGlide(`${value}
${names}`, { box: boxRef });
- return (0, import_react24.createElement)(
-+ return (0, import_react36.createElement)(
++ return (0, import_react37.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_react36.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ options.map(([id, text2, icon3]) => (0, import_react36.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react36.createElement)(icon3) : null, text2))
++ (0, import_react37.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ options.map(([id, text2, icon3]) => (0, import_react37.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react37.createElement)(icon3) : null, text2))
);
}
function Stats({ range, total, previous }) {
@@ -14557,25 +14784,25 @@ diff --git a/client.js b/client.js
const change = previous > 0 ? Math.round((total.total - previous) / previous * 100) : null;
- const cell = (label, value) => (0, import_react24.createElement)("div", { className: "bt-us-stat" }, (0, import_react24.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react24.createElement)("span", { className: "bt-us-stat-value" }, value));
- return (0, import_react24.createElement)(
-+ const cell = (label, value) => (0, import_react36.createElement)("div", { className: "bt-us-stat" }, (0, import_react36.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react36.createElement)("span", { className: "bt-us-stat-value" }, value));
-+ return (0, import_react36.createElement)(
++ const cell = (label, value) => (0, import_react37.createElement)("div", { className: "bt-us-stat" }, (0, import_react37.createElement)("span", { className: "bt-us-stat-label" }, label), (0, import_react37.createElement)("span", { className: "bt-us-stat-value" }, value));
++ return (0, import_react37.createElement)(
"section",
{ className: "bt-us-stats", style: { "--i": 0 } },
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)("span", { className: "bt-us-stat-label" }, lastText(range)),
++ (0, import_react37.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_react36.createElement)("small", null, t("tokens")),
-+ change === null ? null : (0, import_react36.createElement)(
++ (0, import_react37.createElement)("small", null, t("tokens")),
++ change === null ? null : (0, import_react37.createElement)(
"span",
{ className: "bt-us-delta", title: compareText(range) },
`${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%`
@@ -14584,7 +14811,7 @@ diff --git a/client.js b/client.js
}
function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) {
- const [hover, setHover] = (0, import_react24.useState)(null);
-+ const [hover, setHover] = (0, import_react36.useState)(null);
++ const [hover, setHover] = (0, import_react37.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]));
@@ -14592,77 +14819,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_react36.createElement)(
++ return (0, import_react37.createElement)(
"section",
{ className: "bt-us-card bt-us-chart", style: { "--i": 1 } },
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("h3", null, t("Tokens over time"), (0, import_react36.createElement)("span", null, unitLabel(range.unit))),
-+ onBy ? (0, import_react36.createElement)(Tabs, { label: t("Group usage by"), value: by, onChange: onBy, options: [["bot", t("By Bot")], ["model", t("By model")]] }) : null
++ (0, import_react37.createElement)("h3", null, t("Tokens over time"), (0, import_react37.createElement)("span", null, unitLabel(range.unit))),
++ onBy ? (0, import_react37.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_react36.createElement)(
++ (0, import_react37.createElement)(
"div",
{ className: "bt-us-chart-body" },
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react36.createElement)("span", null, compact(top * level)))),
-+ (0, import_react36.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react36.createElement)("span", null, "0")),
-+ (0, import_react36.createElement)(
++ [1, 0.5].map((level) => (0, import_react37.createElement)("div", { key: level, className: "bt-us-grid", style: { bottom: `${level * 100}%` } }, (0, import_react37.createElement)("span", null, compact(top * level)))),
++ (0, import_react37.createElement)("div", { className: "bt-us-grid bt-us-base", style: { bottom: 0 } }, (0, import_react37.createElement)("span", null, "0")),
++ (0, import_react37.createElement)(
"div",
{ className: "bt-us-cols" },
- columns.map((column, at) => (0, import_react24.createElement)("div", {
-+ columns.map((column, at) => (0, import_react36.createElement)("div", {
++ columns.map((column, at) => (0, import_react37.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_react36.createElement)(
++ }, column.total > 0 ? (0, import_react37.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_react36.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null)
-+ ) : (0, import_react36.createElement)("div", { className: "bt-us-stub" })))
++ column.parts.map(([key, value]) => value > 0 ? (0, import_react37.createElement)("i", { key, style: { flexGrow: value, background: fillOf.get(key) } }) : null)
++ ) : (0, import_react37.createElement)("div", { className: "bt-us-stub" })))
),
- tip ? (0, import_react24.createElement)(
-+ tip ? (0, import_react36.createElement)(
++ tip ? (0, import_react37.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_react36.createElement)("div", { className: "bt-us-tip-title" }, tip.title),
-+ (0, import_react36.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_react36.createElement)(
++ (0, import_react37.createElement)("div", { className: "bt-us-tip-title" }, tip.title),
++ (0, import_react37.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_react37.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_react36.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }),
-+ (0, import_react36.createElement)("span", null, tipOf.get(key)?.name),
-+ (0, import_react36.createElement)("b", null, compact(value))
++ (0, import_react37.createElement)("i", { className: "bt-us-dot", style: { background: tipOf.get(key)?.fill } }),
++ (0, import_react37.createElement)("span", null, tipOf.get(key)?.name),
++ (0, import_react37.createElement)("b", null, compact(value))
))
) : null
),
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)(
++ columns.map((column, at) => (0, import_react37.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 : ""
@@ -14670,16 +14897,16 @@ 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_react36.createElement)(
++ series.length > 1 || series[0]?.pick ? (0, import_react37.createElement)(
"div",
{ className: "bt-us-legend" },
- series.map((entry) => (0, import_react24.createElement)(entry.pick ? "button" : "span", {
-+ series.map((entry) => (0, import_react36.createElement)(entry.pick ? "button" : "span", {
++ series.map((entry) => (0, import_react37.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_react36.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name))
++ }, (0, import_react37.createElement)("i", { className: "bt-us-dot", style: { background: entry.fill } }), entry.name))
) : null
);
}
@@ -14687,7 +14914,7 @@ diff --git a/client.js b/client.js
+function Row3({ kind, glyph, name, sub, sum, whole, fill: fill3, 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_react36.createElement)(
++ return (0, import_react37.createElement)(
onClick ? "button" : "div",
{
className: "bt-us-row",
@@ -14697,77 +14924,77 @@ diff --git a/client.js b/client.js
},
- glyph ? (0, import_react24.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
- (0, import_react24.createElement)(
-+ glyph ? (0, import_react36.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
-+ (0, import_react36.createElement)(
++ glyph ? (0, import_react37.createElement)("span", { className: "bt-us-row-glyph" }, glyph) : null,
++ (0, import_react37.createElement)(
"span",
{ className: "bt-us-row-body" },
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("span", { className: "bt-us-row-name" }, name),
-+ sub ? (0, import_react36.createElement)("span", { className: "bt-us-row-sub" }, sub) : null
++ (0, import_react37.createElement)("span", { className: "bt-us-row-name" }, name),
++ sub ? (0, import_react37.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_react36.createElement)("span", { className: "bt-us-track" }, (0, import_react36.createElement)("span", { style: { width: `${width}%`, background: fill3 } }))
++ (0, import_react37.createElement)("span", { className: "bt-us-track" }, (0, import_react37.createElement)("span", { style: { width: `${width}%`, background: fill3 } }))
),
- (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_react36.createElement)("span", { className: "bt-us-row-num" }, (0, import_react36.createElement)("b", null, compact(sum.total)), (0, import_react36.createElement)("small", null, `${percent(sum.total, whole)}%`))
++ (0, import_react37.createElement)("span", { className: "bt-us-row-num" }, (0, import_react37.createElement)("b", null, compact(sum.total)), (0, import_react37.createElement)("small", null, `${percent(sum.total, whole)}%`))
);
}
function ListCard({ title, count, index, children }) {
- return (0, import_react24.createElement)(
-+ return (0, import_react36.createElement)(
++ return (0, import_react37.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_react36.createElement)("div", { className: "bt-us-card-head" }, (0, import_react36.createElement)("h3", null, title, count === void 0 ? null : (0, import_react36.createElement)("span", null, count))),
-+ (0, import_react36.createElement)("div", { className: "bt-us-rows" }, children)
++ (0, import_react37.createElement)("div", { className: "bt-us-card-head" }, (0, import_react37.createElement)("h3", null, title, count === void 0 ? null : (0, import_react37.createElement)("span", null, count))),
++ (0, import_react37.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_react36.createElement)(
++ return (0, import_react37.createElement)(
"section",
{ className: "bt-us-profile" },
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)(
"span",
{ className: "bt-us-ring" },
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("defs", null, (0, import_react36.createElement)(
++ face.stops ? (0, import_react37.createElement)("defs", null, (0, import_react37.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_react36.createElement)("stop", { key: offset, offset, stopColor: color }))
++ face.stops.map(([offset, color]) => (0, import_react37.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_react36.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }),
-+ arc > 0 ? (0, import_react36.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_react37.createElement)("circle", { className: "bt-us-ring-track", cx: 48, cy: 48, r: RING_R }),
++ arc > 0 ? (0, import_react37.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_react36.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64))
++ (0, import_react37.createElement)("span", { className: "bt-us-ring-face" }, face.glyph(64))
),
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("h3", null, face.name),
-+ face.sub ? (0, import_react36.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null,
-+ (0, import_react36.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_react37.createElement)("h3", null, face.name),
++ face.sub ? (0, import_react37.createElement)("span", { className: "bt-us-profile-sub" }, face.sub) : null,
++ (0, import_react37.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) }))
)
);
}
@@ -14775,17 +15002,17 @@ diff --git a/client.js b/client.js
var addMonths = (ms, count) => Date.UTC(yearOf(ms), new Date(ms).getUTCMonth() + count, 1);
function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
- const box = (0, import_react24.useRef)(null);
-+ const box = (0, import_react36.useRef)(null);
++ const box = (0, import_react37.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_react36.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today))));
-+ const [anchor, setAnchor] = (0, import_react36.useState)(null);
-+ const [hover, setHover] = (0, import_react36.useState)(null);
-+ (0, import_react36.useEffect)(() => {
++ const [right, setRight] = (0, import_react37.useState)(() => Math.max(addMonths(oldest, 1), monthOf(hourMs(span?.to ?? today))));
++ const [anchor, setAnchor] = (0, import_react37.useState)(null);
++ const [hover, setHover] = (0, import_react37.useState)(null);
++ (0, import_react37.useEffect)(() => {
const onDown = (event) => {
if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose();
};
@@ -14794,19 +15021,19 @@ diff --git a/client.js b/client.js
onPick(from, to);
};
- const weekdays = (0, import_react24.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []);
-+ const weekdays = (0, import_react36.useMemo)(() => Array.from({ length: 7 }, (_, at) => dates({ weekday: "narrow" }).format(Date.UTC(2024, 0, 1 + at))), []);
++ const weekdays = (0, import_react37.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_react36.createElement)("span", { key: `b${at}` }));
++ const cells = Array.from({ length: lead }, (_, at) => (0, import_react37.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_react36.createElement)("button", {
++ cells.push((0, import_react37.createElement)("button", {
key,
type: "button",
className: "bt-us-day",
@@ -14815,20 +15042,20 @@ diff --git a/client.js b/client.js
onMouseEnter: () => setHover(key),
onClick: () => press(key)
- }, (0, import_react24.createElement)("b", null, day)));
-+ }, (0, import_react36.createElement)("b", null, day)));
++ }, (0, import_react37.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_react36.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_react36.createElement)(ChevronLeftIcon)) : (0, import_react36.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_react36.createElement)(ChevronRightIcon));
-+ return (0, import_react36.createElement)(
++ const nav = side === "left" ? (0, import_react37.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_react37.createElement)(ChevronLeftIcon)) : (0, import_react37.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_react37.createElement)(ChevronRightIcon));
++ return (0, import_react37.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_react36.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react36.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))),
-+ (0, import_react36.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react36.createElement)("span", { key: at }, name))),
-+ (0, import_react36.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells)
++ (0, import_react37.createElement)("div", { className: "bt-us-month-head" }, nav, (0, import_react37.createElement)("span", null, dates({ year: "numeric", month: "long" }).format(start))),
++ (0, import_react37.createElement)("div", { className: "bt-us-weekdays", "aria-hidden": true }, weekdays.map((name, at) => (0, import_react37.createElement)("span", { key: at }, name))),
++ (0, import_react37.createElement)("div", { className: "bt-us-days", onMouseLeave: () => setHover(null) }, cells)
);
};
const shortcuts = [
@@ -14837,15 +15064,15 @@ diff --git a/client.js b/client.js
];
const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1;
- return (0, import_react24.createElement)(
-+ return (0, import_react36.createElement)(
++ return (0, import_react37.createElement)(
"div",
{ ref: box, className: "bt-us-pick", role: "dialog", "aria-label": t("Custom range") },
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("button", {
++ shortcuts.map(([id, label, from, to]) => (0, import_react37.createElement)("button", {
key: id,
type: "button",
"aria-pressed": span?.from === from && span?.to === to,
@@ -14854,20 +15081,20 @@ diff --git a/client.js b/client.js
),
- (0, import_react24.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)("div", { className: "bt-us-pick-months" }, month(addMonths(right, -1), "left"), month(right, "right")),
++ (0, import_react37.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_react36.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")),
-+ low === null ? null : (0, import_react36.createElement)("span", null, (0, import_react36.createElement)("b", null, spanText(low, high)), ` · ${daysText(count)}`)
++ (0, import_react37.createElement)("span", null, anchor ? t("Pick the last day") : t("Pick the first day")),
++ low === null ? null : (0, import_react37.createElement)("span", null, (0, import_react37.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_react36.useState)(null);
++ const [hover, setHover] = (0, import_react37.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);
@@ -14876,14 +15103,14 @@ diff --git a/client.js b/client.js
const monday = first + week * 7 * DAY_MS;
if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) {
- months.push((0, import_react24.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday)));
-+ months.push((0, import_react36.createElement)("span", { key: week, style: { gridColumn: week + 1 } }, dates({ month: "short" }).format(monday)));
++ months.push((0, import_react37.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_react36.createElement)("span", {
++ cells.push((0, import_react37.createElement)("span", {
key: ms,
className: "bt-us-cell",
style: { "--w": week },
@@ -14892,7 +15119,7 @@ diff --git a/client.js b/client.js
"data-today": ms === today ? "" : void 0,
onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day })
- }, level > 0 ? (0, import_react24.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
-+ }, level > 0 ? (0, import_react36.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
++ }, level > 0 ? (0, import_react37.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
}
}
const tipSide = hover ? {
@@ -14901,63 +15128,63 @@ diff --git a/client.js b/client.js
const active = [...daily.values()].filter((value) => value > 0).length;
const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active });
- return (0, import_react24.createElement)(
-+ return (0, import_react36.createElement)(
++ return (0, import_react37.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_react36.createElement)("div", { className: "bt-us-card-head" }, (0, import_react36.createElement)("h3", null, t("Activity")), (0, import_react36.createElement)("span", { className: "bt-us-head-note" }, summary)),
-+ (0, import_react36.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months),
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)("div", { className: "bt-us-card-head" }, (0, import_react37.createElement)("h3", null, t("Activity")), (0, import_react37.createElement)("span", { className: "bt-us-head-note" }, summary)),
++ (0, import_react37.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months),
++ (0, import_react37.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_react36.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
-+ hover ? (0, import_react36.createElement)(
++ (0, import_react37.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
++ hover ? (0, import_react37.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_react36.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)),
-+ (0, import_react36.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) }))
++ (0, import_react37.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)),
++ (0, import_react37.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_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react36.createElement)("i", { style: { background: soften(paint, level) } }))),
++ levels.map((level) => (0, import_react37.createElement)("span", { key: level, className: "bt-us-cell" }, (0, import_react37.createElement)("i", { style: { background: soften(paint, level) } }))),
t("Heavy")
)
);
}
function Skeleton2() {
- return (0, import_react24.createElement)(
-+ return (0, import_react36.createElement)(
++ return (0, import_react37.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_react36.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
++ (0, import_react37.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_react36.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
-+ (0, import_react36.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
++ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
++ (0, import_react37.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_react36.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }),
-+ (0, import_react36.createElement)("div", { className: "bt-us-skel", style: { height: 200 } })
++ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }),
++ (0, import_react37.createElement)("div", { className: "bt-us-skel", style: { height: 200 } })
)
)
);
@@ -14971,16 +15198,16 @@ diff --git a/client.js b/client.js
- const [picking, setPicking] = (0, import_react24.useState)(false);
- const [by, setBy] = (0, import_react24.useState)("bot");
- const tabs = (0, import_react24.useRef)(null);
-+ const [view, setView] = (0, import_react36.useState)(null);
-+ const [error, setError] = (0, import_react36.useState)("");
-+ const [rangeId, setRangeId] = (0, import_react36.useState)("7d");
-+ const [span, setSpan] = (0, import_react36.useState)(null);
-+ const [picking, setPicking] = (0, import_react36.useState)(false);
-+ const [by, setBy] = (0, import_react36.useState)("bot");
-+ const tabs = (0, import_react36.useRef)(null);
++ const [view, setView] = (0, import_react37.useState)(null);
++ const [error, setError] = (0, import_react37.useState)("");
++ const [rangeId, setRangeId] = (0, import_react37.useState)("7d");
++ const [span, setSpan] = (0, import_react37.useState)(null);
++ const [picking, setPicking] = (0, import_react37.useState)(false);
++ const [by, setBy] = (0, import_react37.useState)("bot");
++ const tabs = (0, import_react37.useRef)(null);
const focus = focusOf(focusId);
- (0, import_react24.useEffect)(() => {
-+ (0, import_react36.useEffect)(() => {
++ (0, import_react37.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(() => {
@@ -14989,7 +15216,7 @@ diff --git a/client.js b/client.js
await resized;
if (stopped) return;
- (0, import_react24.startTransition)(() => setView(value));
-+ (0, import_react36.startTransition)(() => setView(value));
++ (0, import_react37.startTransition)(() => setView(value));
setError("");
timer = setTimeout(load, value.scanning ? 3e3 : 2e4);
} catch (failure) {
@@ -14998,7 +15225,7 @@ diff --git a/client.js b/client.js
const today = view?.now.slice(0, 10);
const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId);
- const shaped = (0, import_react24.useMemo)(() => {
-+ const shaped = (0, import_react36.useMemo)(() => {
++ const shaped = (0, import_react37.useMemo)(() => {
if (!view) return null;
const faces2 = view.owners.map((owner) => ownerFace(owner, roster));
const lifetime = /* @__PURE__ */ new Map();
@@ -15007,7 +15234,7 @@ diff --git a/client.js b/client.js
const name = model.name || model.model || t("Unknown model");
const paint = modelPaint.get(at) ?? "var(--bt-us-rest)";
- return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react24.createElement)(ModelGlyph, { name, paint, size }) };
-+ return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react36.createElement)(ModelGlyph, { name, paint, size }) };
++ return { kind: "model", name, sub: model.providerName ?? model.provider, paint, glyph: (size) => (0, import_react37.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;
@@ -15017,29 +15244,29 @@ diff --git a/client.js b/client.js
}, [view, rangeId, span, by, focusId, roster]);
- const root = (0, import_react24.useRef)(null);
- (0, import_react24.useEffect)(() => {
-+ const root = (0, import_react36.useRef)(null);
-+ (0, import_react36.useEffect)(() => {
++ const root = (0, import_react37.useRef)(null);
++ (0, import_react37.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_react36.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react36.createElement)(Skeleton2);
++ return error ? (0, import_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : (0, import_react37.createElement)(Skeleton2);
}
if (view.rows.length === 0) {
- return (0, import_react24.createElement)(
-+ return (0, import_react36.createElement)(
++ return (0, import_react37.createElement)(
"div",
{ className: "bt-us" },
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }),
-+ (0, import_react36.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")),
-+ (0, import_react36.createElement)("span", null, t("Token usage shows up here after a model is called."))
++ (0, import_react37.createElement)(BotMark, { look: MAIN_LOOK, size: 56, live: true }),
++ (0, import_react37.createElement)("b", null, view.scanning ? t("Reading your chats…") : t("No usage yet.")),
++ (0, import_react37.createElement)("span", null, t("Token usage shows up here after a model is called."))
)
);
}
@@ -15048,7 +15275,7 @@ diff --git a/client.js b/client.js
];
const whole = part.total.total;
- const ownerRow = ([at, sum], index, total = whole) => (0, import_react24.createElement)(Row2, {
-+ const ownerRow = ([at, sum], index, total = whole) => (0, import_react36.createElement)(Row3, {
++ const ownerRow = ([at, sum], index, total = whole) => (0, import_react37.createElement)(Row3, {
key: `o${at}`,
kind: faces[at].kind,
glyph: faces[at].glyph(32),
@@ -15057,7 +15284,7 @@ diff --git a/client.js b/client.js
onClick: () => pick2({ kind: "owner", key: view.owners[at].key })
});
- const modelRow = ([at, sum], index) => (0, import_react24.createElement)(Row2, {
-+ const modelRow = ([at, sum], index) => (0, import_react36.createElement)(Row3, {
++ const modelRow = ([at, sum], index) => (0, import_react37.createElement)(Row3, {
key: `m${at}`,
kind: "model",
glyph: models[at].glyph(32),
@@ -15066,7 +15293,7 @@ diff --git a/client.js b/client.js
onClick: () => pick2({ kind: "model", key: view.models[at].key })
});
- const nothing = (0, import_react24.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range."));
-+ const nothing = (0, import_react36.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range."));
++ const nothing = (0, import_react37.createElement)("div", { className: "bt-us-none" }, t("Nothing in this range."));
let body;
if (face) {
const kinds = ranked(part.kinds);
@@ -15075,30 +15302,30 @@ diff --git a/client.js b/client.js
body = [
- (0, import_react24.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
++ (0, import_react37.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_react36.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
-+ (0, import_react36.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
++ (0, import_react37.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
++ (0, import_react37.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_react36.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react36.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_react36.createElement)(
++ ownerAt >= 0 ? (0, import_react37.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react37.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_react37.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_react36.createElement)(Row3, { 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_react37.createElement)(Row3, { 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_react36.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 })
++ (0, import_react37.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 })
)
];
} else {
@@ -15107,52 +15334,52 @@ diff --git a/client.js b/client.js
const usedModels = ranked(all.models);
body = [
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
-+ (0, import_react36.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
-+ (0, import_react36.createElement)(
++ (0, import_react37.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
++ (0, import_react37.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
++ (0, import_react37.createElement)(
"div",
{ key: "pair", className: "bt-us-pair" },
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)("div", { className: "bt-us-split", role: "separator" }) : null,
++ bots.length > 0 && rest.length > 0 ? (0, import_react37.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_react36.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing)
++ (0, import_react37.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_react36.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 })
++ (0, import_react37.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 })
)
];
}
- return (0, import_react24.createElement)(
-+ return (0, import_react36.createElement)(
++ return (0, import_react37.createElement)(
"div",
{ ref: root, className: "bt-us", key: focusId ?? "all" },
- (0, import_react24.createElement)(
-+ (0, import_react36.createElement)(
++ (0, import_react37.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_react36.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }),
-+ view.scanning ? (0, import_react36.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null,
-+ picking ? (0, import_react36.createElement)(SpanPicker, { span, today, oldest, active: activeDays, near: tabs, onPick: pickSpan, onClose: () => setPicking(false) }) : null
++ (0, import_react37.createElement)(Tabs, { label: t("Time range"), value: rangeId, onChange: chooseRange, options: rangeOptions, boxRef: tabs }),
++ view.scanning ? (0, import_react37.createElement)("span", { className: "bt-us-note", role: "status" }, t("Still reading older chats; totals may grow.")) : null,
++ picking ? (0, import_react37.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_react36.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
++ error ? (0, import_react37.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
body
);
}
@@ -15169,7 +15396,7 @@ diff --git a/client.js b/client.js
// src/client/connectors.js
-var import_react25 = require("react");
-+var import_react37 = require("react");
++var import_react38 = require("react");
var TOKEN_URL = "https://github.com/settings/personal-access-tokens/new";
-var GitHubMark = () => (0, import_react25.createElement)(
+var COMPOSIO_KEY_URL = "https://platform.composio.dev/";
@@ -15204,21 +15431,21 @@ diff --git a/client.js b/client.js
+ ["youtube", "YouTube"]
+];
+var appName = (slug) => APPS.find(([id]) => id === slug)?.[1] ?? slug;
-+var GitHubMark = () => (0, import_react37.createElement)(
++var GitHubMark = () => (0, import_react38.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_react37.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_react38.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 FeishuMark = () => (0, import_react38.createElement)(
++ "svg",
++ { width: 20, height: 20, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
++ (0, import_react38.createElement)("path", { d: "M3.2 6.6c3 .1 6.4 1.5 8.6 3.9-1.9.4-3.5 1.4-4.7 2.9-1.9-.6-3.4-1.9-3.9-3.6-.2-.7-.2-1.5 0-3.2z" }),
++ (0, import_react38.createElement)("path", { d: "M16.8 5.4c-2.9.2-5.6 1.5-7.4 3.4 1.7 1.2 3.1 2.8 3.9 4.9 2.2-1.1 3.8-3.2 4-5.6.1-.9-.1-1.9-.4-2.7z" }),
++ (0, import_react38.createElement)("path", { d: "M8.6 12.1c.9.7 1.7 1.6 2.2 2.7-1.1.2-2.3 0-3.3-.6.2-.8.6-1.5 1.1-2.1z" })
);
-var MARKS = { github: GitHubMark };
-var ABOUT = { github: () => t("Repositories, issues, and pull requests") };
-+var FeishuMark = () => (0, import_react37.createElement)(
-+ "svg",
-+ { width: 20, height: 20, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true },
-+ (0, import_react37.createElement)("path", { d: "M3.2 6.6c3 .1 6.4 1.5 8.6 3.9-1.9.4-3.5 1.4-4.7 2.9-1.9-.6-3.4-1.9-3.9-3.6-.2-.7-.2-1.5 0-3.2z" }),
-+ (0, import_react37.createElement)("path", { d: "M16.8 5.4c-2.9.2-5.6 1.5-7.4 3.4 1.7 1.2 3.1 2.8 3.9 4.9 2.2-1.1 3.8-3.2 4-5.6.1-.9-.1-1.9-.4-2.7z" }),
-+ (0, import_react37.createElement)("path", { d: "M8.6 12.1c.9.7 1.7 1.6 2.2 2.7-1.1.2-2.3 0-3.3-.6.2-.8.6-1.5 1.1-2.1z" })
-+);
+var MARKS = { github: GitHubMark, composio: ConnectorIcon, feishu: FeishuMark };
+var ABOUT = {
+ github: () => t("Repositories, issues, and pull requests"),
@@ -15242,11 +15469,11 @@ diff --git a/client.js b/client.js
- const [error, setError] = (0, import_react25.useState)(null);
+function CredentialField({ credential, value, onChange, onEnter, autoFocus }) {
+ const label = t(credential.label);
-+ if (credential.masked) return (0, import_react37.createElement)(MaskedInput, { value, onChange, onEnter, label, autoFocus });
-+ return (0, import_react37.createElement)(
++ if (credential.masked) return (0, import_react38.createElement)(MaskedInput, { value, onChange, onEnter, label, autoFocus });
++ return (0, import_react38.createElement)(
+ "label",
+ { className: "bt-q-field" },
-+ (0, import_react37.createElement)("input", {
++ (0, import_react38.createElement)("input", {
+ className: "bt-input",
+ type: "text",
+ value,
@@ -15262,31 +15489,31 @@ diff --git a/client.js b/client.js
+ );
+}
+function AllowedSenders({ connector, actions, run, busy }) {
-+ const [openId, setOpenId] = (0, import_react37.useState)("");
++ const [openId, setOpenId] = (0, import_react38.useState)("");
+ const allowed = connector.allowed ?? [];
+ const refused = connector.inbound?.refusedSenders ?? [];
+ const allow = (id) => void run(actions.connectorAllow(connector.id, id));
-+ return (0, import_react37.createElement)(
++ return (0, import_react38.createElement)(
+ "div",
+ { className: "bt-connector-allowed" },
-+ (0, import_react37.createElement)("span", { className: "bt-connector-person-title" }, t("Who may talk to the Bots")),
-+ allowed.length === 0 && refused.length === 0 ? (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("Nobody yet. A message from anyone else starts no turn.")) : null,
-+ allowed.map((entry) => (0, import_react37.createElement)(
++ (0, import_react38.createElement)("span", { className: "bt-connector-person-title" }, t("Who may talk to the Bots")),
++ allowed.length === 0 && refused.length === 0 ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("Nobody yet. A message from anyone else starts no turn.")) : null,
++ allowed.map((entry) => (0, import_react38.createElement)(
+ "div",
+ { key: entry.openId, className: "bt-connector-person" },
-+ (0, import_react37.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId),
-+ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorBlock(connector.id, entry.openId)) }, t("Block"))
++ (0, import_react38.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId),
++ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => void run(actions.connectorBlock(connector.id, entry.openId)) }, t("Block"))
+ )),
-+ refused.map((entry) => (0, import_react37.createElement)(
++ refused.map((entry) => (0, import_react38.createElement)(
+ "div",
+ { key: entry.openId, className: "bt-connector-person" },
-+ (0, import_react37.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId),
-+ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => allow(entry.openId) }, t("Allow"))
++ (0, import_react38.createElement)("span", { className: "bt-connector-person-name" }, entry.name ?? entry.openId),
++ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => allow(entry.openId) }, t("Allow"))
+ )),
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)(
+ "div",
+ { className: "bt-connector-form-row" },
-+ (0, import_react37.createElement)("input", {
++ (0, import_react38.createElement)("input", {
+ className: "bt-input",
+ type: "text",
+ value: openId,
@@ -15301,7 +15528,7 @@ diff --git a/client.js b/client.js
+ }
+ }
+ }),
-+ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || openId.trim() === "", onClick: () => {
++ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || openId.trim() === "", onClick: () => {
+ allow(openId.trim());
+ setOpenId("");
+ } }, t("Allow"))
@@ -15311,20 +15538,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_react37.createElement)(
++ return (0, import_react38.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_react37.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
++ label ? (0, import_react38.createElement)("span", { className: "bt-app-status" }, ` · ${label}`) : null
+ );
+}
+function AppPicker({ apps, onChange, search, statuses = {} }) {
-+ const [query, setQuery] = (0, import_react37.useState)("");
-+ const [found, setFound] = (0, import_react37.useState)(null);
-+ const [failed, setFailed] = (0, import_react37.useState)(null);
-+ const names = (0, import_react37.useRef)(new Map(APPS));
-+ const asked = (0, import_react37.useRef)(0);
-+ (0, import_react37.useEffect)(() => {
++ const [query, setQuery] = (0, import_react38.useState)("");
++ const [found, setFound] = (0, import_react38.useState)(null);
++ const [failed, setFailed] = (0, import_react38.useState)(null);
++ const names = (0, import_react38.useRef)(new Map(APPS));
++ const asked = (0, import_react38.useRef)(0);
++ (0, import_react38.useEffect)(() => {
+ const text2 = query.trim();
+ if (text2 === "" || search === void 0) {
+ setFound(null);
@@ -15351,13 +15578,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_react37.createElement)(
++ return (0, import_react38.createElement)(
+ "div",
+ { className: "bt-connector-apps" },
-+ apps.length > 0 ? (0, import_react37.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react37.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null,
-+ (0, import_react37.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_react37.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null,
-+ (0, import_react37.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react37.createElement)("button", { key: app.slug, type: "button", className: "bt-state", "aria-pressed": false, onClick: () => {
++ apps.length > 0 ? (0, import_react38.createElement)("div", { className: "bt-states", "aria-label": t("Picked apps") }, apps.map((slug) => (0, import_react38.createElement)(AppChip, { key: slug, name: nameOf(slug), status: statuses[slug], onRemove: () => onChange(apps.filter((app) => app !== slug)) }))) : null,
++ (0, import_react38.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_react38.createElement)("span", { className: "bt-set-hint" }, t("Could not search Composio: {error}", { error: failed })) : null,
++ (0, import_react38.createElement)("div", { className: "bt-states", "aria-label": t("Apps to add") }, results.length === 0 ? (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("No apps match")) : results.map((app) => (0, import_react38.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)))
@@ -15365,22 +15592,22 @@ 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_react37.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react37.createElement)(
++ return (0, import_react38.createElement)("ul", { className: "bt-connector-app-list", "aria-label": t("App status") }, apps.map((slug) => (0, import_react38.createElement)(
+ "li",
+ { key: slug, className: "bt-connector-app", "data-status": statuses[slug] ?? "unknown" },
-+ (0, import_react37.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)),
-+ (0, import_react37.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "")
++ (0, import_react38.createElement)("span", { className: "bt-connector-app-name" }, appName(slug)),
++ (0, import_react38.createElement)("span", { className: "bt-app-status" }, APP_STATUS[statuses[slug]]?.() ?? "")
+ )));
+}
+function ConnectorRow({ connector, roster, actions }) {
-+ const [form, setForm] = (0, import_react37.useState)(false);
-+ const [bots, setBots] = (0, import_react37.useState)(false);
-+ const [token, setToken] = (0, import_react37.useState)("");
-+ const [apps, setApps] = (0, import_react37.useState)(() => connector.apps ?? []);
-+ const [picking, setPicking] = (0, import_react37.useState)(false);
-+ const [busy, setBusy] = (0, import_react37.useState)(false);
-+ const [error, setError] = (0, import_react37.useState)(null);
-+ const [message, setMessage] = (0, import_react37.useState)("");
++ const [form, setForm] = (0, import_react38.useState)(false);
++ const [bots, setBots] = (0, import_react38.useState)(false);
++ const [token, setToken] = (0, import_react38.useState)("");
++ const [apps, setApps] = (0, import_react38.useState)(() => connector.apps ?? []);
++ const [picking, setPicking] = (0, import_react38.useState)(false);
++ const [busy, setBusy] = (0, import_react38.useState)(false);
++ const [error, setError] = (0, import_react38.useState)(null);
++ const [message, setMessage] = (0, import_react38.useState)("");
const run = (task, after) => {
setBusy(true);
setError(null);
@@ -15392,15 +15619,15 @@ diff --git a/client.js b/client.js
+ const sender = connector.kind === "sender";
+ const channel = connector.kind === "channel";
+ const modes = connector.modes ?? [];
-+ const [mode, setMode] = (0, import_react37.useState)(() => connector.mode ?? modes[0]?.id);
-+ const [values, setValues] = (0, import_react37.useState)({});
++ const [mode, setMode] = (0, import_react38.useState)(() => connector.mode ?? modes[0]?.id);
++ const [values, setValues] = (0, import_react38.useState)({});
+ const chosen = modes.find((entry) => entry.id === mode) ?? modes[0];
+ const field = (name) => values[name] ?? "";
+ const setField = (name, value) => setValues((current) => ({ ...current, [name]: value }));
+ const filled = chosen !== void 0 && chosen.credentials.every((credential) => field(credential.name).trim() !== "");
-+ const [statuses, setStatuses] = (0, import_react37.useState)({});
++ const [statuses, setStatuses] = (0, import_react38.useState)({});
+ const appsKey = (connector.apps ?? []).join(",");
-+ (0, import_react37.useEffect)(() => {
++ (0, import_react38.useEffect)(() => {
+ if (!gateway || connector.state !== "ready" || appsKey === "" || actions.connectorAppStatus === void 0) {
+ setStatuses({});
+ return void 0;
@@ -15415,7 +15642,7 @@ diff --git a/client.js b/client.js
+ live = false;
+ };
+ }, [gateway, connector.state, appsKey, picking]);
-+ const search = (0, import_react37.useCallback)((query) => actions.connectorAppSearch(connector.id, query, connector.state === "off" ? token : void 0), [connector.id, connector.state, token]);
++ const search = (0, import_react38.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), () => {
@@ -15454,65 +15681,65 @@ 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_react37.useRef)(null);
++ const row = (0, import_react38.useRef)(null);
+ useMorph(row, `${form}:${picking}:${bots}:${error !== null}:${connector.state}:${mode}`);
-+ return (0, import_react37.createElement)(
++ return (0, import_react38.createElement)(
"div",
{ ref: row, className: "bt-connector", "data-state": connector.state },
- (0, import_react25.createElement)(
-+ (0, import_react37.createElement)(
++ (0, import_react38.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_react37.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react37.createElement)(Mark) : null),
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)("span", { className: "bt-connector-mark" }, Mark ? (0, import_react38.createElement)(Mark) : null),
++ (0, import_react38.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_react37.createElement)("span", { className: "bt-connector-name" }, connector.name),
-+ (0, import_react37.createElement)("span", { className: "bt-connector-status" }, statusText2(connector))
++ (0, import_react38.createElement)("span", { className: "bt-connector-name" }, connector.name),
++ (0, import_react38.createElement)("span", { className: "bt-connector-status" }, statusText2(connector))
),
- (0, import_react25.createElement)(
-+ (0, import_react37.createElement)(
++ (0, import_react38.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_react37.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_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
++ connector.state === "error" ? (0, import_react38.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_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
+ setApps(connector.apps ?? []);
+ setPicking(true);
+ } }, t("Choose apps")) : null,
-+ on && perBot ? (0, import_react37.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_react37.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_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setForm(true) }, t("Connect"))
++ on && perBot ? (0, import_react38.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_react38.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_react38.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_react37.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null,
-+ on && perBot ? (0, import_react37.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null,
-+ on && bots ? (0, import_react37.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null,
-+ on && channel && connector.mode === "app" ? (0, import_react37.createElement)(AllowedSenders, { connector, actions, run, busy }) : null,
-+ on && canPost ? (0, import_react37.createElement)(
++ on && gateway && !picking ? (0, import_react38.createElement)(AppStatusList, { apps: connector.apps ?? [], statuses }) : null,
++ on && perBot ? (0, import_react38.createElement)("span", { className: "bt-connector-uses" }, usageText(roster, connector.id)) : null,
++ on && bots ? (0, import_react38.createElement)(ConnectorBots, { connector, roster, actions, run, SwitchRow }) : null,
++ on && channel && connector.mode === "app" ? (0, import_react38.createElement)(AllowedSenders, { connector, actions, run, busy }) : null,
++ on && canPost ? (0, import_react38.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_react37.createElement)("label", { className: "bt-q-field" }, (0, import_react37.createElement)("input", { className: "bt-input", type: "text", value: message, "aria-label": t("Message to send to Feishu"), placeholder: t("Type a message for the Feishu group"), onChange: (event) => setMessage(event.target.value) })),
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)("label", { className: "bt-q-field" }, (0, import_react38.createElement)("input", { className: "bt-input", type: "text", value: message, "aria-label": t("Message to send to Feishu"), placeholder: t("Type a message for the Feishu group"), onChange: (event) => setMessage(event.target.value) })),
++ (0, import_react38.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_react37.createElement)("span", { className: "bt-connector-link" }),
-+ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || message.trim() === "" || actions.connectorSend === void 0, onClick: () => void run(actions.connectorSend(connector.id, message), () => setMessage("")) }, t("Send"))
++ (0, import_react38.createElement)("span", { className: "bt-connector-link" }),
++ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || message.trim() === "" || actions.connectorSend === void 0, onClick: () => void run(actions.connectorSend(connector.id, message), () => setMessage("")) }, t("Send"))
+ )
+ ) : null,
-+ !on && form ? (0, import_react37.createElement)(
++ !on && form ? (0, import_react38.createElement)(
+ "div",
+ { className: "bt-connector-form" },
-+ channel ? (0, import_react37.createElement)("div", { className: "bt-states", role: "group", "aria-label": t("How to connect") }, modes.map((entry) => (0, import_react37.createElement)("button", {
++ channel ? (0, import_react38.createElement)("div", { className: "bt-states", role: "group", "aria-label": t("How to connect") }, modes.map((entry) => (0, import_react38.createElement)("button", {
+ key: entry.id,
+ type: "button",
+ className: "bt-state",
@@ -15523,75 +15750,75 @@ diff --git a/client.js b/client.js
+ setError(null);
+ }
+ }, t(entry.label)))) : null,
-+ channel ? (chosen?.credentials ?? []).map((credential, index) => (0, import_react37.createElement)(CredentialField, {
++ channel ? (chosen?.credentials ?? []).map((credential, index) => (0, import_react38.createElement)(CredentialField, {
+ key: credential.name,
+ credential,
+ value: field(credential.name),
+ onChange: (value) => setField(credential.name, value),
+ onEnter: connect,
+ autoFocus: index === 0
-+ })) : (0, import_react37.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react37.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })),
-+ gateway ? (0, import_react37.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null,
-+ (0, import_react37.createElement)(
++ })) : (0, import_react38.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react38.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })),
++ gateway ? (0, import_react38.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null,
++ (0, import_react38.createElement)(
+ "div",
+ { className: "bt-connector-form-row" },
-+ (0, import_react37.createElement)(
++ (0, import_react38.createElement)(
+ "a",
+ { className: "bt-connector-link", href: channel ? chosen?.link ?? FEISHU_WEBHOOK_URL : gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" },
+ channel ? t(chosen?.linkLabel ?? "Get a webhook URL in Feishu") : gateway ? t("Get a Composio API key") : t("Create a token on GitHub")
+ ),
-+ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
++ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
setForm(false);
setToken("");
+ setValues({});
++ setError(null);
++ } }, t("Cancel")),
++ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || (channel ? !filled : token.trim() === ""), onClick: connect }, busy ? t("Connecting…") : t("Connect"))
++ )
++ ) : null,
++ on && gateway && picking ? (0, import_react38.createElement)(
++ "div",
++ { className: "bt-connector-form" },
++ (0, import_react38.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }),
++ (0, import_react38.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_react38.createElement)(
++ "div",
++ { className: "bt-connector-form-row" },
++ (0, import_react38.createElement)("span", { className: "bt-connector-link" }),
++ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
++ setPicking(false);
setError(null);
} }, t("Cancel")),
- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect"))
-+ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || (channel ? !filled : token.trim() === ""), onClick: connect }, busy ? t("Connecting…") : t("Connect"))
++ (0, import_react38.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
-+ on && gateway && picking ? (0, import_react37.createElement)(
-+ "div",
-+ { className: "bt-connector-form" },
-+ (0, import_react37.createElement)(AppPicker, { apps, onChange: setApps, search, statuses }),
-+ (0, import_react37.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_react37.createElement)(
-+ "div",
-+ { className: "bt-connector-form-row" },
-+ (0, import_react37.createElement)("span", { className: "bt-connector-link" }),
-+ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => {
-+ setPicking(false);
-+ setError(null);
-+ } }, t("Cancel")),
-+ (0, import_react37.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save"))
-+ )
-+ ) : null,
-+ error ? (0, import_react37.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null
++ error ? (0, import_react38.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_react37.createElement)(
++ return (0, import_react38.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_react37.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react37.createElement)("div", { className: "bt-set-row" }, (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react37.createElement)(ConnectorRow, { key: connector.id, connector, roster, actions })))
++ (0, import_react38.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react38.createElement)("div", { className: "bt-set-row" }, (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react38.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_react38 = require("react");
-+var FeedbackIcon = () => (0, import_react38.createElement)(
++var import_react39 = require("react");
++var FeedbackIcon = () => (0, import_react39.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_react38.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_react38.createElement)("path", { d: "M10 7v2.6M10 11.4h.01" })
++ (0, import_react39.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_react39.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" }));
@@ -15622,17 +15849,17 @@ diff --git a/client.js b/client.js
- const [note, setNote] = (0, import_react26.useState)("");
+var SOURCES = { turn: "Model call", host: "FlyBot", api: "FlyBot", browser: "This window" };
+function FeedbackPage({ actions, run, Section, Row: Row4 }) {
-+ const [what, setWhat] = (0, import_react38.useState)("");
-+ const [report, setReport] = (0, import_react38.useState)(null);
-+ const [preview, setPreview] = (0, import_react38.useState)(false);
-+ const [note, setNote] = (0, import_react38.useState)("");
++ const [what, setWhat] = (0, import_react39.useState)("");
++ const [report, setReport] = (0, import_react39.useState)(null);
++ const [preview, setPreview] = (0, import_react39.useState)(false);
++ const [note, setNote] = (0, import_react39.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_react38.useEffect)(() => {
++ (0, import_react39.useEffect)(() => {
void run(load());
}, []);
const copyReport = () => run(fetchReport().then(async (next) => {
@@ -15641,7 +15868,7 @@ diff --git a/client.js b/client.js
if (!copied) setPreview(true);
}));
- const [issueLink, setIssueLink] = (0, import_react26.useState)(null);
-+ const [issueLink, setIssueLink] = (0, import_react38.useState)(null);
++ const [issueLink, setIssueLink] = (0, import_react39.useState)(null);
const openIssue = () => {
const win = window.open("", "_blank");
if (win !== null) win.opener = null;
@@ -15661,86 +15888,86 @@ diff --git a/client.js b/client.js
const shown = entries2.slice(0, 8);
return [
- (0, import_react26.createElement)(
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
Section,
{ key: "report", title: t("Report a problem") },
- (0, import_react26.createElement)(
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
"div",
{ className: "bt-set-row bt-set-paste bt-fb-what" },
- (0, import_react26.createElement)(
-+ (0, import_react38.createElement)(
++ (0, import_react39.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_react38.createElement)("span", null, t("What happened?")),
-+ (0, import_react38.createElement)("span", { className: "bt-set-hint" }, t("What you did, what you expected, and what happened instead. It goes into the issue."))
++ (0, import_react39.createElement)("span", null, t("What happened?")),
++ (0, import_react39.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_react38.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_react39.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)(
- Row3,
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
+ Row4,
{ 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_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")),
-+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: copyReport }, t("Copy"))
++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setPreview(!preview), "aria-expanded": preview }, preview ? t("Hide") : t("Show")),
++ (0, import_react39.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)(
- 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"))
-+ preview ? (0, import_react38.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react38.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null,
-+ (0, import_react38.createElement)(
++ preview ? (0, import_react39.createElement)("div", { className: "bt-set-row bt-fb-preview" }, (0, import_react39.createElement)("pre", { tabIndex: 0, "aria-label": t("Diagnostics") }, report?.markdown || t("Loading…"))) : null,
++ (0, import_react39.createElement)(
+ Row4,
+ { 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_react38.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
++ (0, import_react39.createElement)("button", { type: "button", className: "bt-fb-primary", onClick: openIssue }, t("Report on GitHub"))
)
),
- note ? (0, import_react26.createElement)(
-+ note ? (0, import_react38.createElement)(
++ note ? (0, import_react39.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_react38.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null
++ issueLink ? [" ", (0, import_react39.createElement)("a", { key: "link", href: issueLink, target: "_blank", rel: "noopener noreferrer" }, t("Open the issue"))] : null
) : null,
- (0, import_react26.createElement)(
-+ (0, import_react38.createElement)(
++ (0, import_react39.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_react38.createElement)(Row4, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react38.createElement)(
++ shown.length === 0 ? (0, import_react39.createElement)(Row4, { label: t("No errors recorded."), hint: t("Failed model calls and FlyBot problems show here, newest first.") }) : shown.map((entry, index) => (0, import_react39.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_react38.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)("span", { className: "bt-fb-dot", "data-level": entry.level, "aria-hidden": true }),
++ (0, import_react39.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_react38.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)),
-+ (0, import_react38.createElement)("span", { className: "bt-set-hint" }, [whenText2(entry.time), entry.bot, entry.where, t(SOURCES[entry.source] ?? "FlyBot")].filter(Boolean).join(" · "))
++ (0, import_react39.createElement)("span", { className: "bt-fb-text" }, entrySummary(entry)),
++ (0, import_react39.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_react38.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null
++ entry.code ? (0, import_react39.createElement)("span", { className: "bt-tag bt-fb-code" }, entry.status ? `${entry.code} ${entry.status}` : entry.code) : null
)),
- (0, import_react26.createElement)(
- Row3,
-+ (0, import_react38.createElement)(
++ (0, import_react39.createElement)(
+ Row4,
{ 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_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")),
-+ (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear"))
++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: downloadLog }, t("Download")),
++ (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: clearLog, disabled: !report || total === 0 }, t("Clear"))
)
)
];
@@ -15750,8 +15977,8 @@ diff --git a/client.js b/client.js
const toast = useUi((state) => state.toast);
- const [leaving, setLeaving] = (0, import_react26.useState)(false);
- (0, import_react26.useEffect)(() => {
-+ const [leaving, setLeaving] = (0, import_react38.useState)(false);
-+ (0, import_react38.useEffect)(() => {
++ const [leaving, setLeaving] = (0, import_react39.useState)(false);
++ (0, import_react39.useEffect)(() => {
if (!toast) return void 0;
setLeaving(false);
const fade = setTimeout(() => setLeaving(true), TOAST_MS);
@@ -15761,19 +15988,19 @@ diff --git a/client.js b/client.js
if (!toast) return null;
- return (0, import_react26.createElement)(
+ const info = toast.tone === "info";
-+ return (0, import_react38.createElement)(
++ return (0, import_react39.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_react38.createElement)("span", { className: "bt-toast-text" }, toast.text),
-+ info ? null : (0, import_react38.createElement)("button", { type: "button", className: "bt-toast-link", onClick: () => {
++ (0, import_react39.createElement)("span", { className: "bt-toast-text" }, toast.text),
++ info ? null : (0, import_react39.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_react38.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react38.createElement)(CloseIcon))
++ (0, import_react39.createElement)("button", { type: "button", className: "bt-icon-btn bt-toast-close", "aria-label": t("Dismiss"), onClick: () => actions.dismissToast(toast.token) }, (0, import_react39.createElement)(CloseIcon))
);
}
var FEEDBACK_CSS = `
@@ -15798,7 +16025,7 @@ diff --git a/client.js b/client.js
// src/client/about.js
-var import_react27 = require("react");
-var CHEER_MS = 1800;
-+var import_react39 = require("react");
++var import_react40 = 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…");
@@ -15842,9 +16069,9 @@ diff --git a/client.js b/client.js
- const [checking, setChecking] = (0, import_react27.useState)(false);
- const [stars, setStars] = (0, import_react27.useState)(null);
- (0, import_react27.useEffect)(() => {
-+ const [checking, setChecking] = (0, import_react39.useState)(false);
-+ const [stars, setStars] = (0, import_react39.useState)(null);
-+ (0, import_react39.useEffect)(() => {
++ const [checking, setChecking] = (0, import_react40.useState)(false);
++ const [stars, setStars] = (0, import_react40.useState)(null);
++ (0, import_react40.useEffect)(() => {
let live = true;
actions.repoStars().then((count) => {
if (live && Number.isInteger(count)) setStars(count);
@@ -15876,14 +16103,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_react39.createElement)(
++ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react40.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_react39.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_react39.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_react39.createElement)(
++ ) : (0, import_react40.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_react40.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_react40.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")
@@ -15891,22 +16118,22 @@ diff --git a/client.js b/client.js
const repo = update.repoUrl;
return [
- (0, import_react27.createElement)(
-+ (0, import_react39.createElement)(
++ (0, import_react40.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_react39.createElement)("span", { className: "bt-about-mark" }, (0, import_react39.createElement)(FlyBotMark, { size: 52 })),
-+ (0, import_react39.createElement)(
++ (0, import_react40.createElement)("span", { className: "bt-about-mark" }, (0, import_react40.createElement)(FlyBotMark, { size: 52 })),
++ (0, import_react40.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_react39.createElement)("span", { className: "bt-about-name" }, t("FlyBot")),
-+ update.current ? (0, import_react39.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null
++ (0, import_react40.createElement)("span", { className: "bt-about-name" }, t("FlyBot")),
++ update.current ? (0, import_react40.createElement)("span", { className: "bt-about-version" }, versionLine(update)) : null
),
- repo ? (0, import_react27.createElement)(
-+ repo ? (0, import_react39.createElement)(
++ repo ? (0, import_react40.createElement)(
"a",
{
className: "bt-star",
@@ -15920,31 +16147,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_react39.createElement)("span", { className: "bt-star-main" }, (0, import_react39.createElement)("span", { className: "bt-star-icon" }, (0, import_react39.createElement)(StarLineIcon)), "Star"),
-+ stars === null ? null : (0, import_react39.createElement)("span", { className: "bt-star-count" }, starCount(stars))
++ (0, import_react40.createElement)("span", { className: "bt-star-main" }, (0, import_react40.createElement)("span", { className: "bt-star-icon" }, (0, import_react40.createElement)(StarLineIcon)), "Star"),
++ stars === null ? null : (0, import_react40.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_react39.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_react39.createElement)(
++ repo ? (0, import_react40.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_react40.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_react39.createElement)(Row4, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react39.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null),
-+ dsh?.current ? (0, import_react39.createElement)(Row4, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null
++ (0, import_react40.createElement)(Row4, { label: t("FlyBot core"), hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react40.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null),
++ dsh?.current ? (0, import_react40.createElement)(Row4, { label: t("DeepSeek Harness (underlying)"), hint: dshHint(dsh, dshShown) }, dshControl) : null
),
- (0, import_react27.createElement)(
-+ (0, import_react39.createElement)(
++ (0, import_react40.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_react39.createElement)(Row4, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react39.createElement)("a", linkProps(repo), (0, import_react39.createElement)(GitHubMark), t("Open"))) : null,
-+ update.notesUrl ? (0, import_react39.createElement)(Row4, { label: t("Release notes") }, (0, import_react39.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
-+ (0, import_react39.createElement)(Row4, { label: t("Report a problem") }, (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open")))
++ repo ? (0, import_react40.createElement)(Row4, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react40.createElement)("a", linkProps(repo), (0, import_react40.createElement)(GitHubMark), t("Open"))) : null,
++ update.notesUrl ? (0, import_react40.createElement)(Row4, { label: t("Release notes") }, (0, import_react40.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
++ (0, import_react40.createElement)(Row4, { label: t("Report a problem") }, (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open")))
)
];
}
@@ -15964,7 +16191,7 @@ diff --git a/client.js b/client.js
// src/client/settings.js
-var import_react28 = require("react");
-+var import_react40 = require("react");
++var import_react41 = 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 PAGE_ICONS = { ...SETTINGS_ICONS, connectors: ConnectorIcon, capabilities: PlugIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon };
@@ -15972,17 +16199,17 @@ diff --git a/client.js b/client.js
+var PAGE_ICONS = { ...SETTINGS_ICONS, projects: FolderIcon, connectors: ConnectorIcon, workflows: WorkflowIcon, 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_react40.createElement)("section", { className: "bt-set-section" }, (0, import_react40.createElement)("h3", null, title), (0, import_react40.createElement)("div", { className: "bt-set-card" }, children));
++ return (0, import_react41.createElement)("section", { className: "bt-set-section" }, (0, import_react41.createElement)("h3", null, title), (0, import_react41.createElement)("div", { className: "bt-set-card" }, children));
}
function SetRow({ label, hint, children }) {
- return (0, import_react28.createElement)(
-+ return (0, import_react40.createElement)(
++ return (0, import_react41.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_react40.createElement)("div", { className: "bt-set-copy" }, (0, import_react40.createElement)("span", null, label), hint ? (0, import_react40.createElement)("span", { className: "bt-set-hint" }, hint) : null),
-+ children ? (0, import_react40.createElement)("div", { className: "bt-set-control" }, children) : null
++ (0, import_react41.createElement)("div", { className: "bt-set-copy" }, (0, import_react41.createElement)("span", null, label), hint ? (0, import_react41.createElement)("span", { className: "bt-set-hint" }, hint) : null),
++ children ? (0, import_react41.createElement)("div", { className: "bt-set-control" }, children) : null
);
}
+var NOTIFY_KINDS = [
@@ -15997,9 +16224,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_react40.useState)(prefs.accent ?? "#4d6bfe");
-+ const timer = (0, import_react40.useRef)(0);
-+ (0, import_react40.useEffect)(() => () => clearTimeout(timer.current), []);
++ const [custom, setCustom] = (0, import_react41.useState)(prefs.accent ?? "#4d6bfe");
++ const timer = (0, import_react41.useRef)(0);
++ (0, import_react41.useEffect)(() => () => clearTimeout(timer.current), []);
const pickCustom = (value) => {
setCustom(value);
clearTimeout(timer.current);
@@ -16007,33 +16234,33 @@ diff --git a/client.js b/client.js
};
return [
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
SetSection,
{ key: "look", title: t("Appearance") },
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
SetRow,
{ label: t("Theme"), hint: t("Colors for the whole team, on every device.") },
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.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_react40.createElement)("option", { key: id, value: id }, t(theme.label)))
++ Object.entries(themes).map(([id, theme]) => (0, import_react41.createElement)("option", { key: id, value: id }, t(theme.label)))
)
),
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
SetRow,
{ label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") },
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.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_react40.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_react40.createElement)("button", {
++ (0, import_react41.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_react41.createElement)("button", {
key: ink,
type: "button",
className: "bt-swatch",
@@ -16042,18 +16269,18 @@ diff --git a/client.js b/client.js
onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] }))
})),
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.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_react40.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) })
++ (0, import_react41.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_react40.createElement)(
++ (0, import_react41.createElement)(
SetSection,
- { key: "mode", title: t("Mode") },
- (0, import_react28.createElement)(
@@ -16062,13 +16289,13 @@ 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_react40.createElement)(SwitchRow, {
++ (0, import_react41.createElement)(SwitchRow, {
+ label: t("In other chats"),
+ 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_react40.createElement)(SwitchRow, {
++ NOTIFY_KINDS.map(([kind, label, hint]) => (0, import_react41.createElement)(SwitchRow, {
+ key: kind,
+ label: t(label),
+ hint: hint ? t(hint) : void 0,
@@ -16086,40 +16313,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_react40.createElement)(
++ return (0, import_react41.createElement)(
SetSection,
{ title: t("Main Bot") },
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.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_react40.createElement)("select", {
++ (0, import_react41.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_react40.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react40.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
++ }, first ? null : (0, import_react41.createElement)("option", { value: "" }, t("Choose a Bot…")), own.map((bot) => (0, import_react41.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
),
- others.map((bot) => (0, import_react28.createElement)(
-+ others.map((bot) => (0, import_react40.createElement)(
++ others.map((bot) => (0, import_react41.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_react40.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_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role"))
++ (0, import_react41.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_react41.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_react40.createElement)(
++ candidates.length > 0 ? (0, import_react41.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_react40.createElement)("select", {
++ (0, import_react41.createElement)("select", {
className: "bt-select",
"aria-label": t("Add a Main Bot"),
value: "",
@@ -16127,7 +16354,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_react40.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react40.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
++ }, (0, import_react41.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react41.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
) : null
);
}
@@ -16135,15 +16362,15 @@ diff --git a/client.js b/client.js
function MotionSection({ roster, actions, run }) {
const motion = roster.prefs?.motion === "quiet" || roster.prefs?.motion === "lively" ? roster.prefs.motion : "normal";
- return (0, import_react28.createElement)(
-+ return (0, import_react40.createElement)(
++ return (0, import_react41.createElement)(
SetSection,
{ title: t("Animation") },
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.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_react40.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react40.createElement)("button", {
++ (0, import_react41.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react41.createElement)("button", {
key: id,
type: "button",
"aria-pressed": motion === id,
@@ -16152,30 +16379,30 @@ diff --git a/client.js b/client.js
}
function BotsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react28.useState)(false);
-+ const [creating, setCreating] = (0, import_react40.useState)(false);
++ const [creating, setCreating] = (0, import_react41.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_react40.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
-+ (0, import_react40.createElement)(MotionSection, { key: "motion", roster, actions, run }),
-+ creating ? (0, import_react40.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react40.createElement)(
++ (0, import_react41.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
++ (0, import_react41.createElement)(MotionSection, { key: "motion", roster, actions, run }),
++ creating ? (0, import_react41.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react41.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_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react40.createElement)(PlusIcon), t("New Bot"))
++ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react41.createElement)(PlusIcon), t("New Bot"))
),
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.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_react40.createElement)(
++ roster.bots.filter((bot) => !bot.remote).map((bot) => (0, import_react41.createElement)(
"button",
{ key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) },
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react28.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
@@ -16183,42 +16410,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_react40.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
-+ (0, import_react40.createElement)("span", { className: "bt-row-title" }, bot.name),
-+ bot.role ? (0, import_react40.createElement)("span", { className: "bt-tag" }, bot.role) : null,
-+ isMainOf(roster, bot.id) ? (0, import_react40.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
-+ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react40.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
++ (0, import_react41.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
++ (0, import_react41.createElement)("span", { className: "bt-row-title" }, bot.name),
++ bot.role ? (0, import_react41.createElement)("span", { className: "bt-tag" }, bot.role) : null,
++ isMainOf(roster, bot.id) ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
++ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
),
- (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon))
-+ (0, import_react40.createElement)("span", { className: "bt-chevron" }, (0, import_react40.createElement)(ChevronRightIcon))
++ (0, import_react41.createElement)("span", { className: "bt-chevron" }, (0, import_react41.createElement)(ChevronRightIcon))
))
)
];
}
function GroupsPage({ roster, actions, run }) {
- const [creating, setCreating] = (0, import_react28.useState)(false);
-+ const [creating, setCreating] = (0, import_react40.useState)(false);
++ const [creating, setCreating] = (0, import_react41.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_react40.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react40.createElement)(
++ creating ? (0, import_react41.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react41.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_react40.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_react40.createElement)(PlusIcon), t("Create group chat"))
++ (0, import_react41.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_react41.createElement)(PlusIcon), t("Create group chat"))
),
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.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_react40.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_react41.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_react40.createElement)(
++ return (0, import_react41.createElement)(
"button",
{ key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) },
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
"span",
{ className: "bt-set-bot" },
- (0, import_react28.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
@@ -16226,14 +16453,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_react40.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
-+ (0, import_react40.createElement)("span", { className: "bt-row-title" }, room2.name),
-+ (0, import_react40.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })),
-+ admin ? (0, import_react40.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react40.createElement)(AdminStar), admin.name) : null,
-+ room2.hidden ? (0, import_react40.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
++ (0, import_react41.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
++ (0, import_react41.createElement)("span", { className: "bt-row-title" }, room2.name),
++ (0, import_react41.createElement)("span", { className: "bt-tag" }, t(seatedCount(room2) === 1 ? "{count} member" : "{count} members", { count: seatedCount(room2) })),
++ admin ? (0, import_react41.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react41.createElement)(AdminStar), admin.name) : null,
++ room2.hidden ? (0, import_react41.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
),
- (0, import_react28.createElement)("span", { className: "bt-chevron" }, (0, import_react28.createElement)(ChevronRightIcon))
-+ (0, import_react40.createElement)("span", { className: "bt-chevron" }, (0, import_react40.createElement)(ChevronRightIcon))
++ (0, import_react41.createElement)("span", { className: "bt-chevron" }, (0, import_react41.createElement)(ChevronRightIcon))
);
})
)
@@ -16242,8 +16469,8 @@ diff --git a/client.js b/client.js
function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) {
- const [error, setError] = (0, import_react28.useState)("");
- const close = (0, import_react28.useCallback)(() => actions.closeSettings(), []);
-+ const [error, setError] = (0, import_react40.useState)("");
-+ const close = (0, import_react40.useCallback)(() => actions.closeSettings(), []);
++ const [error, setError] = (0, import_react41.useState)("");
++ const close = (0, import_react41.useCallback)(() => actions.closeSettings(), []);
useEscape(close);
const run = (promise) => Promise.resolve(promise).then(() => {
setError("");
@@ -16264,34 +16491,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_react40.useRef)({ key: pageKey, depth, move: "up" });
++ const last = (0, import_react41.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_react40.useState)(shown === "usage");
++ const [usageOpened, setUsageOpened] = (0, import_react41.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_react40.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
-+ return (0, import_react40.createElement)(
++ const errorLine = error ? (0, import_react41.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
++ return (0, import_react41.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_react40.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
++ (0, import_react41.createElement)(
"div",
- { className: "bt-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_react40.createElement)(
++ (0, import_react41.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_react40.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ pages.map(([key, text2]) => (0, import_react40.createElement)("button", {
++ (0, import_react41.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
++ pages.map(([key, text2]) => (0, import_react41.createElement)("button", {
key,
type: "button",
className: "bt-settings-item",
@@ -16299,10 +16526,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_react40.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text2))),
++ }, (0, import_react41.createElement)(extra.find((item) => item.key === key)?.entry.icon ?? PAGE_ICONS[key] ?? ConnectorIcon), t(text2))),
+ // 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_react40.createElement)("button", {
++ (0, import_react41.createElement)("button", {
type: "button",
className: "bt-settings-item bt-settings-shell",
- title: t("Models, providers, and the rest of DeepSeek Harness"),
@@ -16313,23 +16540,23 @@ diff --git a/client.js b/client.js
openHarnessSettings();
}
- }, (0, import_react28.createElement)(SlidersIcon), t("Harness settings"))
-+ }, (0, import_react40.createElement)(SlidersIcon), t("Harness settings"), (0, import_react40.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
++ }, (0, import_react41.createElement)(SlidersIcon), t("Harness settings"), (0, import_react41.createElement)("span", { className: "bt-settings-out", "aria-hidden": true }, "↗"))
),
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
"div",
{ className: "bt-settings-main" },
- (0, import_react28.createElement)(
-+ (0, import_react40.createElement)(
++ (0, import_react41.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_react40.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react40.createElement)(ChevronLeftIcon)) : null,
-+ (0, import_react40.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label)
++ back ? (0, import_react41.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react41.createElement)(ChevronLeftIcon)) : null,
++ (0, import_react41.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_react40.createElement)(
++ shown === "usage" ? null : (0, import_react41.createElement)(
"div",
{ key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move },
errorLine,
@@ -16344,32 +16571,32 @@ 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_react40.createElement)(GeneralPage, { roster, actions, run }) : null,
-+ bot ? (0, import_react40.createElement)("div", { className: "bt-settings-form" }, (0, import_react40.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
-+ shown === "bots" && !bot ? (0, import_react40.createElement)(BotsPage, { roster, actions, run }) : null,
-+ room2 ? (0, import_react40.createElement)("div", { className: "bt-settings-form" }, (0, import_react40.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
-+ shown === "groups" && !room2 ? (0, import_react40.createElement)(GroupsPage, { roster, actions, run }) : null,
-+ shown === "projects" ? (0, import_react40.createElement)(ProjectsPage, { roster, actions, run, call: call2 }) : null,
-+ shown === "connectors" ? (0, import_react40.createElement)(ConnectorsPage, { roster, actions }) : null,
-+ shown === "workflows" ? (0, import_react40.createElement)(WorkflowsPage, { call: call2, roster }) : null,
-+ pluginPage ? (0, import_react40.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null,
-+ shown === "capabilities" ? (0, import_react40.createElement)(CapabilitiesPage, { roster, actions, run }) : null,
-+ shown === "schedules" ? (0, import_react40.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
-+ shown === "secrets" ? (0, import_react40.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
-+ shown === "feedback" ? (0, import_react40.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
-+ shown === "about" ? (0, import_react40.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null
++ shown === "general" ? (0, import_react41.createElement)(GeneralPage, { roster, actions, run }) : null,
++ bot ? (0, import_react41.createElement)("div", { className: "bt-settings-form" }, (0, import_react41.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
++ shown === "bots" && !bot ? (0, import_react41.createElement)(BotsPage, { roster, actions, run }) : null,
++ room2 ? (0, import_react41.createElement)("div", { className: "bt-settings-form" }, (0, import_react41.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
++ shown === "groups" && !room2 ? (0, import_react41.createElement)(GroupsPage, { roster, actions, run }) : null,
++ shown === "projects" ? (0, import_react41.createElement)(ProjectsPage, { roster, actions, run, call: call2 }) : null,
++ shown === "connectors" ? (0, import_react41.createElement)(ConnectorsPage, { roster, actions }) : null,
++ shown === "workflows" ? (0, import_react41.createElement)(WorkflowsPage, { call: call2, roster }) : null,
++ pluginPage ? (0, import_react41.createElement)(SettingsPageSlot, { entry: pluginPage, props: { roster, actions, call: call2, run, t, Section: SetSection, Row: SetRow } }) : null,
++ shown === "capabilities" ? (0, import_react41.createElement)(CapabilitiesPage, { roster, actions, run }) : null,
++ shown === "schedules" ? (0, import_react41.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
++ shown === "secrets" ? (0, import_react41.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
++ shown === "feedback" ? (0, import_react41.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
++ shown === "about" ? (0, import_react41.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null
),
- usageOpened ? (0, import_react28.createElement)(
-+ usageOpened ? (0, import_react40.createElement)(
++ usageOpened ? (0, import_react41.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_react40.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
++ (0, import_react41.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_react40.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react40.createElement)(CloseIcon))
++ (0, import_react41.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react41.createElement)(CloseIcon))
)
);
}
@@ -16380,35 +16607,35 @@ diff --git a/client.js b/client.js
+ ...(roster?.bots ?? []).filter((bot) => !bot.remote).map((bot) => ({ kind: "bot", id: bot.id, name: bot.name, projectId: bot.projectId ?? null })),
+ ...(roster?.rooms ?? []).map((room2) => ({ kind: "group", id: room2.id, name: room2.name, projectId: room2.projectId ?? null }))
+];
-+function ProjectsList({ projects, roster, call: call4, reload }) {
++function ProjectsList({ projects, roster, call: call5, reload }) {
+ const members = projectMembers(roster);
+ const bind = async (choice, project) => {
+ const [kind, id] = String(choice ?? "").split(":");
+ if (!kind || !id) return;
-+ await call4("project-bind", { kind, id, projectId: project.id });
++ await call5("project-bind", { kind, id, projectId: project.id });
+ await reload?.();
+ };
+ const unbind = async (member) => {
-+ await call4("project-bind", { kind: member.kind, id: member.id, projectId: null });
++ await call5("project-bind", { kind: member.kind, id: member.id, projectId: null });
+ await reload?.();
+ };
+ const bound2 = members.filter((member) => member.projectId !== null && member.projectId !== void 0);
+ return [
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
+ SetSection,
+ { key: "projects", title: t("Projects · {count}", { count: projects.length }) },
-+ projects.length === 0 ? (0, import_react40.createElement)("div", { className: "bt-set-row" }, (0, import_react40.createElement)("div", { className: "bt-set-copy" }, (0, import_react40.createElement)("span", null, t("No Project yet — create one.")))) : projects.map((project) => (0, import_react40.createElement)(
++ projects.length === 0 ? (0, import_react41.createElement)("div", { className: "bt-set-row" }, (0, import_react41.createElement)("div", { className: "bt-set-copy" }, (0, import_react41.createElement)("span", null, t("No Project yet — create one.")))) : projects.map((project) => (0, import_react41.createElement)(
+ SetRow,
+ {
+ key: project.id,
+ label: project.name,
+ hint: project.workDir === null || project.workDir === void 0 ? void 0 : project.workDir
+ },
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
+ "div",
+ { className: "bt-project-row-controls" },
-+ (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openProjectBoard(project.id) }, t("Open board")),
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => openProjectBoard(project.id) }, t("Open board")),
++ (0, import_react41.createElement)(
+ "select",
+ {
+ className: "bt-select",
@@ -16418,8 +16645,8 @@ diff --git a/client.js b/client.js
+ void bind(event.target.value, project);
+ }
+ },
-+ (0, import_react40.createElement)("option", { value: "" }, t("Bind a Bot or group")),
-+ members.map((member) => (0, import_react40.createElement)(
++ (0, import_react41.createElement)("option", { value: "" }, t("Bind a Bot or group")),
++ members.map((member) => (0, import_react41.createElement)(
+ "option",
+ { key: `${member.kind}:${member.id}`, value: `${member.kind}:${member.id}` },
+ `${member.kind === "group" ? t("Group") : t("Bot")} · ${member.name}`
@@ -16428,32 +16655,32 @@ diff --git a/client.js b/client.js
+ )
+ ))
+ ),
-+ bound2.length === 0 ? null : (0, import_react40.createElement)(
++ bound2.length === 0 ? null : (0, import_react41.createElement)(
+ SetSection,
+ { key: "bindings", title: t("Bindings") },
-+ bound2.map((member) => (0, import_react40.createElement)(
++ bound2.map((member) => (0, import_react41.createElement)(
+ SetRow,
+ {
+ key: `${member.kind}:${member.id}`,
+ label: member.name,
+ hint: member.kind === "group" ? t("Group") : t("Bot")
+ },
-+ (0, import_react40.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
++ (0, import_react41.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
+ void unbind(member);
+ } }, t("Unbind"))
+ ))
+ )
+ ];
+}
-+function ProjectsPage({ roster, actions, run, call: call4 }) {
-+ const [state, setState] = (0, import_react40.useState)({ projects: [], error: null });
-+ const [name, setName] = (0, import_react40.useState)("");
-+ const [workDir, setWorkDir] = (0, import_react40.useState)(null);
-+ const reload = (0, import_react40.useCallback)(async () => {
-+ const listing = await call4("projects");
++function ProjectsPage({ roster, actions, run, call: call5 }) {
++ const [state, setState] = (0, import_react41.useState)({ projects: [], error: null });
++ const [name, setName] = (0, import_react41.useState)("");
++ const [workDir, setWorkDir] = (0, import_react41.useState)(null);
++ const reload = (0, import_react41.useCallback)(async () => {
++ const listing = await call5("projects");
+ setState({ projects: listing?.projects ?? [], error: null });
-+ }, [call4]);
-+ (0, import_react40.useEffect)(() => {
++ }, [call5]);
++ (0, import_react41.useEffect)(() => {
+ void reload().catch((error) => setState({ projects: [], error: error?.message ?? String(error) }));
+ }, [reload]);
+ const create = async () => {
@@ -16462,7 +16689,7 @@ diff --git a/client.js b/client.js
+ const body = { name: trimmed3 };
+ if (workDir !== null && workDir !== "") body.workDir = workDir;
+ try {
-+ await call4("project-create", body);
++ await call5("project-create", body);
+ setName("");
+ setWorkDir(null);
+ await reload();
@@ -16471,14 +16698,14 @@ diff --git a/client.js b/client.js
+ }
+ };
+ return [
-+ state.error === null ? null : (0, import_react40.createElement)("div", { key: "error", className: "bt-error", role: "alert" }, state.error),
-+ (0, import_react40.createElement)(
++ state.error === null ? null : (0, import_react41.createElement)("div", { key: "error", className: "bt-error", role: "alert" }, state.error),
++ (0, import_react41.createElement)(
+ SetSection,
+ { key: "new", title: t("New Project") },
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
+ SetRow,
+ { label: t("Name") },
-+ (0, import_react40.createElement)("input", {
++ (0, import_react41.createElement)("input", {
+ className: "bt-input",
+ "aria-label": t("Project name"),
+ value: name,
@@ -16486,37 +16713,37 @@ diff --git a/client.js b/client.js
+ onChange: (event) => setName(event.target.value)
+ })
+ ),
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
+ SetRow,
+ { label: t("Working folder"), hint: t("Optional. Where this Project's work happens.") },
-+ (0, import_react40.createElement)(FolderMenu, { value: workDir, actions, run, onPick: setWorkDir })
++ (0, import_react41.createElement)(FolderMenu, { value: workDir, actions, run, onPick: setWorkDir })
+ ),
-+ (0, import_react40.createElement)(
++ (0, import_react41.createElement)(
+ "div",
+ { className: "bt-actions" },
-+ (0, import_react40.createElement)("button", {
++ (0, import_react41.createElement)("button", {
+ type: "button",
+ className: "bt-primary bt-with-icon",
+ disabled: name.trim() === "",
+ onClick: () => {
+ void create();
+ }
-+ }, (0, import_react40.createElement)(PlusIcon), t("New Project"))
++ }, (0, import_react41.createElement)(PlusIcon), t("New Project"))
+ )
+ ),
+ // The list is rendered here rather than as a child element: it holds no state of its own,
+ // and the page keeps one reload path for both halves.
-+ ...ProjectsList({ projects: state.projects, roster, call: call4, reload })
++ ...ProjectsList({ projects: state.projects, roster, call: call5, reload })
+ ];
+}
// src/client/context-menu.js
-var import_react29 = require("react");
-+var import_react41 = require("react");
++var import_react42 = require("react");
function ContextMenu({ menu, roster, actions, leaving = false }) {
useEscape(() => actions.closeMenu());
- (0, import_react29.useEffect)(() => {
-+ (0, import_react41.useEffect)(() => {
++ (0, import_react42.useEffect)(() => {
const onDown = (event) => {
if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu();
};
@@ -16527,15 +16754,15 @@ diff --git a/client.js b/client.js
- const [place, setPlace] = (0, import_react29.useState)({ left: menu.x, top: menu.y });
- (0, import_react29.useLayoutEffect)(() => {
+ const structure = useSidebarStructure();
-+ const ref = (0, import_react41.useRef)(null);
-+ const [place, setPlace] = (0, import_react41.useState)({ left: menu.x, top: menu.y });
-+ (0, import_react41.useLayoutEffect)(() => {
++ const ref = (0, import_react42.useRef)(null);
++ const [place, setPlace] = (0, import_react42.useState)({ left: menu.x, top: menu.y });
++ (0, import_react42.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_react41.createElement)(AgentMenu, { menu, roster, actions, leaving });
++ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react42.createElement)(AgentMenu, { menu, roster, actions, leaving });
const bot = roster.byId[menu.id];
const room2 = roster.roomsById[menu.id];
const entry = bot ?? room2;
@@ -16545,7 +16772,7 @@ diff --git a/client.js b/client.js
const mainCount = (roster.mainBotIds ?? []).length;
- const groups = [
+ const groups = bot?.remote ? [
- [
++ [
+ [t("Mark as Unread"), UnreadIcon, () => actions.markUnread(menu.id)]
+ ],
+ [
@@ -16554,7 +16781,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)]
],
@@ -16578,18 +16805,18 @@ diff --git a/client.js b/client.js
- return (0, import_react29.createElement)(
+ ];
+ const shown = groups.map((group) => group.filter(Boolean)).filter((group) => group.length > 0);
-+ return (0, import_react41.createElement)(
++ return (0, import_react42.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_react41.createElement)(
-+ import_react41.Fragment,
++ shown.map((group, index) => (0, import_react42.createElement)(
++ import_react42.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_react41.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
-+ group.map(([label, Icon, run, danger]) => (0, import_react41.createElement)("button", {
++ index > 0 ? (0, import_react42.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
++ group.map(([label, Icon, run, danger]) => (0, import_react42.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
@@ -16598,7 +16825,7 @@ diff --git a/client.js b/client.js
void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
}
- }, (0, import_react29.createElement)(Icon), label))
-+ }, (0, import_react41.createElement)(Icon), label))
++ }, (0, import_react42.createElement)(Icon), label))
))
);
}
@@ -16606,9 +16833,9 @@ diff --git a/client.js b/client.js
- const ref = (0, import_react29.useRef)(null);
- const [place, setPlace] = (0, import_react29.useState)({ left: menu.x, top: menu.y });
- (0, import_react29.useLayoutEffect)(() => {
-+ const ref = (0, import_react41.useRef)(null);
-+ const [place, setPlace] = (0, import_react41.useState)({ left: menu.x, top: menu.y });
-+ (0, import_react41.useLayoutEffect)(() => {
++ const ref = (0, import_react42.useRef)(null);
++ const [place, setPlace] = (0, import_react42.useState)({ left: menu.x, top: menu.y });
++ (0, import_react42.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)) });
@@ -16617,18 +16844,18 @@ diff --git a/client.js b/client.js
]
];
- return (0, import_react29.createElement)(
-+ return (0, import_react41.createElement)(
++ return (0, import_react42.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_react41.createElement)(
-+ import_react41.Fragment,
++ groups.map((group, index) => (0, import_react42.createElement)(
++ import_react42.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_react41.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
-+ group.map(([label, Icon, run, danger]) => (0, import_react41.createElement)("button", {
++ index > 0 ? (0, import_react42.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
++ group.map(([label, Icon, run, danger]) => (0, import_react42.createElement)("button", {
key: label,
type: "button",
role: "menuitem",
@@ -16637,7 +16864,7 @@ diff --git a/client.js b/client.js
void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error));
}
- }, (0, import_react29.createElement)(Icon), label))
-+ }, (0, import_react41.createElement)(Icon), label))
++ }, (0, import_react42.createElement)(Icon), label))
))
);
}
@@ -16907,7 +17134,7 @@ diff --git a/client.js b/client.js
- }
- return lines.join("\n\n");
-}
-+var import_react42 = require("react");
++var import_react43 = require("react");
var TAB_LABELS = { details: "Details", browser: "Browser" };
var hostOf = (url) => {
try {
@@ -16916,11 +17143,11 @@ diff --git a/client.js b/client.js
var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]);
function PermissionRow({ bot, roster, actions, run }) {
- const [pending, setPending] = (0, import_react31.useState)(null);
-+ const [pending, setPending] = (0, import_react42.useState)(null);
++ const [pending, setPending] = (0, import_react43.useState)(null);
const catalog = roster.permissions;
const saved = bot.permission ?? catalog?.defaultPreset;
- (0, import_react31.useEffect)(() => {
-+ (0, import_react42.useEffect)(() => {
++ (0, import_react43.useEffect)(() => {
if (pending === saved) setPending(null);
}, [pending, saved]);
if (!catalog?.options?.length) return null;
@@ -16929,17 +17156,17 @@ diff --git a/client.js b/client.js
void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null));
};
- return (0, import_react31.createElement)(
-+ return (0, import_react42.createElement)(
++ return (0, import_react43.createElement)(
"div",
{ className: "bt-drawer-permission" },
- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
- (0, import_react31.createElement)(
-+ (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
-+ (0, import_react42.createElement)(
++ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
++ (0, import_react43.createElement)(
Segmented,
{ label: t("Permissions"), value: current },
- catalog.options.map((option) => (0, import_react31.createElement)("button", {
-+ catalog.options.map((option) => (0, import_react42.createElement)("button", {
++ catalog.options.map((option) => (0, import_react43.createElement)("button", {
key: option.value,
type: "button",
"aria-pressed": option.value === current,
@@ -16948,56 +17175,56 @@ diff --git a/client.js b/client.js
const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS);
const pages = useBrowserPages((map) => map);
- (0, import_react31.useEffect)(() => {
-+ (0, import_react42.useEffect)(() => {
++ (0, import_react43.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_react42.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app."));
++ return (0, import_react43.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_react42.createElement)(
++ return (0, import_react43.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_react42.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
-+ (0, import_react42.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
-+ (0, import_react42.createElement)("div", { className: "bt-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_react42.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
++ (0, import_react43.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
++ (0, import_react43.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
++ (0, import_react43.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_react43.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
);
}
- return (0, import_react31.createElement)(
-+ return (0, import_react42.createElement)(
++ return (0, import_react43.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_react42.createElement)(
++ return (0, import_react43.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_react42.createElement)(
++ (0, import_react43.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_react42.createElement)(
++ page.excerpt ? (0, import_react43.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_react42.createElement)("span", { className: "bt-bcard-ptitle" }, label),
-+ (0, import_react42.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
-+ ) : (0, import_react42.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react42.createElement)(GlobeIcon), (0, import_react42.createElement)("span", null, label))
++ (0, import_react43.createElement)("span", { className: "bt-bcard-ptitle" }, label),
++ (0, import_react43.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
++ ) : (0, import_react43.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react43.createElement)(GlobeIcon), (0, import_react43.createElement)("span", null, label))
),
- (0, import_react31.createElement)("button", {
-+ (0, import_react42.createElement)("button", {
++ (0, import_react43.createElement)("button", {
type: "button",
className: "bt-bcard-x",
"aria-label": t("Close tab"),
@@ -17006,20 +17233,20 @@ diff --git a/client.js b/client.js
}
}, "×"),
- (0, import_react31.createElement)(
-+ (0, import_react42.createElement)(
++ (0, import_react43.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_react42.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
-+ (0, import_react42.createElement)("span", { className: "bt-bcard-title" }, label),
-+ page.url ? (0, import_react42.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null
++ (0, import_react43.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
++ (0, import_react43.createElement)("span", { className: "bt-bcard-title" }, label),
++ page.url ? (0, import_react43.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_react42.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react42.createElement)(PlusIcon), t("New tab"))
++ (0, import_react43.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react43.createElement)(PlusIcon), t("New tab"))
);
}
-function About({ text, title = "About" }) {
@@ -17027,27 +17254,27 @@ diff --git a/client.js b/client.js
- const long = text.split("\n").length > 5 || text.length > 240;
- const body = (0, import_react31.useRef)(null);
+function About({ text: text2, title = "About" }) {
-+ const [open, setOpen] = (0, import_react42.useState)(false);
++ const [open, setOpen] = (0, import_react43.useState)(false);
+ const long = text2.split("\n").length > 5 || text2.length > 240;
-+ const body = (0, import_react42.useRef)(null);
++ const body = (0, import_react43.useRef)(null);
useMorph(body, open);
- return (0, import_react31.createElement)(
-+ return (0, import_react42.createElement)(
++ return (0, import_react43.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_react42.createElement)("div", { className: "bt-section-title" }, t(title)),
-+ (0, import_react42.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2),
-+ long ? (0, import_react42.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null
- );
- }
++ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t(title)),
++ (0, import_react43.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2),
++ long ? (0, import_react43.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_react42.useState)(room2.pendingApprovals ?? []);
-+ const [busy, setBusy] = (0, import_react42.useState)("");
++ const [items, setItems] = (0, import_react43.useState)(room2.pendingApprovals ?? []);
++ const [busy, setBusy] = (0, import_react43.useState)("");
+ const face = room2.origin === "face-to-face";
-+ (0, import_react42.useEffect)(() => {
++ (0, import_react43.useEffect)(() => {
+ const haveItems = Array.isArray(room2.pendingApprovals) && room2.pendingApprovals.length;
+ if (haveItems) setItems(room2.pendingApprovals);
+ if (room2.viewerRole !== "invited-owner" && !face) return void 0;
@@ -17073,28 +17300,28 @@ diff --git a/client.js b/client.js
+ setBusy("");
+ }
+ };
-+ return (0, import_react42.createElement)(
++ return (0, import_react43.createElement)(
+ "div",
+ { key: "approvals", className: "bt-shared-wait", "data-flybot": "room-pending-approvals" },
-+ (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react42.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))),
-+ items.map((item) => (0, import_react42.createElement)(
++ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Waiting for your approval"), (0, import_react43.createElement)("span", { className: "bt-chip" }, t("{count} to approve", { count: items.length }))),
++ items.map((item) => (0, import_react43.createElement)(
+ "div",
+ { key: item.id, className: "bt-set-row" },
-+ (0, import_react42.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")),
-+ (0, import_react42.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")),
-+ (0, import_react42.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "reject") }, t("Reject"))
++ (0, import_react43.createElement)("span", { className: "bt-set-copy" }, item.text || item.summary || item.to || t("Outbound reply")),
++ (0, import_react43.createElement)("button", { type: "button", className: "bt-mini", disabled: busy === item.id, onClick: () => decide(item, "approve") }, t("Approve")),
++ (0, import_react43.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_react42.createElement)(
++ return (0, import_react43.createElement)(
+ "div",
+ null,
-+ (0, import_react42.createElement)(FaceLead, { room: room2, roster, pick: pick2 }),
-+ (0, import_react42.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })),
-+ (0, import_react42.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react42.createElement)(FacePersonRow, {
++ (0, import_react43.createElement)(FaceLead, { room: room2, roster, pick: pick2 }),
++ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })),
++ (0, import_react43.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react43.createElement)(FacePersonRow, {
+ key: person.key,
+ person,
+ roster,
@@ -17102,8 +17329,8 @@ diff --git a/client.js b/client.js
+ 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);
@@ -17120,31 +17347,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_react42.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
-+ invited && room2.sharedFrom && !face ? (0, import_react42.createElement)(
- "div",
-+ { key: "from", className: "bt-shared-from" },
-+ (0, import_react42.createElement)("div", { className: "bt-section-title" }, title),
-+ (0, import_react42.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_react42.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null,
-+ (0, import_react42.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot"))
-+ ) : face ? null : room2.notice ? (0, import_react42.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react42.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
-+ face ? null : (0, import_react42.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
-+ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react42.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react42.createElement)(
++ (0, import_react43.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
++ invited && room2.sharedFrom && !face ? (0, import_react43.createElement)(
+ "div",
++ { key: "from", className: "bt-shared-from" },
++ (0, import_react43.createElement)("div", { className: "bt-section-title" }, title),
++ (0, import_react43.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_react43.createElement)("div", { className: "bt-set-hint" }, t("Invited {when}", { when: new Date(room2.sharedFrom.invitedAt).toLocaleString() })) : null,
++ (0, import_react43.createElement)("button", { type: "button", className: "bt-more", onClick: leaveMine }, t("Remove my Bot"))
++ ) : face ? null : room2.notice ? (0, import_react43.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react43.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
++ face ? null : (0, import_react43.createElement)(SharedRoomApprovals, { key: "approvals", room: room2 }),
++ face && (room2.face?.status === "candidate" || room2.face?.status === "joining") ? null : face ? (0, import_react43.createElement)(FaceMembers, { key: "members", room: room2, roster, actions }) : (0, import_react43.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_react42.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })),
-+ (0, import_react42.createElement)("div", { className: "bt-members", role: "list" }, [
-+ ...humans.map((person) => (0, import_react42.createElement)(
++ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length + humans.length + remotes.length })),
++ (0, import_react43.createElement)("div", { className: "bt-members", role: "list" }, [
++ ...humans.map((person) => (0, import_react43.createElement)(
"div",
- { key: member.id, role: "listitem" },
- (0, import_react31.createElement)(
+ { key: person.id, role: "listitem" },
-+ (0, import_react42.createElement)(
++ (0, import_react43.createElement)(
"div",
{ className: "bt-member" },
- (0, import_react31.createElement)(
@@ -17152,70 +17379,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_react42.createElement)("span", { className: "bt-member-name" }, (0, import_react42.createElement)("span", null, person.displayName || person.id)),
-+ (0, import_react42.createElement)("span", { className: "bt-chip" }, t("Human")),
-+ person.role === "admin" ? (0, import_react42.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
++ (0, import_react43.createElement)("span", { className: "bt-member-name" }, (0, import_react43.createElement)("span", null, person.displayName || person.id)),
++ (0, import_react43.createElement)("span", { className: "bt-chip" }, t("Human")),
++ person.role === "admin" ? (0, import_react43.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
+ )
+ )),
-+ ...remotes.map((person) => (0, import_react42.createElement)(
++ ...remotes.map((person) => (0, import_react43.createElement)(
+ "div",
+ { key: person.handle || person.id, role: "listitem" },
-+ (0, import_react42.createElement)(
++ (0, import_react43.createElement)(
+ "div",
+ { className: "bt-member" },
-+ (0, import_react42.createElement)(
++ (0, import_react43.createElement)(
+ "span",
+ { className: "bt-member-name" },
-+ (0, import_react42.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
-+ (0, import_react42.createElement)("span", { title: person.handle || void 0 }, person.label || remoteBotTitle(person))
++ (0, import_react43.createElement)(BotAvatar, { bot: lookBotForRemote(person, roster), size: 26, badge: false, live: false }),
++ (0, import_react43.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_react42.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
++ face ? null : (0, import_react43.createElement)("span", { className: "bt-chip" }, t("Friend's Bot"))
+ )
+ )),
+ ...members.map((member) => {
+ const entry = entries2.find((item) => item.id === member.id);
-+ return (0, import_react42.createElement)(
++ return (0, import_react43.createElement)(
+ "div",
+ { key: member.id, role: "listitem" },
-+ (0, import_react42.createElement)(
++ (0, import_react43.createElement)(
+ "div",
+ { className: "bt-member" },
-+ (0, import_react42.createElement)(
++ (0, import_react43.createElement)(
+ "button",
+ { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) },
-+ (0, import_react42.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
-+ (0, import_react42.createElement)("span", null, member.name)
++ (0, import_react43.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
++ (0, import_react43.createElement)("span", null, member.name)
+ ),
-+ room2.admin === member.id ? (0, import_react42.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
++ room2.admin === member.id ? (0, import_react43.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
+ ),
-+ entry ? (0, import_react42.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
++ entry ? (0, import_react43.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
+ );
+ })
+ ])
),
- (0, import_react31.createElement)(
-+ face ? null : (0, import_react42.createElement)(
++ face ? null : (0, import_react43.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_react42.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
-+ (0, import_react42.createElement)("div", { className: "bt-kv" }, (0, import_react42.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone")))
++ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
++ (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone")))
),
- usage ? (0, import_react31.createElement)(
-+ face || !usage ? null : (0, import_react42.createElement)(
++ face || !usage ? null : (0, import_react43.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_react42.createElement)("div", { className: "bt-section-title" }, t("Usage")),
-+ (0, import_react42.createElement)("div", { className: "bt-kv" }, (0, import_react42.createElement)("span", null, room2.name), (0, import_react42.createElement)("span", null, usage))
++ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Usage")),
++ (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, room2.name), (0, import_react43.createElement)("span", null, usage))
+ )
];
}
@@ -17224,21 +17451,21 @@ diff --git a/client.js b/client.js
const part = owner ? currentPart(owner) : sessionId;
const isMainBot = isMainOf(roster, owner?.id);
- const [tab, setTab] = (0, import_react31.useState)("details");
-+ const [tab, setTab] = (0, import_react42.useState)("details");
++ const [tab, setTab] = (0, import_react43.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_react42.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
-+ const [editing, setEditing] = (0, import_react42.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
-+ const [draftName, setDraftName] = (0, import_react42.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
-+ const [copied, setCopied] = (0, import_react42.useState)(false);
-+ const [error, setError] = (0, import_react42.useState)("");
++ const [view, setView] = (0, import_react43.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
++ const [editing, setEditing] = (0, import_react43.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
++ const [draftName, setDraftName] = (0, import_react43.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
++ const [copied, setCopied] = (0, import_react43.useState)(false);
++ const [error, setError] = (0, import_react43.useState)("");
const glide = useGlide(tab);
- const last = (0, import_react31.useRef)({ view, tab, move: void 0 });
-+ const last = (0, import_react42.useRef)({ view, tab, move: void 0 });
++ const last = (0, import_react43.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;
@@ -17273,48 +17500,48 @@ diff --git a/client.js b/client.js
};
- const errorLine = error ? (0, import_react31.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
- if ((bot || room2) && view === "settings") {
-+ const errorLine = error ? (0, import_react42.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
++ const errorLine = error ? (0, import_react43.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_react42.createElement)(
++ (0, import_react43.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_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react42.createElement)(ChevronLeftIcon)),
-+ (0, import_react42.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
-+ hasStrip ? null : (0, import_react42.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react42.createElement)(CloseIcon))
++ (0, import_react43.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react43.createElement)(ChevronLeftIcon)),
++ (0, import_react43.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
++ hasStrip ? null : (0, import_react43.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react43.createElement)(CloseIcon))
),
- (0, import_react31.createElement)(
-+ (0, import_react42.createElement)(
++ (0, import_react43.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_react42.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react42.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
++ bot ? (0, import_react43.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react43.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
)
];
}
- const top = (0, import_react31.createElement)(
-+ const top = (0, import_react42.createElement)(
++ const top = (0, import_react43.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_react42.createElement)("button", {
++ bot || room2 ? (0, import_react43.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_react42.createElement)(GearIcon)) : null,
-+ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react42.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_react42.createElement)(GlobeIcon)) : null,
-+ bot || room2 ? (0, import_react42.createElement)("button", {
++ }, (0, import_react43.createElement)(GearIcon)) : null,
++ bot && !bot.remote && roster.browser === true && actions.openBrowser ? (0, import_react43.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_react43.createElement)(GlobeIcon)) : null,
++ bot || room2 ? (0, import_react43.createElement)("button", {
+ type: "button",
+ className: "bt-round",
+ // #1347: the accessible name says what the click does, which is copy — "Share conversation"
@@ -17323,15 +17550,15 @@ diff --git a/client.js b/client.js
+ "aria-label": copied ? t("Copied conversation") : t("Copy conversation"),
+ title: copied ? t("Copied") : t("Copy conversation as Markdown"),
+ onClick: share
-+ }, copied ? (0, import_react42.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react42.createElement)(ShareIcon)) : null,
-+ hasStrip ? null : (0, import_react42.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react42.createElement)(CloseIcon))
++ }, copied ? (0, import_react43.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react43.createElement)(ShareIcon)) : null,
++ hasStrip ? null : (0, import_react43.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react43.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_react42.createElement)(import_react42.Fragment, { key: "top" }, top),
-+ (0, import_react42.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react42.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team.")))
++ (0, import_react43.createElement)(import_react43.Fragment, { key: "top" }, top),
++ (0, import_react43.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react43.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team.")))
];
}
const current = bot?.name ?? room2.name;
@@ -17341,14 +17568,14 @@ diff --git a/client.js b/client.js
return [
- (0, import_react31.createElement)(import_react31.Fragment, { key: "top" }, top),
- (0, import_react31.createElement)(
-+ (0, import_react42.createElement)(import_react42.Fragment, { key: "top" }, top),
-+ (0, import_react42.createElement)(
++ (0, import_react43.createElement)(import_react43.Fragment, { key: "top" }, top),
++ (0, import_react43.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_react42.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react42.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
-+ bot?.remote ? (0, import_react42.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react42.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react42.createElement)("input", {
++ bot ? (0, import_react43.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react43.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
++ bot?.remote ? (0, import_react43.createElement)("div", { className: "bt-drawer-name" }, current) : room2?.origin === "face-to-face" ? (0, import_react43.createElement)("div", { className: "bt-drawer-name bt-drawer-name-face", title: current }, current) : editing ? (0, import_react43.createElement)("input", {
className: "bt-drawer-name-input",
autoFocus: true,
value: draftName,
@@ -17357,75 +17584,75 @@ diff --git a/client.js b/client.js
}
}
- }) : (0, import_react31.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => {
-+ }) : (0, import_react42.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => {
++ }) : (0, import_react43.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_react42.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react42.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null
++ subtitle ? (0, import_react43.createElement)("span", { className: "bt-drawer-role" }, subtitle) : bot?.remote ? (0, import_react43.createElement)("span", { className: "bt-drawer-role" }, t("Friend's Bot")) : null
),
- (0, import_react31.createElement)(
-+ bot?.remote ? null : (0, import_react42.createElement)(
++ bot?.remote ? null : (0, import_react43.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_react42.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
-+ tabs.map((key) => (0, import_react42.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key])))
++ (0, import_react43.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
++ tabs.map((key) => (0, import_react43.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_react42.createElement)(
++ (0, import_react43.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_react42.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
++ room2 && tab === "details" ? (0, import_react43.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
+ bot && bot.remote && tab === "details" ? [
-+ bot.remote.handle ? (0, import_react42.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null,
-+ (0, import_react42.createElement)(FlyBotDetails, { key: "social", bot, roster, actions })
++ bot.remote.handle ? (0, import_react43.createElement)("div", { key: "handle", className: "bt-muted" }, bot.remote.handle) : null,
++ (0, import_react43.createElement)(FlyBotDetails, { key: "social", bot, roster, actions })
+ ] : null,
+ bot && !bot.remote && tab === "details" ? [
-+ (0, import_react42.createElement)(
++ (0, import_react43.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_react42.createElement)("div", { className: "bt-section-title" }, t("Model")),
-+ (0, import_react42.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
-+ modelOf(bot, roster) ? null : (0, import_react42.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name }))
++ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Model")),
++ (0, import_react43.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
++ modelOf(bot, roster) ? null : (0, import_react43.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_react42.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
-+ (0, import_react42.createElement)(
++ (0, import_react43.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
++ (0, import_react43.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_react42.createElement)("div", { className: "bt-section-title" }, t("Working folder")),
-+ (0, import_react42.createElement)(FolderMenu, { value: bot.workDir, actions, run, wide: true, onPick: (workDir) => run(actions.updateBot(bot.id, { workDir })) })
++ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Working folder")),
++ (0, import_react43.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_react42.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react42.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
-+ roster.memory === false ? null : (0, import_react42.createElement)(MemoryRow, { key: "memory", bot, actions }),
-+ usage ? (0, import_react42.createElement)(
++ bot.instructions ? (0, import_react43.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react43.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
++ roster.memory === false ? null : (0, import_react43.createElement)(MemoryRow, { key: "memory", bot, actions }),
++ usage ? (0, import_react43.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_react42.createElement)("div", { className: "bt-section-title" }, t("Usage")),
-+ (0, import_react42.createElement)("div", { className: "bt-kv" }, (0, import_react42.createElement)("span", null, bot.name), (0, import_react42.createElement)("span", null, usage))
++ (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Usage")),
++ (0, import_react43.createElement)("div", { className: "bt-kv" }, (0, import_react43.createElement)("span", null, bot.name), (0, import_react43.createElement)("span", null, usage))
) : null,
- (0, import_react31.createElement)(ScheduleSummary, { key: "routines", bot, actions })
-+ (0, import_react42.createElement)(ScheduleSummary, { key: "routines", bot, actions })
++ (0, import_react43.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_react42.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
++ bot && !bot.remote && tab === "browser" ? (0, import_react43.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
)
];
}
@@ -17433,8 +17660,8 @@ diff --git a/client.js b/client.js
// src/client/browser-pane.js
-var import_react32 = require("react");
-var import_react_dom8 = require("react-dom");
-+var import_react43 = require("react");
-+var import_react_dom9 = require("react-dom");
++var import_react44 = require("react");
++var import_react_dom10 = require("react-dom");
var PANEL_LINGER_MS = 300;
var WIDTH_KEY = STORAGE.browserWidth;
-var DEFAULT_WIDTH = 520;
@@ -17450,11 +17677,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_react43.createElement)(
++ return (0, import_react44.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_react43.createElement)("path", { key: index, d }))
++ paths.map((d, index) => (0, import_react44.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"]);
@@ -17477,9 +17704,9 @@ diff --git a/client.js b/client.js
- const [failed, setFailed] = (0, import_react32.useState)(null);
- if (!src || failed === src) return (0, import_react32.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react32.createElement)(GlobeIcon));
- return (0, import_react32.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
-+ const [failed, setFailed] = (0, import_react43.useState)(null);
-+ if (!src || failed === src) return (0, import_react43.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react43.createElement)(GlobeIcon));
-+ return (0, import_react43.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
++ const [failed, setFailed] = (0, import_react44.useState)(null);
++ if (!src || failed === src) return (0, import_react44.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react44.createElement)(GlobeIcon));
++ return (0, import_react44.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
}
var savedWidth = () => {
try {
@@ -17505,11 +17732,11 @@ diff --git a/client.js b/client.js
- const spot = (0, import_react32.useRef)(null);
- const under = (0, import_react32.useRef)(null);
- (0, import_react32.useLayoutEffect)(() => {
-+ const wrap = (0, import_react43.useRef)(null);
-+ const ind = (0, import_react43.useRef)(null);
-+ const spot = (0, import_react43.useRef)(null);
-+ const under = (0, import_react43.useRef)(null);
-+ (0, import_react43.useLayoutEffect)(() => {
++ const wrap = (0, import_react44.useRef)(null);
++ const ind = (0, import_react44.useRef)(null);
++ const spot = (0, import_react44.useRef)(null);
++ const under = (0, import_react44.useRef)(null);
++ (0, import_react44.useLayoutEffect)(() => {
const box = wrap.current;
const capsule = ind.current;
if (!box || !capsule) return void 0;
@@ -17518,7 +17745,7 @@ diff --git a/client.js b/client.js
};
});
- (0, import_react32.useEffect)(() => {
-+ (0, import_react43.useEffect)(() => {
++ (0, import_react44.useEffect)(() => {
const box = wrap.current;
if (!box) return void 0;
const observer = new ResizeObserver(() => {
@@ -17527,15 +17754,15 @@ diff --git a/client.js b/client.js
return () => observer.disconnect();
}, []);
- return (0, import_react32.createElement)(
-+ return (0, import_react43.createElement)(
++ return (0, import_react44.createElement)(
"div",
{ className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") },
- (0, import_react32.createElement)(
-+ (0, import_react43.createElement)(
++ (0, import_react44.createElement)(
"div",
{ className: "bt-ptabs-in", ref: wrap },
- (0, import_react32.createElement)(
-+ (0, import_react43.createElement)(
++ (0, import_react44.createElement)(
"button",
{
type: "button",
@@ -17545,14 +17772,14 @@ diff --git a/client.js b/client.js
},
- (0, import_react32.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
- view === "details" ? (0, import_react32.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null
-+ (0, import_react43.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
-+ view === "details" ? (0, import_react43.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null
++ (0, import_react44.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
++ view === "details" ? (0, import_react44.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_react43.createElement)(
++ return (0, import_react44.createElement)(
"button",
{
key: tabId,
@@ -17563,9 +17790,9 @@ diff --git a/client.js b/client.js
- (0, import_react32.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
- (0, import_react32.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
- (0, import_react32.createElement)("span", {
-+ (0, import_react43.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
-+ (0, import_react43.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
-+ (0, import_react43.createElement)("span", {
++ (0, import_react44.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
++ (0, import_react44.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
++ (0, import_react44.createElement)("span", {
className: "bt-ptab-x",
role: "button",
"aria-label": t("Close tab"),
@@ -17574,13 +17801,13 @@ diff --git a/client.js b/client.js
);
}),
- (0, import_react32.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true })
-+ (0, import_react43.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true })
++ (0, import_react44.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_react43.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react43.createElement)(PlusTabIcon)),
-+ (0, import_react43.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_react43.createElement)(CloseIcon2))
++ (0, import_react44.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react44.createElement)(PlusTabIcon)),
++ (0, import_react44.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_react44.createElement)(CloseIcon2))
);
}
function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) {
@@ -17588,12 +17815,12 @@ diff --git a/client.js b/client.js
- const frame = (0, import_react32.useRef)(null);
- const input = (0, import_react32.useRef)(null);
- const [draft, setDraft] = (0, import_react32.useState)(null);
-+ const frame = (0, import_react43.useRef)(null);
-+ const input = (0, import_react43.useRef)(null);
-+ const [draft, setDraft] = (0, import_react43.useState)(null);
++ const frame = (0, import_react44.useRef)(null);
++ const input = (0, import_react44.useRef)(null);
++ const [draft, setDraft] = (0, import_react44.useState)(null);
const showGuest = landed && !leaving && Boolean(page.url) && !page.error;
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react43.useLayoutEffect)(() => {
++ (0, import_react44.useLayoutEffect)(() => {
const element = frame.current;
if (!element || leaving || !tabId) {
void actions.browserShow(null);
@@ -17602,7 +17829,7 @@ diff --git a/client.js b/client.js
};
}, [tabId, showGuest, leaving]);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react43.useEffect)(() => {
++ (0, import_react44.useEffect)(() => {
setDraft(null);
if (landed && !page.url) input.current?.focus();
}, [tabId, landed]);
@@ -17622,19 +17849,19 @@ diff --git a/client.js b/client.js
const editing = draft !== null;
const secure = /^https:/i.test(page.url);
- return (0, import_react32.createElement)(
-+ return (0, import_react43.createElement)(
++ return (0, import_react44.createElement)(
"div",
{ className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) },
- (0, import_react32.createElement)(
-+ (0, import_react43.createElement)(
++ (0, import_react44.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_react43.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_react43.createElement)(BackIcon)),
-+ (0, import_react43.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_react43.createElement)(ForwardIcon)),
-+ (0, import_react43.createElement)("button", {
++ (0, import_react44.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_react44.createElement)(BackIcon)),
++ (0, import_react44.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_react44.createElement)(ForwardIcon)),
++ (0, import_react44.createElement)("button", {
type: "button",
className: "bt-icon-btn",
disabled: !page.url,
@@ -17643,14 +17870,14 @@ diff --git a/client.js b/client.js
onClick: () => actions.browserCommand(tabId, page.loading ? "stop" : "reload")
- }, (0, import_react32.createElement)(page.loading ? StopIcon : ReloadIcon)),
- (0, import_react32.createElement)(
-+ }, (0, import_react43.createElement)(page.loading ? StopIcon : ReloadIcon)),
-+ (0, import_react43.createElement)(
++ }, (0, import_react44.createElement)(page.loading ? StopIcon : ReloadIcon)),
++ (0, import_react44.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_react43.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react43.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
-+ (0, import_react43.createElement)("input", {
++ (0, import_react44.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react44.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
++ (0, import_react44.createElement)("input", {
ref: input,
type: "text",
spellCheck: false,
@@ -17659,34 +17886,34 @@ diff --git a/client.js b/client.js
})
),
- page.loading ? (0, import_react32.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
-+ page.loading ? (0, import_react43.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
++ page.loading ? (0, import_react44.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
),
- (0, import_react32.createElement)(
-+ (0, import_react43.createElement)(
++ (0, import_react44.createElement)(
"div",
{ ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 },
- page.error ? (0, import_react32.createElement)(
-+ page.error ? (0, import_react43.createElement)(
++ page.error ? (0, import_react44.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_react43.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
-+ (0, import_react43.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
-+ page.url ? (0, import_react43.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => {
++ (0, import_react44.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
++ (0, import_react44.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
++ page.url ? (0, import_react44.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_react43.createElement)(
++ ) : !page.url ? (0, import_react44.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_react43.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }),
-+ (0, import_react43.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
-+ (0, import_react43.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_react44.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }),
++ (0, import_react44.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
++ (0, import_react44.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
)
);
@@ -17702,36 +17929,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_react43.useRef)(null);
-+ const fly = (0, import_react43.useRef)(null);
-+ const [landed, setLanded] = (0, import_react43.useState)(false);
-+ const [settling, setSettling] = (0, import_react43.useState)(false);
-+ const [width, setWidth] = (0, import_react43.useState)(savedWidth);
-+ const [viewport, setViewport] = (0, import_react43.useState)(() => window.innerWidth);
-+ const [dragging, setDragging] = (0, import_react43.useState)(false);
-+ const [resizing, setResizing] = (0, import_react43.useState)(false);
++ const root = (0, import_react44.useRef)(null);
++ const fly = (0, import_react44.useRef)(null);
++ const [landed, setLanded] = (0, import_react44.useState)(false);
++ const [settling, setSettling] = (0, import_react44.useState)(false);
++ const [width, setWidth] = (0, import_react44.useState)(savedWidth);
++ const [viewport, setViewport] = (0, import_react44.useState)(() => window.innerWidth);
++ const [dragging, setDragging] = (0, import_react44.useState)(false);
++ const [resizing, setResizing] = (0, import_react44.useState)(false);
const instant = dragging || resizing;
- const [reading, setReading] = (0, import_react32.useState)(false);
-+ const [reading, setReading] = (0, import_react43.useState)(false);
++ const [reading, setReading] = (0, import_react44.useState)(false);
const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport));
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react43.useLayoutEffect)(() => {
++ (0, import_react44.useLayoutEffect)(() => {
if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH);
}, [view, width]);
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react43.useLayoutEffect)(() => {
++ (0, import_react44.useLayoutEffect)(() => {
chatRoom.hold(instant);
}, [instant]);
- const [last, setLast] = (0, import_react32.useState)({ view, width: panelWidth });
-+ const [last, setLast] = (0, import_react43.useState)({ view, width: panelWidth });
++ const [last, setLast] = (0, import_react44.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_react43.useRef)(view);
-+ (0, import_react43.useLayoutEffect)(() => {
++ const shownView = (0, import_react44.useRef)(view);
++ (0, import_react44.useLayoutEffect)(() => {
const switched = settling && shownView.current !== view;
shownView.current = view;
if (switched) chatRoom.hold(true);
@@ -17739,12 +17966,12 @@ diff --git a/client.js b/client.js
if (switched) chatRoom.hold(false);
}, [view, panelWidth, leaving]);
- (0, import_react32.useEffect)(() => () => {
-+ (0, import_react43.useEffect)(() => () => {
++ (0, import_react44.useEffect)(() => () => {
chatRoom.set(null);
chatRoom.hold(false);
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react43.useEffect)(() => {
++ (0, import_react44.useEffect)(() => {
let timer;
const onResize = () => {
setResizing(true);
@@ -17753,34 +17980,34 @@ diff --git a/client.js b/client.js
};
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react43.useEffect)(() => {
++ (0, import_react44.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_react43.useEffect)(() => {
++ (0, import_react44.useEffect)(() => {
const timer = setTimeout(() => setLanded(true), 360);
return () => clearTimeout(timer);
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react43.useEffect)(() => {
++ (0, import_react44.useEffect)(() => {
if (canBrowse) actions.browserEnsure(bot.id);
}, [bot?.id, canBrowse]);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react43.useEffect)(() => {
++ (0, import_react44.useEffect)(() => {
if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details");
}, [view, active, tabState.ids.length]);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react43.useEffect)(() => {
++ (0, import_react44.useEffect)(() => {
if (view !== "browser" || leaving) void actions.browserShow?.(null);
}, [view, leaving]);
- (0, import_react32.useEffect)(() => () => {
-+ (0, import_react43.useEffect)(() => () => {
++ (0, import_react44.useEffect)(() => () => {
void actions.browserShow?.(null);
}, []);
- (0, import_react32.useEffect)(() => {
-+ (0, import_react43.useEffect)(() => {
++ (0, import_react44.useEffect)(() => {
if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0;
setReading(true);
const timer = setTimeout(() => setReading(false), READ_FLASH_MS);
@@ -17789,7 +18016,7 @@ diff --git a/client.js b/client.js
};
}
- (0, import_react32.useLayoutEffect)(() => {
-+ (0, import_react43.useLayoutEffect)(() => {
++ (0, import_react44.useLayoutEffect)(() => {
const move = fly.current;
if (!move) return;
fly.current = null;
@@ -17798,7 +18025,7 @@ diff --git a/client.js b/client.js
};
const hasStrip = canBrowse && tabState.ids.length > 0;
- return (0, import_react32.createElement)(
-+ return (0, import_react43.createElement)(
++ return (0, import_react44.createElement)(
"aside",
{
ref: root,
@@ -17807,19 +18034,19 @@ diff --git a/client.js b/client.js
}
},
- leaving ? null : (0, import_react32.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }),
-+ leaving ? null : (0, import_react43.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }),
++ leaving ? null : (0, import_react44.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_dom9.createPortal)((0, import_react43.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
-+ hasStrip ? (0, import_react43.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
-+ view === "browser" && bot ? (0, import_react43.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react43.createElement)(
++ dragging ? (0, import_react_dom10.createPortal)((0, import_react44.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
++ hasStrip ? (0, import_react44.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
++ view === "browser" && bot ? (0, import_react44.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react44.createElement)(
"div",
{ key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" },
- (0, import_react32.createElement)(DetailsDrawer, {
-+ (0, import_react43.createElement)(DetailsDrawer, {
++ (0, import_react44.createElement)(DetailsDrawer, {
sessionId: pane.id,
startRenaming: pane.renaming,
startTab: pane.tab,
@@ -17828,7 +18055,7 @@ diff --git a/client.js b/client.js
`;
+// src/client/remote-bot-settings.js
-+var import_react44 = require("react");
++var import_react45 = require("react");
+var REMOTE_BOT_SETTINGS_CSS = `
+.bt-remote-sheet-layer{position:fixed;inset:0;z-index:120;isolation:isolate;display:flex;align-items:flex-end;justify-content:center;padding:16px;pointer-events:none}
+.bt-remote-sheet-layer[data-leaving]{pointer-events:none}
@@ -17852,8 +18079,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_react44.useRef)(null);
-+ (0, import_react44.useEffect)(() => {
++ const dialog = (0, import_react45.useRef)(null);
++ (0, import_react45.useEffect)(() => {
+ const before = document.activeElement;
+ dialog.current?.focus?.({ preventScroll: true });
+ return () => {
@@ -17868,30 +18095,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_react44.createElement)(
++ return (0, import_react45.createElement)(
+ "div",
+ { className: "bt-remote-sheet-layer", "data-leaving": leaving || void 0, "data-flybot": "remote-bot-sheet" },
-+ (0, import_react44.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
-+ (0, import_react44.createElement)(
++ (0, import_react45.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
++ (0, import_react45.createElement)(
+ "div",
+ { className: "bt-remote-sheet", role: "dialog", "aria-modal": true, "aria-label": t("Bot settings"), tabIndex: -1, ref: dialog, onKeyDown },
-+ (0, import_react44.createElement)(
++ (0, import_react45.createElement)(
+ "div",
+ { className: "bt-remote-sheet-head" },
-+ (0, import_react44.createElement)(BotMark, { bot, size: 40, live: true }),
-+ (0, import_react44.createElement)(
++ (0, import_react45.createElement)(BotMark, { bot, size: 40, live: true }),
++ (0, import_react45.createElement)(
+ "div",
+ { className: "bt-remote-sheet-who" },
-+ (0, import_react44.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name),
-+ (0, import_react44.createElement)("span", { className: "bt-remote-sheet-sub" }, who)
++ (0, import_react45.createElement)("span", { className: "bt-remote-sheet-name" }, bot.name),
++ (0, import_react45.createElement)("span", { className: "bt-remote-sheet-sub" }, who)
+ ),
-+ (0, import_react44.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react44.createElement)(CloseIcon))
++ (0, import_react45.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close settings"), onClick: close }, (0, import_react45.createElement)(CloseIcon))
+ ),
-+ (0, import_react44.createElement)(
++ (0, import_react45.createElement)(
+ "div",
+ { className: "bt-remote-sheet-body" },
-+ bot.remote.handle ? (0, import_react44.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null,
-+ (0, import_react44.createElement)(FlyBotDetails, { bot, roster, actions })
++ bot.remote.handle ? (0, import_react45.createElement)("div", { className: "bt-muted", style: { marginBottom: 8, fontSize: 12 } }, bot.remote.handle) : null,
++ (0, import_react45.createElement)(FlyBotDetails, { bot, roster, actions })
+ )
+ )
+ );
@@ -17900,8 +18127,8 @@ diff --git a/client.js b/client.js
// src/client/overlays.js
-var import_react33 = require("react");
-var import_react_dom9 = require("react-dom");
-+var import_react45 = require("react");
-+var import_react_dom10 = require("react-dom");
++var import_react46 = require("react");
++var import_react_dom11 = require("react-dom");
var DIALOG_EXIT_MS = 160;
var MENU_EXIT_MS3 = 120;
function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useWork, useRegistry, useBrowserPages, useBrowserTabs, useUpdate, actions }) {
@@ -17911,8 +18138,8 @@ diff --git a/client.js b/client.js
const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null);
- const previous = (0, import_react33.useRef)(current);
- (0, import_react33.useEffect)(() => {
-+ const previous = (0, import_react45.useRef)(current);
-+ (0, import_react45.useEffect)(() => {
++ const previous = (0, import_react46.useRef)(current);
++ (0, import_react46.useEffect)(() => {
if (previous.current === current) return;
previous.current = current;
if (current) actions.followPanel?.(current);
@@ -17920,8 +18147,8 @@ diff --git a/client.js b/client.js
const open = ui2.details ? { id: ui2.details, view: ui2.view ?? "details", renaming: ui2.renaming, tab: ui2.tab } : null;
- const [kept, setKept] = (0, import_react33.useState)(open);
- (0, import_react33.useEffect)(() => {
-+ const [kept, setKept] = (0, import_react45.useState)(open);
-+ (0, import_react45.useEffect)(() => {
++ const [kept, setKept] = (0, import_react46.useState)(open);
++ (0, import_react46.useEffect)(() => {
if (open) {
setKept(open);
return void 0;
@@ -17937,8 +18164,8 @@ diff --git a/client.js b/client.js
- import_react33.Fragment,
+ const [inbox, inboxLeaving] = useLinger(ui2.inbox ?? null, DIALOG_EXIT_MS);
+ const [board, boardLeaving] = useLinger(ui2.board ?? null, DIALOG_EXIT_MS);
-+ return (0, import_react45.createElement)(
-+ import_react45.Fragment,
++ return (0, import_react46.createElement)(
++ import_react46.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,
@@ -17948,17 +18175,17 @@ diff --git a/client.js b/client.js
- menu ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null,
- settings ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
- memory && roster.byId[memory] ? (0, import_react_dom9.createPortal)((0, import_react33.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null
-+ pane ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null,
-+ ui2.newChat ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null,
-+ ui2.palette ? (0, import_react_dom10.createPortal)((0, import_react45.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_dom10.createPortal)((0, import_react45.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null,
-+ ui2.voice ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
-+ menu ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null,
-+ settings ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
-+ remoteBotSettings ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null,
-+ memory && roster.byId[memory] ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
-+ inbox ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null,
-+ board ? (0, import_react_dom10.createPortal)((0, import_react45.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null
++ pane ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null,
++ ui2.newChat ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null,
++ ui2.palette ? (0, import_react_dom11.createPortal)((0, import_react46.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_dom11.createPortal)((0, import_react46.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null,
++ ui2.voice ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
++ menu ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null,
++ settings ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
++ remoteBotSettings ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(RemoteBotSettingsSheet, { key: remoteBotSettings, bot: roster.byId[remoteBotSettings], roster, actions, leaving: remoteLeaving }), document.body) : null,
++ memory && roster.byId[memory] ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null,
++ inbox ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(InboxDialog, { roster, useWork, useSessions, actions, leaving: inboxLeaving }), document.body) : null,
++ board ? (0, import_react_dom11.createPortal)((0, import_react46.createElement)(KanbanDialog, { roster, actions, leaving: boardLeaving }), document.body) : null
);
}
@@ -18215,7 +18442,7 @@ diff --git a/client.js b/client.js
+}
+
+// src/client/project-board.js
-+var import_react46 = require("react");
++var import_react47 = require("react");
+var PROJECT_BOARD_CSS = `
+/* #1750: the board was a short dialog in the page flow — no stylesheet defines 'bt-overlay', so the
+ bot tree and workflows stayed legible under its rows. It is a modal now, on the same tier as the
@@ -18388,14 +18615,14 @@ diff --git a/client.js b/client.js
+ };
+}
+function ProjectBoardRows({ view, onOpen }) {
-+ if (view.empty) return (0, import_react46.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText));
-+ return view.groups.map((group) => (0, import_react46.createElement)(
++ if (view.empty) return (0, import_react47.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText));
++ return view.groups.map((group) => (0, import_react47.createElement)(
+ "section",
+ { key: group.source, className: "bt-project-group", "data-source": group.source },
-+ (0, import_react46.createElement)("h4", null, t(group.label)),
++ (0, import_react47.createElement)("h4", null, t(group.label)),
+ group.rows.map((row) => {
+ const open = row.board === null ? void 0 : () => onOpen?.(row.board);
-+ return (0, import_react46.createElement)(
++ return (0, import_react47.createElement)(
+ "div",
+ {
+ key: row.key,
@@ -18410,17 +18637,17 @@ diff --git a/client.js b/client.js
+ if (event.key === "Enter" || event.key === " ") open();
+ }
+ },
-+ (0, import_react46.createElement)("span", { className: "bt-project-column" }, t(row.column)),
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)("span", { className: "bt-project-column" }, t(row.column)),
++ (0, import_react47.createElement)(
+ "span",
+ { className: "bt-project-title-wrap" },
-+ (0, import_react46.createElement)("span", { className: "bt-project-title" }, row.title ?? (row.assignee ? `@${row.assignee}` : "—")),
++ (0, import_react47.createElement)("span", { className: "bt-project-title" }, row.title ?? (row.assignee ? `@${row.assignee}` : "—")),
+ // The audit text the row already carries (#1730): shown inside the card detail, never a
+ // second read. A row without one renders exactly as before.
-+ row.audit === null ? null : (0, import_react46.createElement)("span", { className: "bt-project-audit" }, row.audit)
++ row.audit === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-audit" }, row.audit)
+ ),
-+ row.priority === null ? null : (0, import_react46.createElement)("span", { className: "bt-project-priority" }, row.priority),
-+ row.runId === null ? null : (0, import_react46.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`)
++ row.priority === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-priority" }, row.priority),
++ row.runId === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`)
+ );
+ })
+ ));
@@ -18434,14 +18661,14 @@ diff --git a/client.js b/client.js
+ return `${Math.floor(hours / 24)}d`;
+}
+function ProjectQueueGroups({ view, onOpen }) {
-+ return view.queues.map((entry) => (0, import_react46.createElement)(
++ return view.queues.map((entry) => (0, import_react47.createElement)(
+ "section",
+ { key: `queue:${entry.group}`, className: "bt-project-group", "data-queue": entry.group },
-+ (0, import_react46.createElement)("h4", null, t(entry.label)),
++ (0, import_react47.createElement)("h4", null, t(entry.label)),
+ entry.items.map((row) => {
+ const open = row.board === null ? void 0 : () => onOpen?.(row.board);
-+ const note = entry.group === "waiting" ? (0, import_react46.createElement)("span", { className: "bt-project-wait" }, [row.role === null ? null : (0, import_react46.createElement)("span", { key: "role", className: "bt-project-role" }, row.role), waitedLabel(row.waitedMs) === null ? null : ` · ${waitedLabel(row.waitedMs)}`]) : entry.group === "inflight" ? (0, import_react46.createElement)("span", { className: "bt-project-state" }, row.holder === null ? t(row.state) : `${row.holder} · ${t(row.state)}`) : (0, import_react46.createElement)("span", { className: "bt-project-reason" }, row.reason);
-+ return (0, import_react46.createElement)(
++ const note = entry.group === "waiting" ? (0, import_react47.createElement)("span", { className: "bt-project-wait" }, [row.role === null ? null : (0, import_react47.createElement)("span", { key: "role", className: "bt-project-role" }, row.role), waitedLabel(row.waitedMs) === null ? null : ` · ${waitedLabel(row.waitedMs)}`]) : entry.group === "inflight" ? (0, import_react47.createElement)("span", { className: "bt-project-state" }, row.holder === null ? t(row.state) : `${row.holder} · ${t(row.state)}`) : (0, import_react47.createElement)("span", { className: "bt-project-reason" }, row.reason);
++ return (0, import_react47.createElement)(
+ "div",
+ {
+ key: row.key,
@@ -18455,10 +18682,10 @@ diff --git a/client.js b/client.js
+ if (event.key === "Enter" || event.key === " ") open();
+ }
+ },
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)(
+ "span",
+ { className: "bt-project-title-wrap" },
-+ (0, import_react46.createElement)("span", { className: "bt-project-title" }, row.title ?? `#${row.id}`)
++ (0, import_react47.createElement)("span", { className: "bt-project-title" }, row.title ?? `#${row.id}`)
+ ),
+ note
+ );
@@ -18468,77 +18695,77 @@ diff --git a/client.js b/client.js
+var countText = (value) => value === null ? t("unknown") : String(value);
+function ProjectBoardRecord({ record }) {
+ const { decisions, meetings } = record;
-+ const decisionRows = decisions.files.map((file) => (0, import_react46.createElement)(
++ const decisionRows = decisions.files.map((file) => (0, import_react47.createElement)(
+ "div",
+ { key: file.key, className: "bt-project-decision", "data-path": file.path, "data-sha256": file.sha256 },
-+ (0, import_react46.createElement)("span", { className: "bt-project-date" }, file.date),
-+ (0, import_react46.createElement)("span", { className: "bt-project-title" }, file.title === "" ? file.name : file.title),
-+ (0, import_react46.createElement)("span", { className: "bt-project-hash", title: file.sha256 }, file.shortHash)
++ (0, import_react47.createElement)("span", { className: "bt-project-date" }, file.date),
++ (0, import_react47.createElement)("span", { className: "bt-project-title" }, file.title === "" ? file.name : file.title),
++ (0, import_react47.createElement)("span", { className: "bt-project-hash", title: file.sha256 }, file.shortHash)
+ ));
-+ const decidedRows = meetings.decided.map((meeting) => (0, import_react46.createElement)(
++ const decidedRows = meetings.decided.map((meeting) => (0, import_react47.createElement)(
+ "div",
+ { key: meeting.key, className: "bt-project-meeting", "data-status": "decided" },
-+ (0, import_react46.createElement)("span", { className: "bt-project-date" }, meeting.date),
-+ (0, import_react46.createElement)("span", { className: "bt-project-title" }, meeting.question),
-+ meeting.owner === null ? null : (0, import_react46.createElement)("span", { className: "bt-project-owner" }, `@${meeting.owner}`),
++ (0, import_react47.createElement)("span", { className: "bt-project-date" }, meeting.date),
++ (0, import_react47.createElement)("span", { className: "bt-project-title" }, meeting.question),
++ meeting.owner === null ? null : (0, import_react47.createElement)("span", { className: "bt-project-owner" }, `@${meeting.owner}`),
+ // A decided meeting whose file is gone still reads as decided; the missing path says so
+ // instead of the row disappearing with the file.
-+ meeting.path === "" ? (0, import_react46.createElement)("span", { className: "bt-project-missing" }, "decision file gone") : (0, import_react46.createElement)("span", { className: "bt-project-path" }, meeting.path)
++ meeting.path === "" ? (0, import_react47.createElement)("span", { className: "bt-project-missing" }, "decision file gone") : (0, import_react47.createElement)("span", { className: "bt-project-path" }, meeting.path)
+ ));
-+ const pendingRows = meetings.pending.map((meeting) => (0, import_react46.createElement)(
++ const pendingRows = meetings.pending.map((meeting) => (0, import_react47.createElement)(
+ "div",
+ { key: meeting.key, className: "bt-project-meeting", "data-status": meeting.status },
-+ (0, import_react46.createElement)("span", { className: "bt-project-date" }, meeting.date),
-+ (0, import_react46.createElement)("span", { className: "bt-project-title" }, meeting.blockingQuestion === null ? meeting.question : meeting.blockingQuestion),
-+ (0, import_react46.createElement)("span", { className: "bt-project-status" }, meeting.status)
++ (0, import_react47.createElement)("span", { className: "bt-project-date" }, meeting.date),
++ (0, import_react47.createElement)("span", { className: "bt-project-title" }, meeting.blockingQuestion === null ? meeting.question : meeting.blockingQuestion),
++ (0, import_react47.createElement)("span", { className: "bt-project-status" }, meeting.status)
+ ));
-+ return (0, import_react46.createElement)(
++ return (0, import_react47.createElement)(
+ "section",
+ { className: "bt-project-record", "data-decisions": String(decisions.count), "data-meetings": String(meetings.count) },
-+ (0, import_react46.createElement)("h4", null, t("Decisions")),
-+ decisions.empty ? (0, import_react46.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(decisions.emptyText)) : decisionRows,
-+ (0, import_react46.createElement)("h4", null, t("Meetings")),
-+ meetings.empty ? (0, import_react46.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(meetings.emptyText)) : decidedRows,
-+ meetings.pending.length === 0 ? null : (0, import_react46.createElement)("h5", null, t("Not decided")),
++ (0, import_react47.createElement)("h4", null, t("Decisions")),
++ decisions.empty ? (0, import_react47.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(decisions.emptyText)) : decisionRows,
++ (0, import_react47.createElement)("h4", null, t("Meetings")),
++ meetings.empty ? (0, import_react47.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(meetings.emptyText)) : decidedRows,
++ meetings.pending.length === 0 ? null : (0, import_react47.createElement)("h5", null, t("Not decided")),
+ meetings.pending.length === 0 ? null : pendingRows
+ );
+}
+function ProjectBoardPane({ project, rows, canManage, tasks, decisions, meetings, queues, onOpen, onSettings, onMembers }) {
+ const view = projectBoardView({ project, rows, canManage, tasks, decisions, meetings, queues });
-+ return (0, import_react46.createElement)(
++ return (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-board", "data-can-manage": view.canManage ? "true" : "false" },
-+ (0, import_react46.createElement)("h3", null, view.title),
++ (0, import_react47.createElement)("h3", null, view.title),
+ // Header counts (#1768): four numbers read from the host, an unreadable one shown as unknown.
-+ view.counts === null ? null : (0, import_react46.createElement)(
++ view.counts === null ? null : (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-counts", "data-counts": "waiting,inflight,blocked,done" },
-+ (0, import_react46.createElement)("span", null, [t("Waiting"), ": ", (0, import_react46.createElement)("b", { key: "w" }, countText(view.counts.waiting))]),
-+ (0, import_react46.createElement)("span", null, [t("In flight"), ": ", (0, import_react46.createElement)("b", { key: "f" }, countText(view.counts.inflight))]),
-+ (0, import_react46.createElement)("span", null, [t("Blocked"), ": ", (0, import_react46.createElement)("b", { key: "b" }, countText(view.counts.blocked))]),
-+ (0, import_react46.createElement)("span", null, [t("Done"), ": ", (0, import_react46.createElement)("b", { key: "d" }, countText(view.counts.done))])
++ (0, import_react47.createElement)("span", null, [t("Waiting"), ": ", (0, import_react47.createElement)("b", { key: "w" }, countText(view.counts.waiting))]),
++ (0, import_react47.createElement)("span", null, [t("In flight"), ": ", (0, import_react47.createElement)("b", { key: "f" }, countText(view.counts.inflight))]),
++ (0, import_react47.createElement)("span", null, [t("Blocked"), ": ", (0, import_react47.createElement)("b", { key: "b" }, countText(view.counts.blocked))]),
++ (0, import_react47.createElement)("span", null, [t("Done"), ": ", (0, import_react47.createElement)("b", { key: "d" }, countText(view.counts.done))])
+ ),
+ // #1749: both buttons open a panel and neither is hidden from a non-owner. The host refuses
+ // every write a non-owner attempts, so the refusal is an answer the reader sees, not a
+ // control that silently disappeared.
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-actions" },
-+ (0, import_react46.createElement)("button", { type: "button", "data-action": "project-settings", onClick: () => onSettings?.() }, t("Project settings")),
-+ (0, import_react46.createElement)("button", { type: "button", "data-action": "project-members", onClick: () => onMembers?.() }, t("Members"))
++ (0, import_react47.createElement)("button", { type: "button", "data-action": "project-settings", onClick: () => onSettings?.() }, t("Project settings")),
++ (0, import_react47.createElement)("button", { type: "button", "data-action": "project-members", onClick: () => onMembers?.() }, t("Members"))
+ ),
-+ (0, import_react46.createElement)(ProjectBoardRows, { view, onOpen }),
-+ (0, import_react46.createElement)(ProjectQueueGroups, { view, onOpen }),
-+ (0, import_react46.createElement)(ProjectBoardRecord, { record: view.record })
++ (0, import_react47.createElement)(ProjectBoardRows, { view, onOpen }),
++ (0, import_react47.createElement)(ProjectQueueGroups, { view, onOpen }),
++ (0, import_react47.createElement)(ProjectBoardRecord, { record: view.record })
+ );
+}
+function ProjectSettingsPanel({ project, call: request, onClose, onSaved } = {}) {
+ const save = request ?? call;
-+ const [kind, setKind] = (0, import_react46.useState)("bot");
-+ const [memberId, setMemberId] = (0, import_react46.useState)("");
-+ const [note, setNote] = (0, import_react46.useState)("");
-+ const [assistantOff, setAssistantOff] = (0, import_react46.useState)(project?.assistant?.off === true);
-+ const [status, setStatus] = (0, import_react46.useState)("");
++ const [kind, setKind] = (0, import_react47.useState)("bot");
++ const [memberId, setMemberId] = (0, import_react47.useState)("");
++ const [note, setNote] = (0, import_react47.useState)("");
++ const [assistantOff, setAssistantOff] = (0, import_react47.useState)(project?.assistant?.off === true);
++ const [status, setStatus] = (0, import_react47.useState)("");
+ const id = text(project?.id);
+ const run = async (endpoint, payload, done) => {
+ try {
@@ -18551,57 +18778,57 @@ diff --git a/client.js b/client.js
+ return null;
+ }
+ };
-+ return (0, import_react46.createElement)(
++ return (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-panel", "data-panel": "project-settings" },
-+ (0, import_react46.createElement)("h3", null, t("Project settings")),
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)("h3", null, t("Project settings")),
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-field" },
-+ (0, import_react46.createElement)("label", { htmlFor: "bt-project-member-id" }, t("Bind a member to this Project")),
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)("label", { htmlFor: "bt-project-member-id" }, t("Bind a member to this Project")),
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-row-actions" },
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)(
+ "select",
+ { "aria-label": t("Member kind"), value: kind, onChange: (event) => setKind(event.target.value) },
-+ (0, import_react46.createElement)("option", { value: "bot" }, t("Bot")),
-+ (0, import_react46.createElement)("option", { value: "group" }, t("Group"))
++ (0, import_react47.createElement)("option", { value: "bot" }, t("Bot")),
++ (0, import_react47.createElement)("option", { value: "group" }, t("Group"))
+ ),
-+ (0, import_react46.createElement)("input", { id: "bt-project-member-id", value: memberId, placeholder: t("Member id"), onChange: (event) => setMemberId(event.target.value) }),
-+ (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-bind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: id }, t("Member bound.")) }, t("Bind")),
-+ (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-unbind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: null }, t("Member unbound.")) }, t("Unbind"))
++ (0, import_react47.createElement)("input", { id: "bt-project-member-id", value: memberId, placeholder: t("Member id"), onChange: (event) => setMemberId(event.target.value) }),
++ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-bind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: id }, t("Member bound.")) }, t("Bind")),
++ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-unbind", onClick: () => run("project-bind", { kind, id: text(memberId), projectId: null }, t("Member unbound.")) }, t("Unbind"))
+ )
+ ),
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-field" },
-+ (0, import_react46.createElement)("label", null, t("Project Assistant")),
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)("label", null, t("Project Assistant")),
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-row-actions" },
-+ (0, import_react46.createElement)("label", null, (0, import_react46.createElement)("input", { type: "checkbox", "data-action": "assistant-off", checked: assistantOff, onChange: (event) => setAssistantOff(event.target.checked) }), " ", t("Pause the daily digest")),
-+ (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-assistant-save", onClick: () => run("project-assistant-owner", { id, settings: { off: assistantOff === true } }, t("Assistant settings saved.")) }, t("Save"))
++ (0, import_react47.createElement)("label", null, (0, import_react47.createElement)("input", { type: "checkbox", "data-action": "assistant-off", checked: assistantOff, onChange: (event) => setAssistantOff(event.target.checked) }), " ", t("Pause the daily digest")),
++ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-assistant-save", onClick: () => run("project-assistant-owner", { id, settings: { off: assistantOff === true } }, t("Assistant settings saved.")) }, t("Save"))
+ )
+ ),
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-field" },
+ // The host knows two memory kinds; the free text the panel takes is recorded as a decision,
+ // never as a third kind the host would refuse (#1820).
-+ (0, import_react46.createElement)("label", { htmlFor: "bt-project-note" }, t("Add to the assistant memory")),
-+ (0, import_react46.createElement)("textarea", { id: "bt-project-note", value: note, onChange: (event) => setNote(event.target.value) }),
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)("label", { htmlFor: "bt-project-note" }, t("Add to the assistant memory")),
++ (0, import_react47.createElement)("textarea", { id: "bt-project-note", value: note, onChange: (event) => setNote(event.target.value) }),
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-row-actions" },
-+ (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-memory-save", onClick: () => run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")) }, t("Save memory"))
++ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "data-action": "project-memory-save", onClick: () => run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")) }, t("Save memory"))
+ )
+ ),
-+ (0, import_react46.createElement)("div", { className: "bt-project-status-line", role: "status" }, status),
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)("div", { className: "bt-project-status-line", role: "status" }, status),
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-row-actions" },
-+ (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board"))
++ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board"))
+ )
+ );
+}
@@ -18618,32 +18845,32 @@ diff --git a/client.js b/client.js
+ const name = text(row?.assignee);
+ if (name !== "" && !boardMembers.includes(name)) boardMembers.push(name);
+ }
-+ return (0, import_react46.createElement)(
++ return (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-panel", "data-panel": "project-members" },
-+ (0, import_react46.createElement)("h3", null, t("Members")),
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)("h3", null, t("Members")),
++ (0, import_react47.createElement)(
+ "ul",
+ { className: "bt-project-members" },
-+ owners.map((owner) => (0, import_react46.createElement)(
++ owners.map((owner) => (0, import_react47.createElement)(
+ "li",
+ { key: `owner:${owner}`, className: "bt-project-member", "data-member-role": "owner", "data-member-id": owner },
-+ (0, import_react46.createElement)("span", null, `@${owner}`),
-+ (0, import_react46.createElement)("span", { className: "bt-project-member-kind" }, t("Owner"))
++ (0, import_react47.createElement)("span", null, `@${owner}`),
++ (0, import_react47.createElement)("span", { className: "bt-project-member-kind" }, t("Owner"))
+ )),
-+ boardMembers.map((name) => (0, import_react46.createElement)(
++ boardMembers.map((name) => (0, import_react47.createElement)(
+ "li",
+ { key: `board:${name}`, className: "bt-project-member", "data-member-role": "board", "data-member-id": name },
-+ (0, import_react46.createElement)("span", null, `@${name}`),
-+ (0, import_react46.createElement)("span", { className: "bt-project-member-kind" }, t("On the board"))
++ (0, import_react47.createElement)("span", null, `@${name}`),
++ (0, import_react47.createElement)("span", { className: "bt-project-member-kind" }, t("On the board"))
+ ))
+ ),
-+ owners.length === 0 && boardMembers.length === 0 ? (0, import_react46.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null,
-+ (0, import_react46.createElement)("p", { className: "bt-project-note" }, t("Member management is not part of this slice; owners and the members their cards name are listed here.")),
-+ (0, import_react46.createElement)(
++ owners.length === 0 && boardMembers.length === 0 ? (0, import_react47.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null,
++ (0, import_react47.createElement)("p", { className: "bt-project-note" }, t("Member management is not part of this slice; owners and the members their cards name are listed here.")),
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-row-actions" },
-+ (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board"))
++ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board"))
+ )
+ );
+}
@@ -18651,15 +18878,15 @@ diff --git a/client.js b/client.js
+var boardPollPaused = () => typeof document !== "undefined" && document.visibilityState === "hidden" || typeof window !== "undefined" && !window.location.hash.startsWith("#project");
+function ProjectBoardOverlay({ actions, call: request } = {}) {
+ const read = request ?? call;
-+ const [state, setState] = (0, import_react46.useState)(null);
-+ const [panel, setPanel] = (0, import_react46.useState)(null);
-+ const [hash, setHash] = (0, import_react46.useState)(() => typeof window === "undefined" ? "" : window.location.hash);
-+ const close = (0, import_react46.useCallback)(() => {
++ const [state, setState] = (0, import_react47.useState)(null);
++ const [panel, setPanel] = (0, import_react47.useState)(null);
++ const [hash, setHash] = (0, import_react47.useState)(() => typeof window === "undefined" ? "" : window.location.hash);
++ const close = (0, import_react47.useCallback)(() => {
+ if (typeof window !== "undefined") window.location.hash = "";
+ setPanel(null);
+ setState(null);
+ }, []);
-+ (0, import_react46.useEffect)(() => {
++ (0, import_react47.useEffect)(() => {
+ if (typeof window === "undefined") return void 0;
+ const onKey = (event) => {
+ if (event.key !== "Escape") return;
@@ -18669,14 +18896,14 @@ diff --git a/client.js b/client.js
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, [panel, close]);
-+ (0, import_react46.useEffect)(() => {
++ (0, import_react47.useEffect)(() => {
+ if (typeof window === "undefined") return void 0;
+ const sync = () => setHash(window.location.hash);
+ window.addEventListener("hashchange", sync);
+ sync();
+ return () => window.removeEventListener("hashchange", sync);
+ }, []);
-+ (0, import_react46.useEffect)(() => {
++ (0, import_react47.useEffect)(() => {
+ if (typeof window === "undefined") return void 0;
+ if (!hash.startsWith("#project")) {
+ setState(null);
@@ -18716,32 +18943,32 @@ diff --git a/client.js b/client.js
+ };
+ }, [hash]);
+ if (state === null) return null;
-+ return (0, import_react46.createElement)(
++ return (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-overlay" },
-+ (0, import_react46.createElement)("div", { className: "bt-scrim", onClick: close }),
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)("div", { className: "bt-scrim", onClick: close }),
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-mem bt-project-dialog", role: "dialog", "aria-modal": true, "aria-label": t("Project board") },
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-mem-head" },
-+ (0, import_react46.createElement)("h2", {}, t("Project board")),
-+ (0, import_react46.createElement)("span", { className: "bt-mem-head-gap" }),
-+ (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-project-close", autoFocus: true, onClick: close }, t("Close"))
++ (0, import_react47.createElement)("h2", {}, t("Project board")),
++ (0, import_react47.createElement)("span", { className: "bt-mem-head-gap" }),
++ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-project-close", autoFocus: true, onClick: close }, t("Close"))
+ ),
-+ (0, import_react46.createElement)(
++ (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-body" },
-+ state.project === null ? (0, import_react46.createElement)(
++ state.project === null ? (0, import_react47.createElement)(
+ "div",
+ { className: "bt-project-empty" },
-+ (0, import_react46.createElement)("p", null, t("No Project yet — create one.")),
-+ (0, import_react46.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
++ (0, import_react47.createElement)("p", null, t("No Project yet — create one.")),
++ (0, import_react47.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => {
+ close();
+ actions?.openSettings?.("projects");
+ } }, t("Open Projects in Settings"))
-+ ) : panel === "settings" ? (0, import_react46.createElement)(ProjectSettingsPanel, { project: state.project, call: read, onClose: () => setPanel(null) }) : panel === "members" ? (0, import_react46.createElement)(ProjectMembersPanel, { project: state.project, rows: state.rows, onClose: () => setPanel(null) }) : (0, import_react46.createElement)(ProjectBoardPane, {
++ ) : panel === "settings" ? (0, import_react47.createElement)(ProjectSettingsPanel, { project: state.project, call: read, onClose: () => setPanel(null) }) : panel === "members" ? (0, import_react47.createElement)(ProjectMembersPanel, { project: state.project, rows: state.rows, onClose: () => setPanel(null) }) : (0, import_react47.createElement)(ProjectBoardPane, {
+ project: state.project,
+ rows: state.rows,
+ canManage: state.canManage,
@@ -18814,7 +19041,7 @@ diff --git a/client.js b/client.js
// src/client/user-message.js
-var import_react34 = require("react");
-+var import_react47 = require("react");
++var import_react48 = require("react");
var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
var canDrawUserMessages = () => typeof ui.projectUserText === "function";
function contentParts(content) {
@@ -18854,8 +19081,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) => withMentionLook({ id: p.handle || p.id, name: remoteBotTitle(p), kind: "bot", remote: p, mentionLabel: remoteBotTitle(p) }, roster));
+ return bots.concat(humans, remotes, remoteMentionableBots(roster, roster.bots));
- }
--function userTextParts(text, bots, referenceLabels = []) {
++}
+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 });
@@ -18883,7 +19109,8 @@ diff --git a/client.js b/client.js
+ ownerName: remote?.ownerName,
+ mine: roster?.self?.memberId === authorId
+ };
-+}
+ }
+-function userTextParts(text, bots, referenceLabels = []) {
+function userTextParts(text2, bots, referenceLabels = []) {
const parts = [];
let cursor = 0;
@@ -18909,38 +19136,38 @@ diff --git a/client.js b/client.js
- part.label
- ) : (0, import_react34.createElement)(import_react34.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references)));
+function UserText({ text: text2, bots, referenceLabels, skillNames, references, actions }) {
-+ const chip = (part) => [(0, import_react47.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`];
++ const chip = (part) => [(0, import_react48.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`];
+ return userTextParts(text2, bots, referenceLabels).map((part, index) => {
-+ if (!part.bot) return (0, import_react47.createElement)(import_react47.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
++ if (!part.bot) return (0, import_react48.createElement)(import_react48.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references));
+ const title = `@${part.bot.name}`;
-+ return typeof part.bot.id === "string" ? (0, import_react47.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react47.createElement)("span", { key: index, className: "bt-umention", title }, chip(part));
++ return typeof part.bot.id === "string" ? (0, import_react48.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react48.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_react47.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => {
++ return (0, import_react48.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_react47.createElement)(import_react47.Fragment, { key: index }, renderMessageImages({ images: [{ attachment: block.attachment }], align: "end", compact: compact2 })) : null;
++ return renderMessageImages ? (0, import_react48.createElement)(import_react48.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_react47.createElement)(
++ return (0, import_react48.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_react47.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null,
-+ (0, import_react47.createElement)("span", { className: "bt-ufile-text" }, (0, import_react47.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react47.createElement)("span", { className: "bt-ufile-meta" }, meta) : null)
++ ui.FileTypeIcon ? (0, import_react48.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null,
++ (0, import_react48.createElement)("span", { className: "bt-ufile-text" }, (0, import_react48.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react48.createElement)("span", { className: "bt-ufile-meta" }, meta) : null)
);
}));
}
-function CopyButton({ text }) {
- const [copied, setCopied] = (0, import_react34.useState)(false);
+function CopyButton({ text: text2 }) {
-+ const [copied, setCopied] = (0, import_react47.useState)(false);
++ const [copied, setCopied] = (0, import_react48.useState)(false);
const copy2 = () => {
- void navigator.clipboard?.writeText(text).then(() => {
+ void navigator.clipboard?.writeText(text2).then(() => {
@@ -18949,7 +19176,7 @@ diff --git a/client.js b/client.js
});
};
- return (0, import_react34.createElement)("button", { type: "button", className: "bt-tool", "aria-label": copied ? t("Copied") : t("Copy"), title: copied ? t("Copied") : t("Copy"), onClick: copy2 }, copied ? (0, import_react34.createElement)(CheckIcon) : (0, import_react34.createElement)(CopyIcon));
-+ return (0, import_react47.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_react47.createElement)(CheckIcon) : (0, import_react47.createElement)(CopyIcon));
++ return (0, import_react48.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_react48.createElement)(CheckIcon) : (0, import_react48.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 }) {
@@ -18968,7 +19195,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_react47.createElement)(
++ return (0, import_react48.createElement)(
"div",
- { className: "bt-umsg", "data-steering": steering || void 0 },
- (0, import_react34.createElement)(
@@ -18980,28 +19207,28 @@ diff --git a/client.js b/client.js
+ "data-author-kind": author?.authorKind,
+ "data-author-name": author?.name
+ },
-+ person ? (0, import_react47.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react47.createElement)(PersonMark, { name: author.name })) : null,
-+ faceMine ? (0, import_react47.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react47.createElement)(PersonMark, { name: myName || void 0 })) : null,
-+ (0, import_react47.createElement)(
++ person ? (0, import_react48.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react48.createElement)(PersonMark, { name: author.name })) : null,
++ faceMine ? (0, import_react48.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react48.createElement)(PersonMark, { name: myName || void 0 })) : null,
++ (0, import_react48.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_react47.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react47.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react47.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null,
-+ attachments.length > 0 ? (0, import_react47.createElement)(Attachments, { attachments, renderMessageImages }) : null,
-+ text2 !== "" || rest.length > 0 ? (0, import_react47.createElement)(
++ faceMine && myName ? (0, import_react48.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react48.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react48.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null,
++ attachments.length > 0 ? (0, import_react48.createElement)(Attachments, { attachments, renderMessageImages }) : null,
++ text2 !== "" || rest.length > 0 ? (0, import_react48.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_react47.createElement)(UserText, { text: text2, bots: mentionableBots(roster, sessionId, { fromFriend: Boolean(incoming && author.ownerName) }), referenceLabels, skillNames: data.skillNames ?? [], references, actions }),
-+ rest.map((block, index) => ui.JsonBlock ? (0, import_react47.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null)
++ (0, import_react48.createElement)(UserText, { text: text2, bots: mentionableBots(roster, sessionId, { fromFriend: Boolean(incoming && author.ownerName) }), referenceLabels, skillNames: data.skillNames ?? [], references, actions }),
++ rest.map((block, index) => ui.JsonBlock ? (0, import_react48.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_react47.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null
++ referenceLabels.length > 0 ? (0, import_react48.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 }))
-+ text2 === "" ? null : (0, import_react47.createElement)("div", { className: "bt-utools" }, (0, import_react47.createElement)(CopyButton, { text: text2 }))
++ text2 === "" ? null : (0, import_react48.createElement)("div", { className: "bt-utools" }, (0, import_react48.createElement)(CopyButton, { text: text2 }))
);
}
var USER_MESSAGE_CSS = `
@@ -19103,7 +19330,7 @@ diff --git a/client.js b/client.js
}
+// src/client/stats-dock.js
-+var import_react48 = require("react");
++var import_react49 = require("react");
+var STATS_DOCK = "conversation.composer.dock";
+var STATS_ID = "stats";
+var STATS_IDS = [STATS_ID, "activity"];
@@ -19119,17 +19346,17 @@ diff --git a/client.js b/client.js
+function statsDock(entriesOf, id = STATS_ID) {
+ const shell = () => (entriesOf(STATS_DOCK) ?? []).find((entry) => entry?.component !== StatsDock && entry?.options?.id === id);
+ function StatsDock(props) {
-+ const t2 = (0, import_react48.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]);
++ const t2 = (0, import_react49.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_react48.createElement)(entry.component, { ...props, t: t2 });
++ return (0, import_react49.createElement)(entry.component, { ...props, t: t2 });
+ }
+ return { component: StatsDock, inject: (...args) => shell()?.inject?.(...args) ?? {} };
+}
+var statsDocks = (entriesOf) => STATS_IDS.map((id) => ({ id, ...statsDock(entriesOf, id) }));
+
// src/client/index.js
-+var import_react49 = require("react");
++var import_react50 = require("react");
var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout"];
function apply(ctx) {
+ const flybot = createClientApi2();
@@ -19508,7 +19735,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,49 @@
+@@ -14019,33 +14019,50 @@
installCompiledStyles(SHELL_CSS, "shell"),
installCompiledStyles(SIDEBAR_CSS, "sidebar"),
installCompiledStyles(SECRET_CSS, "secrets"),
@@ -19526,6 +19753,7 @@ diff --git a/client.js b/client.js
+ installCompiledStyles(MOBILE_CSS, "mobile"),
+ installCompiledStyles(PANE_MIN_WIDTH_CSS, "panes"),
+ installCompiledStyles(PROJECT_BOARD_CSS, "project-board"),
++ installCompiledStyles(PROJECTS_NAV_CSS, "projects-nav"),
+ typeof document !== "undefined" ? watchShellLevel(document) : null,
browsers ? installCompiledStyles(BROWSER_CSS, "browser") : null,
installTheme(),
@@ -19600,7 +19828,7 @@ diff --git a/client.js b/client.js
];
ctx.effect(() => {
+ const style = installStyles(TURN_FAILURES_CSS, "turn-failures");
-+ const off = registry().register("flybot.session.dock", { id: "flybot-turn-failures", order: 20, component: (props) => (0, import_react49.createElement)(TurnFailures, { ...props, work }) });
++ const off = registry().register("flybot.session.dock", { id: "flybot-turn-failures", order: 20, component: (props) => (0, import_react50.createElement)(TurnFailures, { ...props, work }) });
+ return () => {
+ off();
+ style.remove();
diff --git a/patches/ds-bot/files/src/client/projects-nav.js b/patches/ds-bot/files/src/client/projects-nav.js
new file mode 100644
index 000000000..59f9901cc
--- /dev/null
+++ b/patches/ds-bot/files/src/client/projects-nav.js
@@ -0,0 +1,191 @@
+// #1828: Projects as a first-level sidebar entry. The row expands to the Projects this caller can
+// see, each with the role they hold there; a row opens that board straight (`#project=`), and
+// the right-click menu offers only the roles the host says the caller may switch to, with the
+// current one marked. The policy itself lives in the host (`roleChoices`/`changeRole`), so this
+// module never decides who may switch what — it draws what the host answered and shows a refusal
+// as the host worded it.
+import { createElement as h, useCallback, useEffect, useRef, useState } from 'react'
+import { createPortal } from 'react-dom'
+import { call as callHost } from './api.js'
+import { t } from './i18n.js'
+
+const call = (endpoint, payload) => callHost(`flybot/${endpoint}`, payload)
+
+export const PROJECTS_NAV_CSS = `
+.bt-projects-row { display: flex; align-items: center; gap: 8px; }
+.bt-projects-chevron { margin-left: auto; display: inline-flex; transition: transform 120ms ease; }
+.bt-projects-row[aria-expanded="true"] .bt-projects-chevron { transform: rotate(90deg); }
+.bt-projects-list { display: flex; flex-direction: column; margin: 0 0 4px; padding-left: 30px; }
+.bt-project-entry { display: flex; align-items: baseline; gap: 8px; width: 100%; text-align: left; border: 0; background: none; color: inherit; font: inherit; padding: 4px 8px; border-radius: 6px; cursor: pointer; }
+.bt-project-entry:hover, .bt-project-entry:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; }
+.bt-project-entry[aria-current="true"] { background: var(--bt-hover, rgba(127,127,127,.14)); }
+.bt-project-entry-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.bt-project-entry-role { margin-left: auto; opacity: .62; font-size: .85em; white-space: nowrap; }
+.bt-projects-empty, .bt-projects-note { padding: 2px 8px 6px; opacity: .7; font-size: .85em; }
+.bt-project-menu { position: fixed; z-index: 60; min-width: 168px; padding: 4px; border-radius: 8px; background: var(--bt-menu-bg, #1f2126); border: 1px solid var(--bt-menu-border, rgba(255,255,255,.12)); box-shadow: 0 8px 24px rgba(0,0,0,.32); }
+.bt-project-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: none; color: inherit; font: inherit; text-align: left; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
+.bt-project-menu-item:hover, .bt-project-menu-item:focus-visible { background: var(--bt-hover, rgba(127,127,127,.14)); outline: none; }
+.bt-project-menu-item[aria-checked="true"]::before { content: "✓"; margin-right: 2px; }
+.bt-project-menu-item:not([aria-checked="true"])::before { content: " "; margin-right: 2px; }
+.bt-project-menu-item[disabled] { opacity: .6; cursor: default; }
+`
+
+const ROLE_LABELS = { owner: 'Owner', maintainer: 'Maintainer', member: 'Member' }
+
+export function roleText(role) {
+ if (role === null || role === undefined || role === '') return ''
+ return t(ROLE_LABELS[role] ?? String(role))
+}
+
+// `#project=` is the one entry point the board reads (`ProjectBoardOverlay`), so opening a row
+// writes the hash instead of mounting a second copy of the board.
+export function projectHash(id) {
+ return `#project=${encodeURIComponent(String(id ?? ''))}`
+}
+
+export function projectName(project) {
+ const name = typeof project?.name === 'string' ? project.name.trim() : ''
+ return name === '' ? `#${project?.id ?? ''}` : name
+}
+
+// The menu lists what the host said this caller may switch to, with the current rank marked. A
+// Project written before roles existed answers `role: null`, so the row reads without a rank
+// rather than guessing owner-only.
+export function projectMenuItems(project) {
+ const current = project?.role ?? null
+ const choices = Array.isArray(project?.choices) ? project.choices : []
+ const items = current === null ? [] : [{ role: current, label: roleText(current), current: true, pick: false }]
+ for (const role of choices) {
+ if (role === current) continue
+ items.push({ role, label: roleText(role), current: false, pick: true })
+ }
+ return items
+}
+
+// A refusal names a machine-readable reason (`not-owner`, `cannot-self-promote`, `last-owner`); the
+// reader gets the code back unchanged, as the brief asks, rather than a guess at its meaning.
+export function roleNote(answer) {
+ if (answer?.changed === true) return t('Role changed.')
+ const reason = typeof answer?.reason === 'string' && answer.reason !== '' ? answer.reason : t('refused')
+ return t('Role unchanged: {reason}', { reason })
+}
+
+// The expanded list on its own, so a test can draw it without a fetch or a click.
+export function ProjectList({ projects, error, note, onOpen, onRole }) {
+ const rows = projects === null
+ ? h('div', { className: 'bt-projects-note' }, t('Reading Projects…'))
+ : error !== ''
+ ? h('div', { className: 'bt-projects-note' }, error)
+ : projects.length === 0
+ ? h('div', { className: 'bt-projects-empty' }, t('No Projects yet'))
+ : projects.map(project => h('button', {
+ key: String(project.id),
+ type: 'button', className: 'bt-project-entry', 'data-project-id': String(project.id),
+ 'aria-current': project.current === true ? 'true' : undefined,
+ title: roleText(project.role) === '' ? projectName(project) : `${projectName(project)} · ${roleText(project.role)}`,
+ onClick: () => onOpen?.(project.id),
+ onContextMenu: (event) => { event.preventDefault(); onRole?.(project, event) },
+ },
+ h('span', { className: 'bt-project-entry-name' }, projectName(project)),
+ roleText(project.role) === '' ? null : h('span', { className: 'bt-project-entry-role' }, roleText(project.role))))
+ return h('div', { className: 'bt-projects-list', role: 'group', 'aria-label': t('Projects') },
+ rows,
+ note === '' || note === undefined ? null : h('div', { className: 'bt-projects-note', role: 'status' }, note))
+}
+
+function Chevron() {
+ return h('svg', { width: 12, height: 12, viewBox: '0 0 20 20', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
+ h('path', { d: 'M7.5 4.5L13 10l-5.5 5.5' }))
+}
+
+function ProjectsMark() {
+ 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('path', { d: 'M2.5 5.5h6l1.5 2h7.5v8a1 1 0 0 1-1 1h-13a1 1 0 0 1-1-1z' }),
+ h('path', { d: 'M2.5 5.5v-1a1 1 0 0 1 1-1h4l1.5 2' }))
+}
+
+// The menu is a portal at the cursor, closed by a click anywhere else, by Escape, and by picking.
+export function ProjectRoleMenu({ project, x, y, onPick, onClose }) {
+ const ref = useRef(null)
+ useEffect(() => {
+ const onDown = (event) => { if (!ref.current?.contains(event.target)) onClose() }
+ const onKey = (event) => { if (event.key === 'Escape') onClose() }
+ window.addEventListener('mousedown', onDown, true)
+ window.addEventListener('keydown', onKey, true)
+ return () => { window.removeEventListener('mousedown', onDown, true); window.removeEventListener('keydown', onKey, true) }
+ }, [onClose])
+ const items = projectMenuItems(project)
+ // A member has nothing to switch: the menu still opens and marks the rank they hold, so the
+ // right-click answers "where am I" instead of doing nothing.
+ return createPortal(h('div', {
+ ref, className: 'bt-project-menu', role: 'menu', 'aria-label': t('My role'),
+ style: { left: `${x}px`, top: `${y}px` }, 'data-project-id': String(project?.id ?? ''),
+ },
+ h('div', { className: 'bt-projects-note' }, projectName(project)),
+ items.length === 0
+ ? h('div', { className: 'bt-projects-note' }, t('No role to switch here.'))
+ : items.map(item => h('button', {
+ key: item.role, type: 'button', role: 'menuitemradio', className: 'bt-project-menu-item',
+ 'aria-checked': item.current ? 'true' : 'false', disabled: !item.pick,
+ onClick: () => { if (item.pick) onPick(item.role); else onClose() },
+ }, item.label))), document.body)
+}
+
+export function ProjectsNav({ wide = true, request, onOpen } = {}) {
+ const read = request ?? call
+ const open = useCallback((id) => {
+ if (onOpen) { onOpen(id); return }
+ if (typeof window !== 'undefined') window.location.hash = projectHash(id)
+ }, [onOpen])
+ const [expanded, setExpanded] = useState(false)
+ const [projects, setProjects] = useState(null)
+ const [error, setError] = useState('')
+ const [note, setNote] = useState('')
+ const [menu, setMenu] = useState(null)
+ const load = useCallback(async () => {
+ try {
+ const listing = await read('projects')
+ const list = Array.isArray(listing?.projects) ? listing.projects : []
+ setProjects(list)
+ setError('')
+ } catch (failure) {
+ setProjects([])
+ setError(failure?.message ?? String(failure))
+ }
+ }, [read])
+ useEffect(() => {
+ if (!expanded) return
+ void load()
+ }, [expanded, load])
+ const toggle = () => {
+ setNote('')
+ if (!wide) { void open(''); return }
+ setExpanded(value => !value)
+ }
+ const listed = expanded && wide
+ return h('div', { className: 'bt-projects' },
+ h('button', {
+ type: 'button', className: 'bt-row bt-projects-row', 'aria-expanded': listed ? 'true' : 'false',
+ 'aria-label': t('Projects'), title: wide ? undefined : t('Projects'), onClick: toggle,
+ },
+ h('span', { className: 'bt-projects-mark' }, h(ProjectsMark)),
+ wide ? h('span', { className: 'bt-projects-label' }, t('Projects')) : null,
+ wide ? h('span', { className: 'bt-projects-chevron', 'aria-hidden': true }, h(Chevron)) : null),
+ listed ? h(ProjectList, { projects, error, note, onOpen: open, onRole: (project, event) => setMenu({ project, x: event.clientX, y: event.clientY }) }) : null,
+ menu === null ? null : h(ProjectRoleMenu, {
+ project: menu.project, x: menu.x, y: menu.y,
+ onClose: () => setMenu(null),
+ onPick: async (role) => {
+ const project = menu.project
+ setMenu(null)
+ try {
+ const answer = await read('project-role', { id: project.id, role })
+ setNote(roleNote(answer))
+ if (answer?.changed === true) void load()
+ } catch (failure) {
+ // The host's sentence is the answer; quoting it beats a generic failure line.
+ setNote(failure?.message ?? String(failure))
+ }
+ },
+ }))
+}
diff --git a/patches/ds-bot/files/src/host/project-board.js b/patches/ds-bot/files/src/host/project-board.js
index 6af97145a..8b830395f 100644
--- a/patches/ds-bot/files/src/host/project-board.js
+++ b/patches/ds-bot/files/src/host/project-board.js
@@ -21,6 +21,74 @@ import { join } from 'node:path'
import { columnOf } from '../shared/task-board.js'
import { DECISIONS_DIR, listProjectState, sha256 } from './project-state.js'
+// #1828: a Project's roles. The record stores only `roles: { maintainers, members }`; the owner
+// is read from `ownerUserIds`, never stored twice, so the two cannot disagree. A record written
+// before this slice carries no `roles` and derives to owner-only — no migration pass.
+export const PROJECT_ROLE_RANKS = { member: 1, maintainer: 2, owner: 3 }
+const roleList = value => (Array.isArray(value) ? [...new Set(value.map(id => text(id)).filter(id => id !== ''))] : [])
+export function rolesOf(project) {
+ const owner = roleList(project?.ownerUserIds)
+ const primary = text(project?.ownerUserId)
+ if (primary !== '' && !owner.includes(primary)) owner.unshift(primary)
+ const stored = project?.roles !== null && typeof project?.roles === 'object' ? project.roles : {}
+ const maintainers = roleList(stored.maintainers).filter(id => !owner.includes(id))
+ const members = roleList(stored.members).filter(id => !owner.includes(id) && !maintainers.includes(id))
+ return { owner, maintainers, members }
+}
+export function roleOf(project, memberId) {
+ const id = text(memberId)
+ if (id === '') return null
+ const roles = rolesOf(project)
+ if (roles.owner.includes(id)) return 'owner'
+ if (roles.maintainers.includes(id)) return 'maintainer'
+ if (roles.members.includes(id)) return 'member'
+ return null
+}
+// The roles a caller may switch a target to: the current one is not repeated, and the Owner rank
+// is never an assignment (transfer is a separate slice). A Member sees nothing, a Maintainer
+// sees only Member, and an Owner acting on anyone else sees the two assignable ranks.
+export function roleChoices(project, actorId, targetId = actorId) {
+ const actor = roleOf(project, actorId)
+ const self = text(targetId) === text(actorId)
+ const target = self ? actor : roleOf(project, targetId)
+ const choices = []
+ // The Owner of a Project never drops its own rank here: the menu shows only the current item.
+ if (self && actor === 'owner') return choices
+ for (const role of ['maintainer', 'member']) {
+ if (role === target) continue
+ if (actor === 'owner') choices.push(role)
+ else if (self && actor !== null && PROJECT_ROLE_RANKS[role] < PROJECT_ROLE_RANKS[actor]) choices.push(role)
+ }
+ return choices
+}
+// The policy Chief's matrix (2026-10-30) fixes, in one place the endpoint and the menu both read:
+// only an Owner re-roles someone else, nobody promotes themselves, and a Project always keeps its
+// Owner. Returns the next stored roles on success and a machine-readable reason on refusal; the
+// caller's record is untouched either way.
+export function changeRole(project, { actorId, targetId, role } = {}) {
+ const actor = roleOf(project, actorId)
+ const self = text(targetId) === '' || text(targetId) === text(actorId)
+ const target = self ? text(actorId) : text(targetId)
+ const wanted = text(role)
+ const current = rolesOf(project)
+ if (PROJECT_ROLE_RANKS[wanted] === undefined) return { ok: false, reason: 'unknown-role' }
+ if (wanted === 'owner') return { ok: false, reason: 'owner-not-assignable' }
+ if (actor === null) return { ok: false, reason: 'not-owner' }
+ if (self) {
+ if (actor === 'owner') return { ok: false, reason: 'last-owner' }
+ if (PROJECT_ROLE_RANKS[wanted] > PROJECT_ROLE_RANKS[actor]) return { ok: false, reason: 'cannot-self-promote' }
+ } else if (actor !== 'owner') {
+ return { ok: false, reason: 'not-owner' }
+ } else if (current.owner.includes(target)) {
+ return { ok: false, reason: 'last-owner' }
+ }
+ const maintainers = current.maintainers.filter(id => id !== target)
+ const members = current.members.filter(id => id !== target)
+ if (wanted === 'maintainer') maintainers.push(target)
+ else members.push(target)
+ return { ok: true, roles: { maintainers, members } }
+}
+
const text = value => (typeof value === 'string' ? value.trim() : '')
// The queue, the leases and the blocked cards are host truth the board never copied (#1768):
@@ -364,6 +432,13 @@ export function install(rt) {
waiting.sort((a, b) => (a.since ?? Infinity) - (b.since ?? Infinity) || a.id.localeCompare(b.id))
return { waiting, inflight, blocked, counts: { waiting: waiting.length, inflight: inflight.length, blocked: blocked.length, done } }
}
+ // #1828: the endpoint the host calls for a role switch. It answers the policy's own decision;
+ // the store is what persists the two assignable lists.
+ rt.projectRole = (project, { actorId, targetId, role } = {}) => changeRole(project, { actorId, targetId, role })
+ // The listing's own read (#1828): the caller's rank and the ranks they may switch to. Both are
+ // the functions the endpoint uses, so the sidebar menu offers exactly what the host accepts.
+ rt.projectRoleOf = (project, memberId) => roleOf(project, memberId)
+ rt.projectRoleChoices = (project, memberId) => roleChoices(project, memberId)
rt.projectBoard = projectBoard
// The three groups are their own read over the same pass, for a caller that wants only the queue.
rt.projectBoardQueues = async (projectId, actor) => {
diff --git a/patches/ds-bot/files/test/project-roles.test.js b/patches/ds-bot/files/test/project-roles.test.js
new file mode 100644
index 000000000..470d682ec
--- /dev/null
+++ b/patches/ds-bot/files/test/project-roles.test.js
@@ -0,0 +1,47 @@
+// #1828: a Project's roles. The record stores only maintainers and members; the Owner is derived
+// from `ownerUserIds`, so a Project written before this slice is owner-only without a migration.
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { changeRole, roleChoices, roleOf, rolesOf } from '../src/host/project-board.js'
+
+const project = (extra = {}) => ({ id: 'project-1', name: 'P', ownerUserIds: ['owner'], ...extra })
+
+test('a Project written before roles derives to owner-only (#1828)', () => {
+ const old = project()
+ assert.deepEqual(rolesOf(old), { owner: ['owner'], maintainers: [], members: [] })
+ assert.equal(roleOf(old, 'owner'), 'owner')
+ assert.equal(roleOf(old, 'someone'), null)
+ assert.deepEqual(roleChoices(old, 'owner'), [])
+})
+
+test('a member cannot promote themselves and the roles stay put (#1828)', () => {
+ const p = project({ roles: { maintainers: [], members: ['m'] } })
+ const before = rolesOf(p)
+ assert.deepEqual(changeRole(p, { actorId: 'm', targetId: 'm', role: 'maintainer' }), { ok: false, reason: 'cannot-self-promote' })
+ assert.deepEqual(changeRole(p, { actorId: 'm', targetId: 'm', role: 'owner' }), { ok: false, reason: 'owner-not-assignable' })
+ assert.deepEqual(rolesOf(p), before)
+ assert.deepEqual(roleChoices(p, 'm'), [])
+})
+
+test('a member has no switchable role and cannot promote themselves (#1828)', () => {
+ const p = project({ roles: { maintainers: [], members: ['b'] } })
+ assert.deepEqual(roleChoices(p, 'b'), [])
+ assert.deepEqual(changeRole(p, { actorId: 'b', targetId: 'b', role: 'maintainer' }), { ok: false, reason: 'cannot-self-promote' })
+ assert.deepEqual(changeRole(p, { actorId: 'b', targetId: 'c', role: 'member' }), { ok: false, reason: 'not-owner' })
+ assert.deepEqual(rolesOf(p), { owner: ['owner'], maintainers: [], members: ['b'] })
+})
+
+test('an owner demotes another member to Member (#1828)', () => {
+ const p = project({ roles: { maintainers: ['m'], members: ['n'] } })
+ const done = changeRole(p, { actorId: 'owner', targetId: 'm', role: 'member' })
+ assert.deepEqual(done, { ok: true, roles: { maintainers: [], members: ['n', 'm'] } })
+ assert.deepEqual(roleChoices(p, 'owner', 'm'), ['member'], 'the current role is not offered')
+ assert.deepEqual(roleChoices(p, 'owner', 'n'), ['maintainer'])
+})
+
+test('the last owner cannot be demoted, by themselves or by anyone else (#1828)', () => {
+ const p = project()
+ assert.deepEqual(changeRole(p, { actorId: 'owner', targetId: 'owner', role: 'member' }), { ok: false, reason: 'last-owner' })
+ assert.deepEqual(changeRole(p, { actorId: 'owner', targetId: 'owner', role: 'owner' }), { ok: false, reason: 'owner-not-assignable' })
+ assert.deepEqual(rolesOf(p), { owner: ['owner'], maintainers: [], members: [] })
+})
diff --git a/patches/ds-bot/files/test/projects-nav.test.js b/patches/ds-bot/files/test/projects-nav.test.js
new file mode 100644
index 000000000..53397e5b8
--- /dev/null
+++ b/patches/ds-bot/files/test/projects-nav.test.js
@@ -0,0 +1,118 @@
+// #1828: Projects in the sidebar. The list is the host's answer — a Project's role and the ranks
+// the caller may switch to both come from the endpoint's own policy — so these tests draw what the
+// host said and never guess a rank.
+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/projects-nav.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: () => 0, clearTimeout: () => {}, Date, Promise, console,
+ window: { addEventListener: () => {}, removeEventListener: () => {}, location: { hash: '' } },
+ document: { body: {} },
+ require: (name) => {
+ const platform = {
+ react: {
+ createElement: (type, props, ...children) => ({ type, props: props ?? {}, children: children.flat(Infinity) }),
+ useState: initial => [typeof initial === 'function' ? initial() : initial, () => {}],
+ useEffect: () => {}, useLayoutEffect: () => {}, useRef: current => ({ current }), useCallback: fn => fn, useMemo: fn => fn(), Fragment: 'fragment',
+ },
+ 'react-dom': { createPortal: element => element },
+ }
+ if (!(name in platform)) throw new Error(`no platform module ${name}`)
+ return platform[name]
+ },
+})
+const { ProjectList, ProjectRoleMenu, ProjectsNav, projectHash, projectMenuItems, projectName, roleNote, roleText } = module.exports
+
+const textOf = node => (node?.children ?? []).flat(Infinity).filter(child => typeof child === 'string').join('')
+const buttons = node => (node?.children ?? []).flat(Infinity).filter(child => child && child.type === 'button')
+const find = (node, className) => {
+ if (node === null || typeof node !== 'object') return null
+ if (String(node.props?.className ?? '').split(' ').includes(className)) return node
+ for (const child of node.children ?? []) {
+ const hit = find(child, className)
+ if (hit) return hit
+ }
+ return null
+}
+const project = (extra = {}) => ({ id: 'p1', name: 'Alpha', role: 'member', choices: [], ...extra })
+// Values built inside the vm carry that realm's prototypes; compare the data, not the origin.
+const plain = value => JSON.parse(JSON.stringify(value))
+
+test('the list draws each Project with the role the host answered (#1828)', () => {
+ const view = ProjectList({ projects: [project(), project({ id: 'p2', name: '', role: 'owner' })], error: '', note: '' })
+ const rows = buttons(view)
+ assert.equal(rows.length, 2)
+ assert.deepEqual(rows.map(row => row.props['data-project-id']), ['p1', 'p2'])
+ const roles = rows.map(row => (find(row, 'bt-project-entry-role') ?? {}).children?.join(''))
+ assert.deepEqual(roles, ['Member', 'Owner'])
+ // A Project with no name reads by id, never blank.
+ assert.equal(projectName({ id: 'x9' }), '#x9')
+})
+
+test('an empty list and an unreadable one say so instead of drawing nothing (#1828)', () => {
+ assert.equal(textOf(find(ProjectList({ projects: [], error: '', note: '' }), 'bt-projects-empty')), 'No Projects yet')
+ assert.equal(textOf(find(ProjectList({ projects: null, error: '', note: '' }), 'bt-projects-note')), 'Reading Projects…')
+ assert.equal(textOf(find(ProjectList({ projects: [], error: 'No permission', note: '' }), 'bt-projects-note')), 'No permission')
+})
+
+test('a row opens its board by id, and the hash is the board entry point (#1828)', () => {
+ const opened = []
+ const view = ProjectList({ projects: [project()], error: '', note: '', onOpen: id => opened.push(id) })
+ buttons(view)[0].props.onClick({})
+ assert.deepEqual(opened, ['p1'])
+ assert.equal(projectHash('p 1'), '#project=p%201')
+})
+
+test('the menu lists only switchable roles and marks the current one (#1828)', () => {
+ // A Member sees only the rank they hold: nothing to pick.
+ const member = plain(projectMenuItems(project()))
+ assert.deepEqual(member, [{ role: 'member', label: 'Member', current: true, pick: false }])
+ // An Owner cannot demote themselves, so the menu is the current item alone.
+ assert.deepEqual(plain(projectMenuItems(project({ role: 'owner', choices: [] }))), [{ role: 'owner', label: 'Owner', current: true, pick: false }])
+ // A Maintainer may drop to Member, and the current rank is not repeated.
+ assert.deepEqual(plain(projectMenuItems(project({ role: 'maintainer', choices: ['member'] }))), [
+ { role: 'maintainer', label: 'Maintainer', current: true, pick: false },
+ { role: 'member', label: 'Member', current: false, pick: true },
+ ])
+ // A record written before roles existed carries none, and the menu does not invent owner-only.
+ assert.deepEqual(plain(projectMenuItems({ id: 'old', role: null, choices: [] })), [])
+ assert.equal(roleText(null), '')
+})
+
+test('the menu draws the current rank checked and disabled, and picking calls back (#1828)', () => {
+ const picked = []
+ const view = ProjectRoleMenu({
+ project: project({ id: 'p3', role: 'maintainer', choices: ['member'], name: 'Beta' }),
+ x: 10, y: 20, onPick: role => picked.push(role), onClose: () => {},
+ })
+ const items = (view.children ?? []).flat(Infinity).filter(child => child && child.type === 'button')
+ assert.equal(items.length, 2)
+ assert.equal(items[0].props['aria-checked'], 'true')
+ assert.equal(items[0].props.disabled, true)
+ assert.equal(items[1].props['aria-checked'], 'false')
+ items[1].props.onClick()
+ assert.deepEqual(picked, ['member'])
+})
+
+test('a refusal is quoted with the host reason, and a switch says so (#1828)', () => {
+ assert.equal(roleNote({ changed: false, reason: 'last-owner' }), 'Role unchanged: last-owner')
+ assert.equal(roleNote({ changed: false, reason: 'cannot-self-promote' }), 'Role unchanged: cannot-self-promote')
+ assert.equal(roleNote({ changed: true }), 'Role changed.')
+})
+
+test('the sidebar row carries the expand arrow and reports its state (#1828)', () => {
+ const row = ProjectsNav({ wide: true })
+ const button = buttons(row)[0]
+ assert.equal(button.props['aria-expanded'], 'false')
+ assert.ok(find(row, 'bt-projects-chevron'), 'the expand arrow is drawn')
+ assert.equal(textOf(find(row, 'bt-projects-label')), 'Projects')
+})