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') +})