diff --git a/changelog.d/1710.added.md b/changelog.d/1710.added.md new file mode 100644 index 000000000..1cb105c23 --- /dev/null +++ b/changelog.d/1710.added.md @@ -0,0 +1 @@ +A Project now has a board of its own: one read-only view over the work under it, grouped by where it lives (Bots, groups, workflow runs), showing each row's column, assignee and the run and node it came from, with every row opening the task it comes from. A Project with nothing under it says so instead of failing. Only the Project's owner sees the management controls, and the host refuses a binding or a Project change from anyone else rather than relying on the page to hide a button. Open the board at `#project=` ([#1710](https://github.com/ThinkFlowLab/FlyBot/issues/1710)). diff --git a/core/client.js b/core/client.js index 38e5217d1..cf9b40c34 100644 --- a/core/client.js +++ b/core/client.js @@ -1353,7 +1353,10 @@ var ZH = { "Use this folder": "使用这个文件夹", "Up one level": "上一级", "No folders inside.": "里面没有文件夹。", - "Some folders are not shown.": "还有一些文件夹没有显示。" + "Some folders are not shown.": "还有一些文件夹没有显示。", + "Project settings": "项目设置", + "Project board": "项目看板", + "No Project here.": "这里没有项目。" }; var HOST_ERRORS = [ [/^Model (.+) is not available\. Available: (.*)$/, "模型 $1 不可用。可用的:$2"], @@ -1439,7 +1442,7 @@ var HOST_ZH = { "saved by a newer FlyBot; update FlyBot": "由更新的 FlyBot 保存;请更新 FlyBot" }; Object.assign(ZH, HOST_ZH); -var fill = (text, params) => params ? text.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match) : text; +var fill = (text2, params) => params ? text2.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match) : text2; var service = null; var bound = null; function installLocale(locale) { @@ -1490,23 +1493,23 @@ function noteClientError(where, error) { } const message = String(error?.message ?? error ?? "error").slice(0, 300); const code = typeof error?.code === "string" ? error.code : void 0; - const text = `${where}: ${message}`; + const text2 = `${where}: ${message}`; const now = Date.now(); const last = entries.at(-1); - if (last && last.text === text && now - last.time < REPEAT_MS) { + if (last && last.text === text2 && now - last.time < REPEAT_MS) { last.time = now; last.count = (last.count ?? 1) + 1; return; } - 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); } 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 text = `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] ${text}`); - entries.push({ time: Date.now(), level: "warn", text }); + 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); } var quietly = (where) => (error) => noteClientError(where, error); @@ -1547,9 +1550,9 @@ var WHAT_MAX = 2e3; var PASTE_NOTE = "FlyBot copied the diagnostics to the clipboard. Paste them here."; var REST_NOTE = "The recent errors do not fit in the link. FlyBot copied the diagnostics to the clipboard: paste them below this line."; function issueUrl(base, { what = "", diagnostics }) { - const text = what.trim().slice(0, WHAT_MAX); - const title = text.split("\n")[0].slice(0, 80); - const build = (body) => `${base}?${new URLSearchParams({ template: "bug.yml", ...title ? { title } : {}, ...text ? { what: text } : {}, diagnostics: body })}`; + const text2 = what.trim().slice(0, WHAT_MAX); + const title = text2.split("\n")[0].slice(0, 80); + const build = (body) => `${base}?${new URLSearchParams({ template: "bug.yml", ...title ? { title } : {}, ...text2 ? { what: text2 } : {}, diagnostics: body })}`; const full2 = build(diagnostics); if (full2.length <= URL_LIMIT) return { url: full2, inline: true }; const facts = build(`${diagnostics.split("\n\n")[0]} @@ -1603,8 +1606,8 @@ var call = async (endpoint, payload, signal) => { // src/shared/remote-label.js function cleanPeerName(name, fallback = "") { - const text = String(name ?? "").replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ").replace(/[[\]"`<>]/g, "").replace(/\s+/g, " ").trim().slice(0, 60); - return text || fallback; + const text2 = String(name ?? "").replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ").replace(/[[\]"`<>]/g, "").replace(/\s+/g, " ").trim().slice(0, 60); + return text2 || fallback; } function normRemoteHandle(handle) { return `@${String(handle ?? "").trim().replace(/^@+/, "").toLowerCase()}`; @@ -1709,10 +1712,10 @@ function remoteCallName(record) { const handle = String(record?.handle ?? "").replace(/^@+/, "").trim(); const bare2 = remoteBotName(record, "").replace(/^@+/, "").trim(); const unusable = (value) => { - const text = String(value ?? "").replace(/^@+/, "").trim(); - if (!text || /^friend$/i.test(text)) return true; - if (text.includes("@") || /\.home\b/i.test(text)) return true; - if (handle && text.toLowerCase() === handle.toLowerCase()) return true; + const text2 = String(value ?? "").replace(/^@+/, "").trim(); + if (!text2 || /^friend$/i.test(text2)) return true; + if (text2.includes("@") || /\.home\b/i.test(text2)) return true; + if (handle && text2.toLowerCase() === handle.toLowerCase()) return true; return false; }; if (!unusable(bare2)) return bare2; @@ -1773,11 +1776,11 @@ function ungrantedLocalFix(details) { const friends = [...new Set((Array.isArray(details.remotes) ? details.remotes : []).map((remote) => cleanPeerName(remote?.owner || remote?.name, "")).filter(Boolean))]; return { bots, friends }; } -async function copiedToClipboard(text, nav = globalThis.navigator) { +async function copiedToClipboard(text2, nav = globalThis.navigator) { const write = nav?.clipboard?.writeText; if (typeof write !== "function") return false; try { - await write.call(nav.clipboard, text); + await write.call(nav.clipboard, text2); return true; } catch { return false; @@ -1925,8 +1928,8 @@ function remoteMembersByHome(roster) { } return [...homes.values()]; } -function remoteMessageText(text) { - const value = String(text ?? ""); +function remoteMessageText(text2) { + const value = String(text2 ?? ""); const match = value.match(/\n?([\s\S]*?)\n?<\/untrusted-message>/); return match ? match[1] : value; } @@ -2046,9 +2049,9 @@ var MAIN_LOOK = Object.freeze({ shape: "whale", color: "deepseek" }); var CHARACTER_COLORS = ["black", "brown", "red", "orange", "yellow", "green", "cyan", "blue", "violet", "magenta", "gray", "aurora"]; var DEFAULT_COLORS = CHARACTER_COLORS.filter((color) => !["black", "gray", "brown"].includes(color) && !GRADIENTS[color]); var isColor = (color) => isHex(color) || Object.hasOwn(INKS, color); -function fnv1a(text) { +function fnv1a(text2) { let value = 2166136261; - for (let index = 0; index < text.length; index += 1) value = Math.imul(value ^ text.charCodeAt(index), 16777619); + for (let index = 0; index < text2.length; index += 1) value = Math.imul(value ^ text2.charCodeAt(index), 16777619); return value >>> 0; } function defaultColor(id) { @@ -2169,8 +2172,8 @@ function bodyOf(def, paint, flat = false) { return ""; } } -var eyeMarkup = ([cx, cy, w, h48, tilt]) => ``; -var happyEyeMarkup = ([cx, cy, w, h48, tilt]) => ``; +var eyeMarkup = ([cx, cy, w, h49, tilt]) => ``; +var happyEyeMarkup = ([cx, cy, w, h49, 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%"; @@ -2221,30 +2224,30 @@ var ALL_WORDS = ["all", "everyone", "所有人", "全体"]; var NAME_GOES_ON = /^(?:[a-z0-9_\-/\\]|\.[a-z0-9_-])/i; var ADDRESS_BEFORE = /[a-z0-9_./]/i; var LITERAL = /```[\s\S]*?(?:```|$)|`[^`\n]*`|\[[^\]\n]*\]\([^)\s]*(?:\s+(?:"[^"\n]*"|'[^'\n]*'))?\)/g; -var fitsAt = (text, at, word) => text.slice(at, at + word.length).toLowerCase() === word.toLowerCase() && !NAME_GOES_ON.test(text.slice(at + word.length)); +var fitsAt = (text2, at, word) => text2.slice(at, at + word.length).toLowerCase() === word.toLowerCase() && !NAME_GOES_ON.test(text2.slice(at + word.length)); var overlaps = (spans, start, end) => spans.some((span) => start < span.end && span.start < end); function nameProblem(name) { - const text = String(name ?? "").trim(); - if (text === "") return "A Bot needs a name"; - if (/[@`\r\n]/.test(text)) return "A Bot name cannot contain @, ` or a line break"; - if (ALL_WORDS.includes(text.toLowerCase())) return `"${text}" calls everyone in a group, so it cannot be a Bot name`; + const text2 = String(name ?? "").trim(); + if (text2 === "") return "A Bot needs a name"; + if (/[@`\r\n]/.test(text2)) return "A Bot name cannot contain @, ` or a line break"; + if (ALL_WORDS.includes(text2.toLowerCase())) return `"${text2}" calls everyone in a group, so it cannot be a Bot name`; return void 0; } -function referenceSpans(text, labels = []) { +function referenceSpans(text2, labels = []) { const spans = []; for (const label of [...labels].sort((a, b) => b.length - a.length)) { const mention = `@${label}`; - for (let at = text.indexOf(mention); at !== -1; at = text.indexOf(mention, at + 1)) { + for (let at = text2.indexOf(mention); at !== -1; at = text2.indexOf(mention, at + 1)) { const end = at + mention.length; - if (at > 0 && ADDRESS_BEFORE.test(text[at - 1]) || NAME_GOES_ON.test(text.slice(end)) || overlaps(spans, at, end)) continue; + if (at > 0 && ADDRESS_BEFORE.test(text2[at - 1]) || NAME_GOES_ON.test(text2.slice(end)) || overlaps(spans, at, end)) continue; spans.push({ start: at, end, label }); break; } } return spans.sort((a, b) => a.start - b.start); } -function mentionSpans(text, bots, { references = [] } = {}) { - const value = String(text ?? ""); +function mentionSpans(text2, bots, { references = [] } = {}) { + const value = String(text2 ?? ""); const skip = [ ...[...value.matchAll(LITERAL)].map((match) => ({ start: match.index, end: match.index + match[0].length })), ...referenceSpans(value, references) @@ -2262,8 +2265,8 @@ function mentionSpans(text, bots, { references = [] } = {}) { } return spans; } -function unmatchedMentions(text, bots, { references = [] } = {}) { - const value = String(text ?? ""); +function unmatchedMentions(text2, bots, { references = [] } = {}) { + const value = String(text2 ?? ""); const skip = [ ...[...value.matchAll(LITERAL)].map((match) => ({ start: match.index, end: match.index + match[0].length })), ...referenceSpans(value, references) @@ -2289,16 +2292,16 @@ var FENCE_TAG = /<\s*\/?\s*untrusted-(?:message|file)\b[^<>]*>/gi; var FENCE_ATTR = /\bfrom\s*=\s*(?:"[^"]*"|'[^']*')\s*>/gi; var FENCE_TAIL = /<[^<>]*\buntrusted-(?:message|file)\b[^<>]*$/i; var FENCE_BARE = /(?:\/\s*)?untrusted-(?:message|file)\b\s*>?/gi; -function stripFence(text) { - let out = String(text ?? ""); +function stripFence(text2) { + let out = String(text2 ?? ""); for (let previous; previous !== out; ) { previous = out; out = out.replace(FENCE_TAG, "").replace(FENCE_ATTR, ""); } return out.replace(FENCE_TAIL, "").replace(FENCE_BARE, ""); } -function visibleText(text) { - let out = String(text ?? ""); +function visibleText(text2) { + let out = String(text2 ?? ""); for (let previous; previous !== out; ) { previous = out; const open = out.indexOf(" Number(Boolean(a?.remote)) - Number(Boolean(b?.remote))); let out = ""; let cursor = 0; - for (const span of mentionSpans(text, ordered)) { + for (const span of mentionSpans(text2, ordered)) { if (!span.bot || span.bot.id === selfId) continue; - out += `${text.slice(cursor, span.start)}[${text.slice(span.start + 1, span.end).replace(/[[\]\\]/g, "\\$&")}](${MENTION_ORIGIN}${span.bot.id})`; + out += `${text2.slice(cursor, span.start)}[${text2.slice(span.start + 1, span.end).replace(/[[\]\\]/g, "\\$&")}](${MENTION_ORIGIN}${span.bot.id})`; cursor = span.end; } - return cursor === 0 ? text : out + text.slice(cursor); + return cursor === 0 ? text2 : out + text2.slice(cursor); } function mentionTarget(event) { const anchor = event.target instanceof Element ? event.target.closest(`a[href^="${MENTION_ORIGIN}"]`) : null; return anchor ? anchor.getAttribute("href").slice(MENTION_ORIGIN.length) : null; } -function nameMentions(text, roster) { - const value = String(text ?? ""); +function nameMentions(text2, roster) { + const value = String(text2 ?? ""); if (!value.includes("@") || roster === null || typeof roster !== "object") return value; const records = [...Array.isArray(roster.remoteBots) ? roster.remoteBots : [], ...Array.isArray(roster.bots) ? roster.bots : []]; const names = /* @__PURE__ */ new Map(); @@ -2371,9 +2374,9 @@ function landingPart(roster, last, mainId, { stay = false, view = "", sessions } const main = roster.byId[mainId]; return main ? currentPart(main) : mainId; } -function reminderPrompts(text) { - const one = /^reminder_prompt_json: (.*)$/m.exec(text); - const batch = /^reminders_json: (.*)$/m.exec(text); +function reminderPrompts(text2) { + const one = /^reminder_prompt_json: (.*)$/m.exec(text2); + const batch = /^reminders_json: (.*)$/m.exec(text2); try { const prompts = [...one ? [JSON.parse(one[1])] : [], ...batch ? JSON.parse(batch[1]).map((entry) => entry.reminder_prompt) : []]; return prompts.filter((prompt) => typeof prompt === "string" && prompt.trim() !== ""); @@ -2381,13 +2384,13 @@ function reminderPrompts(text) { return []; } } -var stripHeader = (text) => text.replace(HEADER, ""); +var stripHeader = (text2) => text2.replace(HEADER, ""); var TRACE = /^[ \t]*[^\n→]{1,60}(?:[ \t]*→[ \t]*[^\n→]{1,60})+[ \t]*\n+/; -function stripTrace(text) { - if (typeof text !== "string") return text; - const line2 = text.match(TRACE)?.[0]; - if (!line2 || /[.!?。!?::,,]/.test(line2.replace(/[^\S\n]*\n+$/, "").replace(/→/g, ""))) return text; - return text.slice(line2.length).replace(/^\s+/, ""); +function stripTrace(text2) { + if (typeof text2 !== "string") return text2; + const line2 = text2.match(TRACE)?.[0]; + if (!line2 || /[.!?。!?::,,]/.test(line2.replace(/[^\S\n]*\n+$/, "").replace(/→/g, ""))) return text2; + return text2.slice(line2.length).replace(/^\s+/, ""); } var COPY_FENCE = /untrusted-(?:message|file)\b/i; var escapeName = (name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -2397,8 +2400,8 @@ var isTraceLine = (line2, known) => { const hop = `(?:${known.map(escapeName).join("|")})`; return new RegExp(`^[ \\t]*${hop}(?:[ \\t]*→[ \\t]*${hop})+[ \\t]*$`).test(line2); }; -function stripFlatTrace(text, names = []) { - const body = String(text ?? ""); +function stripFlatTrace(text2, names = []) { + const body = String(text2 ?? ""); const known = sortedNames(names); if (known.length === 0) return body; const hop = `(?:${known.map(escapeName).join("|")})`; @@ -2406,8 +2409,8 @@ function stripFlatTrace(text, names = []) { const found = trace.exec(body); return found ? body.slice(found[0].length) : body; } -function copyBodyOf(text, names = []) { - let body = String(text ?? ""); +function copyBodyOf(text2, names = []) { + let body = String(text2 ?? ""); for (let previous; previous !== body; ) { previous = body; body = stripHeader(body); @@ -2459,14 +2462,14 @@ function latestMemoryChange(news, ids) { const at = (id) => news?.[id]?.seen ? news[id].at ?? 0 : 0; return ids.reduce((best, id) => at(id) > at(best) ? id : best, ids[0] ?? null); } -function summaryBlocks(text) { +function summaryBlocks(text2) { const blocks = []; let paragraph = []; const flush = () => { if (paragraph.length > 0) blocks.push({ kind: "p", text: paragraph.join(" ") }); paragraph = []; }; - for (const raw of String(text ?? "").split("\n")) { + for (const raw of String(text2 ?? "").split("\n")) { const line2 = raw.trim().replace(/\*\*|__/g, ""); const heading = /^#{1,6}\s+(.+)$/.exec(line2); if (heading) { @@ -2581,17 +2584,17 @@ function previewOf(list, id, hiddenTurns = [], unsentTurns = [], names = []) { for (let index = outline.length - 1; index >= 0; index -= 1) { const entry = outline[index]; if (hiddenTurns.includes(entry.turn)) continue; - const text = (unsentTurns.includes(entry.turn) ? "" : entry.response) || entry.prompt || ""; - if (text === "") continue; - return readableLine(text, names); + const text2 = (unsentTurns.includes(entry.turn) ? "" : entry.response) || entry.prompt || ""; + if (text2 === "") continue; + return readableLine(text2, names); } return ""; } -function readableLine(text, names = []) { - return dropKnownTrace(collapsedLine(text), names); +function readableLine(text2, names = []) { + return dropKnownTrace(collapsedLine(text2), names); } -function collapsedLine(text) { - return visibleText(stripTrace(stripHeader(`${text}`)).replace(/\s+/g, " ").trim()); +function collapsedLine(text2) { + return visibleText(stripTrace(stripHeader(`${text2}`)).replace(/\s+/g, " ").trim()); } function dropKnownTrace(line2, names) { const known = [...new Set((Array.isArray(names) ? names : []).map((name) => String(name ?? "").trim()).filter((name) => name !== ""))].sort((a, b) => b.length - a.length); @@ -2605,8 +2608,8 @@ function dropKnownTrace(line2, names) { return /^[\s,,.。::]*$/.test(rest) ? "" : rest; } var botNames = (roster) => [...roster?.bots ?? [], ...roster?.remoteBots ?? [], ...roster?.agents ?? []].map((entry) => entry?.name).filter((name) => typeof name === "string" && name !== ""); -function speechOf(text, names = []) { - return readableLine(text, names).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim(); +function speechOf(text2, names = []) { + return readableLine(text2, names).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim(); } var formatTokens = (count) => count >= 1e6 ? `${(count / 1e6).toFixed(1)}M` : count >= 1e3 ? `${(count / 1e3).toFixed(1)}K` : String(count); function usageOf(list, ids) { @@ -2625,10 +2628,10 @@ function usageOf(list, ids) { return t("{tokens} tok · Cache hit {hit}%", { tokens: formatTokens(input + output), hit }); } function faceRowTitle(title) { - const text = String(title ?? ""); - const named = text.match(/^Face-to-face · (.+) · (\d{6})$/); + const text2 = String(title ?? ""); + const named = text2.match(/^Face-to-face · (.+) · (\d{6})$/); if (named) return `${named[2]} · ${named[1]}`; - return text.match(/^Face-to-face · (\d{6})$/)?.[1] ?? text; + return text2.match(/^Face-to-face · (\d{6})$/)?.[1] ?? text2; } function modelOf(bot, roster) { const ref = bot.model ?? bot.appliedModel; @@ -2942,8 +2945,8 @@ function RoomAvatar({ room: room2, roster, size = 36 }) { // src/client/room-invite-preview.js var INVITE_PREVIEW_NAME_MAX = 60; -function stripInviteMarkers(text) { - let out = String(text ?? ""); +function stripInviteMarkers(text2) { + let out = String(text2 ?? ""); for (let previous; previous !== out; ) { previous = out; out = out.replace(/<\/?\s*untrusted-(?:file|message)/gi, ""); @@ -2956,9 +2959,9 @@ function cleanInviteName(value, fallback = "") { function inviteMemberCount(value) { if (typeof value === "number") return Number.isFinite(value) ? value : NaN; if (typeof value !== "string") return NaN; - const text = value.trim(); - if (!text) return NaN; - const count = Number(text); + const text2 = value.trim(); + if (!text2) return NaN; + const count = Number(text2); return Number.isFinite(count) ? count : NaN; } function invitePreviewOf(seen) { @@ -2980,29 +2983,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: h48, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) { - if (!preview || typeof h48 !== "function") return null; +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; 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 h48( + return h49( "div", { className: "fs-preview", "data-flybot": at, role: "status" }, - h48("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"), - preview.lead ? h48("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null, - preview.people ? h48("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null, - preview.expiresAt ? h48("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null, - onJoin || onClear ? h48( + 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( "div", { className: "fs-preview-foot", "data-flybot": child("foot") }, - showClear && onClear ? h48("button", { + showClear && onClear ? h49("button", { type: "button", className: "fs-btn fs-btn-sm", "data-flybot": child("clear"), disabled: !!busy, onClick: onClear }, clearLabel) : null, - onJoin ? h48("button", { + onJoin ? h49("button", { type: "button", className: "fs-btn fs-btn-sm fs-btn-primary", "data-flybot": child("join"), @@ -3397,10 +3400,10 @@ var FLYBOT_CSS = ` `; // src/shared/failure-copy.js -var providerCode = (text) => { - const json = /"code"\s*:\s*"([A-Za-z0-9_.-]+)"/.exec(text); +var providerCode = (text2) => { + const json = /"code"\s*:\s*"([A-Za-z0-9_.-]+)"/.exec(text2); if (json) return json[1]; - const plain = /^([A-Za-z][A-Za-z0-9_.-]{2,40}):\s/.exec(text); + const plain = /^([A-Za-z][A-Za-z0-9_.-]{2,40}):\s/.exec(text2); return plain ? plain[1] : ""; }; var KNOWN = { @@ -3411,13 +3414,13 @@ var KNOWN = { InsufficientQuota: "The account is out of quota.", RateLimitExceeded: "Too many requests right now. Try again in a moment." }; -var fill2 = (text, params) => params === void 0 ? text : String(text).replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match); +var fill2 = (text2, params) => params === void 0 ? text2 : String(text2).replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match); function failureSentence(raw, translate = (key) => key) { - const text = String(raw ?? "").trim(); - if (text === "") return text; - const key = KNOWN[providerCode(text)]; - if (key === void 0) return text; - const feature = /does not support\s+([a-z][a-z0-9 _-]{2,40})/i.exec(text)?.[1]?.trim(); + const text2 = String(raw ?? "").trim(); + if (text2 === "") return text2; + const key = KNOWN[providerCode(text2)]; + if (key === void 0) return text2; + const feature = /does not support\s+([a-z][a-z0-9 _-]{2,40})/i.exec(text2)?.[1]?.trim(); if (key !== KNOWN.UnsupportedModel || feature === void 0) return fill2(translate(key), void 0); const withFeature = "This model does not support {feature}. Pick another model in the Bot's settings."; return fill2(translate(withFeature, { feature }), { feature }); @@ -3632,12 +3635,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 h48 = dt / steps; + const h49 = 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) * h48; - value.x += value.v * h48; + value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h49; + value.x += value.v * h49; } if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { value.x = value.to; @@ -4915,10 +4918,10 @@ div:has(> nav [class*="_marks"]){display:none!important} .bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} .bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} `; -function installStyles(text, tag) { +function installStyles(text2, tag) { const style = document.createElement("style"); style.dataset.dshBot = tag; - style.textContent = text; + style.textContent = text2; document.head.appendChild(style); return style; } @@ -4938,9 +4941,9 @@ function sheetClassNames(sheet) { } return names; } -function compileFragmentSelectors(text, names) { +function compileFragmentSelectors(text2, names) { const compiled = /* @__PURE__ */ new Map(); - return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { + return text2.replace(FRAGMENT_SELECTOR, (whole, fragment) => { if (!compiled.has(fragment)) { const exact = []; for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); @@ -4949,7 +4952,7 @@ function compileFragmentSelectors(text, names) { return compiled.get(fragment); }); } -function installCompiledStyles(text, tag) { +function installCompiledStyles(text2, tag) { const style = installStyles("", tag); const cache = /* @__PURE__ */ new WeakMap(); const render = () => { @@ -4963,7 +4966,7 @@ function installCompiledStyles(text, tag) { } for (const name of list) names.add(name); } - const next = compileFragmentSelectors(text, names); + const next = compileFragmentSelectors(text2, names); if (style.textContent !== next) style.textContent = next; }; render(); @@ -5060,11 +5063,11 @@ function flybotMarkSvg({ wing = "#8FB2FF", brand = "#3B5BFD", face = "#FFFFFF" } return ``; } function brandTitle(title) { - const text = String(title ?? ""); - if (text === "DeepSeek Harness" || text === "DS Bot") return "FlyBot"; - if (text.endsWith(" — DeepSeek Harness")) return `${text.slice(0, -" — DeepSeek Harness".length)} — FlyBot`; - if (/\bDS Bot\b/.test(text)) return text.replace(/\bDS Bot\b/g, "FlyBot"); - return text; + const text2 = String(title ?? ""); + if (text2 === "DeepSeek Harness" || text2 === "DS Bot") return "FlyBot"; + if (text2.endsWith(" — DeepSeek Harness")) return `${text2.slice(0, -" — DeepSeek Harness".length)} — FlyBot`; + if (/\bDS Bot\b/.test(text2)) return text2.replace(/\bDS Bot\b/g, "FlyBot"); + return text2; } function sessionTabLabel(roster, sessionId) { if (!roster || !sessionId) return ""; @@ -5543,14 +5546,14 @@ function ScopeSwitch({ value, onChange, bot }) { "div", { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, (0, import_react7.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), - options.map(([id, text]) => (0, import_react7.createElement)("button", { + options.map(([id, text2]) => (0, import_react7.createElement)("button", { key: id, type: "button", role: "radio", "aria-checked": value === id, className: "bt-secret-seg", onClick: () => onChange(id) - }, text)) + }, text2)) ); } function SecretHead({ bot, title, detail, name, onClose }) { @@ -5628,10 +5631,10 @@ function SecretCard({ bot, request, roster, actions }) { } function SettledSecret({ bot, request, roster }) { const canceled = request.status === "canceled"; - const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); + const text2 = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); return (0, import_react7.createElement)( "div", - { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, + { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text2, "data-status": request.status }, (0, import_react7.createElement)( "div", { className: "bt-q-head" }, @@ -5639,7 +5642,7 @@ function SettledSecret({ bot, request, roster }) { (0, import_react7.createElement)("div", { className: "bt-q-text" }, (0, import_react7.createElement)( "p", { className: "bt-q-title" }, - text, + text2, canceled ? null : (0, import_react7.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) )), canceled ? (0, import_react7.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react7.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react7.createElement)(CheckIcon)) @@ -5684,11 +5687,11 @@ function ScopeEditor({ secret, roster, onSave }) { (0, import_react7.createElement)( "div", { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, - [["all", t("All my Bots")], ["some", t("Chosen Bots")], ["none", t("No Bot")]].map(([id, text]) => (0, import_react7.createElement)( + [["all", t("All my Bots")], ["some", t("Chosen Bots")], ["none", t("No Bot")]].map(([id, text2]) => (0, import_react7.createElement)( "label", { key: id, className: "bt-secret-radio" }, (0, import_react7.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), - text + text2 )) ), mode === "some" ? (0, import_react7.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react7.createElement)("button", { @@ -6060,7 +6063,7 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, return (0, import_react9.createElement)( "div", { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, - texts2.map((text, index) => { + texts2.map((text2, index) => { const key = msgKey ? `${msgKey}:${index}` : void 0; const picked = key ? reactions?.[index] ?? [] : []; const last = index === texts2.length - 1; @@ -6075,9 +6078,9 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, (0, import_react9.createElement)( "div", { className: "bt-bubble" }, - (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) + (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text2, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) ), - live ? null : (0, import_react9.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) + live ? null : (0, import_react9.createElement)(MessageTools, { text: text2, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) ), picked.length ? (0, import_react9.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react9.createElement)("button", { key: emoji, @@ -6092,9 +6095,9 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, } var WAITING_OTHERS_NOTICE = "Waiting for the owner's approval"; var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; -function leaveNoticeCoveredByClose(room2, text) { +function leaveNoticeCoveredByClose(room2, text2) { if (room2?.origin !== "face-to-face" || room2.face?.status !== "closed") return false; - const match = String(text ?? "").trim().match(/^(.+) left(?: the room)?$/); + const match = String(text2 ?? "").trim().match(/^(.+) left(?: the room)?$/); if (!match) return false; const who = match[1]; for (const participant of room2.participants ?? []) { @@ -6125,7 +6128,7 @@ function useDismiss(open, close, refs) { }; }, [open]); } -function MessageTools({ text, name, sessionId, react, actions }) { +function MessageTools({ text: text2, name, sessionId, react, actions }) { const [panel, setPanel] = (0, import_react9.useState)(null); const [place, setPlace] = (0, import_react9.useState)(null); const [copied, setCopied] = (0, import_react9.useState)(false); @@ -6144,13 +6147,13 @@ function MessageTools({ text, name, sessionId, react, actions }) { setPanel(kind); }; const copy2 = () => { - void navigator.clipboard?.writeText(text).then(() => { + void navigator.clipboard?.writeText(text2).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1e3); }); }; const download2 = () => { - const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); + const url = URL.createObjectURL(new Blob([text2], { type: "text/markdown" })); const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); link.click(); setTimeout(() => URL.revokeObjectURL(url), 1e3); @@ -6163,7 +6166,7 @@ function MessageTools({ text, name, sessionId, react, actions }) { "div", { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, react ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react9.createElement)(SmileIcon)) : null, - sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react9.createElement)(ReplyIcon)) : null, + sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text2) }, (0, import_react9.createElement)(ReplyIcon)) : null, (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react9.createElement)(CheckIcon) : (0, import_react9.createElement)(DotsIcon)), panel === "react" ? (0, import_react_dom.createPortal)((0, import_react9.createElement)( "div", @@ -6196,14 +6199,14 @@ var clip = (line2) => { return kept + CUT; }; var onlyMarkup = (lines) => lines.length === 1 ? t("{n} line of reasoning", { n: lines.length }) : t("{n} lines of reasoning", { n: lines.length }); -var peekLine = (text) => { - const lines = text.split("\n").map((part) => part.replace(/\s+/g, " ").trim()).filter((part) => part !== ""); +var peekLine = (text2) => { + const lines = text2.split("\n").map((part) => part.replace(/\s+/g, " ").trim()).filter((part) => part !== ""); const prose = lines.find((line2) => !isMarkup(line2)); return prose === void 0 ? onlyMarkup(lines) : clip(prose); }; -function ThinkingRow({ text }) { - if (typeof text !== "string" || text.trim() === "") return null; - const peek = peekLine(text); +function ThinkingRow({ text: text2 }) { + if (typeof text2 !== "string" || text2.trim() === "") return null; + const peek = peekLine(text2); return (0, import_react9.createElement)( "details", { className: "bt-think" }, @@ -6214,7 +6217,7 @@ function ThinkingRow({ text }) { t("Thought it through"), peek ? (0, import_react9.createElement)("span", { className: "bt-think-peek" }, peek) : null ), - (0, import_react9.createElement)("div", { className: "bt-think-text" }, text) + (0, import_react9.createElement)("div", { className: "bt-think-text" }, text2) ); } function useFileMentions({ node, openFile, fileMentions, useTurnData }) { @@ -6235,7 +6238,7 @@ function useClosingText(node, useTurnData) { } function friendLead(bot, texts2) { if (bot?.remote === void 0 || texts2.length === 0) return void 0; - if (texts2.every((text) => /^Not sent:/.test(text.trim()))) return void 0; + if (texts2.every((text2) => /^Not sent:/.test(text2.trim()))) return void 0; return (0, import_react9.createElement)("span", { className: "bt-lead", title: bot.name, "data-flybot": "friend-lead" }, (0, import_react9.createElement)(BotMark, { bot, size: 22 })); } function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { @@ -6248,7 +6251,7 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use const closing = useClosingText(node, useTurnData); const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); const person = typedBy(roster, sessionId, data.turn); - const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block, index) => index === 0 ? stripTrace(block.text) : block.text).filter((text) => text.trim() !== ""); + const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block, index) => index === 0 ? stripTrace(block.text) : block.text).filter((text2) => text2.trim() !== ""); const running = data.status === "running"; useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); if (hidden) return null; @@ -6324,11 +6327,11 @@ function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { }); return (0, import_react9.createElement)("div", { className: "bt-event" }, parts); } -var heldForm = (text) => String(text ?? "").replace(/\s+/g, " ").trim().replace(/^(to (the )?face[ -]?room|to the room)\s*:\s*/i, "").trim(); +var heldForm = (text2) => String(text2 ?? "").replace(/\s+/g, " ").trim().replace(/^(to (the )?face[ -]?room|to the room)\s*:\s*/i, "").trim(); var sameName = (a, b) => String(a ?? "").replace(/\s+/g, " ").trim().toLowerCase() === String(b ?? "").replace(/\s+/g, " ").trim().toLowerCase(); -function isHeldLine(room2, text, bot) { +function isHeldLine(room2, text2, bot) { if (room2?.origin !== "face-to-face") return false; - const line2 = heldForm(text); + const line2 = heldForm(text2); if (!line2) return false; return (room2.pendingApprovals ?? []).some((item) => { if (item?.kind !== "face_room_message") return false; @@ -6338,9 +6341,9 @@ function isHeldLine(room2, text, bot) { }); } var HELD_SENDING_MS = 6e4; -function heldLineState(room2, { seq, botId, text } = {}, now = Date.now()) { +function heldLineState(room2, { seq, botId, text: text2 } = {}, now = Date.now()) { if (room2?.origin !== "face-to-face" || !Array.isArray(room2.heldLines)) return void 0; - const said = String(text ?? "").trim(); + const said = String(text2 ?? "").trim(); const entry = room2.heldLines.filter((item) => seq !== void 0 && item?.seq !== void 0 ? item.seq === seq : item?.botId === botId && String(item?.text ?? "").trim() === said).at(-1); if (entry === void 0) return void 0; if (entry.status === "rejected") return "rejected"; @@ -6348,8 +6351,8 @@ function heldLineState(room2, { seq, botId, text } = {}, now = Date.now()) { if (entry.status !== "pending" && entry.status !== "sending") return void 0; return room2.face?.status === "left" || room2.face?.status === "closed" ? "rejected" : "held"; } -function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi, seq }) { - const held = heldLineState(room2, { seq, botId: bot.id, text }); +function GroupMessage({ bot, text: text2, room: room2, roster, actions, msgKey, useUi, seq }) { + const held = heldLineState(room2, { seq, botId: bot.id, text: text2 }); if (held === "rejected") { return (0, import_react9.createElement)( "div", @@ -6358,7 +6361,7 @@ function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi, (0, import_react9.createElement)("span", null, t("{name} · Not sent", { name: bot.name })) ); } - if (held === "held" || isHeldLine(room2, text, bot)) return null; + if (held === "held" || isHeldLine(room2, text2, bot)) return null; const lead = (0, import_react9.createElement)( "button", { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, @@ -6374,7 +6377,7 @@ function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi, "div", { className: "bt-group-msg" }, name, - (0, import_react9.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) + (0, import_react9.createElement)(Bubbles, { texts: [text2], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) ); } function remoteSpeaker(source, roster, sessionId) { @@ -6409,7 +6412,7 @@ function faceSeatLook(room2, handle) { return Object.keys(look).length ? look : void 0; } var cleanCarrier = (value) => typeof value === "string" && value.trim() && !/[@()]/.test(value) ? value.trim() : ""; -function RemoteMessage({ source, text, roster, actions, sessionId, msgKey, useUi }) { +function RemoteMessage({ source, text: text2, roster, actions, sessionId, msgKey, useUi }) { const room2 = roster.roomsById?.[sessionId]; const face = room2?.origin === "face-to-face"; const { bot, member, name, handle, carrier, chatId } = remoteSpeaker(source, roster, sessionId); @@ -6426,7 +6429,7 @@ function RemoteMessage({ source, text, roster, actions, sessionId, msgKey, useUi person, carrier ? (0, import_react9.createElement)("span", { className: "bt-remote-handle" }, t("via {name}", { name: carrier })) : null ), - (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react9.createElement)("span", { className: "bt-lead", title: person }, (0, import_react9.createElement)(PersonMark, { name: person })) }), + (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text2)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react9.createElement)("span", { className: "bt-lead", title: person }, (0, import_react9.createElement)(PersonMark, { name: person })) }), files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null ); } @@ -6442,7 +6445,7 @@ function RemoteMessage({ source, text, roster, actions, sessionId, msgKey, useUi name, (0, import_react9.createElement)("span", { className: "bt-remote-handle", title: source.remote.home ?? handle, "aria-label": t("Friend's Bot") }, (0, import_react9.createElement)(GlobeMark), handle) ), - (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, 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 ); } @@ -7515,13 +7518,13 @@ function Face({ entry, size }) { } var elapsedSince = (now, at) => typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : ""; function WorkRow({ bot, tool, step, label, at, now, onOpen }) { - const text = `${label} · ${workText(tool, step)}`; + const text2 = `${label} · ${workText(tool, step)}`; const elapsed = elapsedSince(now, at); return (0, import_react12.createElement)( "button", - { type: "button", className: "bt-mw-row", "aria-label": [text, elapsed].filter(Boolean).join(" · "), onClick: onOpen }, + { type: "button", className: "bt-mw-row", "aria-label": [text2, elapsed].filter(Boolean).join(" · "), onClick: onOpen }, (0, import_react12.createElement)(BotMark, { bot, size: 22, state: toolState(tool ?? ""), live: true }), - (0, import_react12.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react12.createElement)(Shimmer, null, text)), + (0, import_react12.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react12.createElement)(Shimmer, null, text2)), elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null ); } @@ -7800,11 +7803,11 @@ var roomName = (roster, id) => roster.roomsById?.[id]?.name; var placeOf = (roster, roomId) => roomId && roomName(roster, roomId) ? t("in {name}", { name: roomName(roster, roomId) }) : ""; var lastWhy = (task) => [...task.actors ?? []].reverse().find((entry) => typeof entry.why === "string" && entry.why !== "")?.why ?? ""; var chatOfTask = (task) => task.origin?.roomId ?? task.origin?.sessionId ?? task.ownerBotId; -var ownAskChat = (roster, bot, text) => { - if (bot === void 0 || typeof text !== "string" || text === "") return void 0; +var ownAskChat = (roster, bot, text2) => { + if (bot === void 0 || typeof text2 !== "string" || text2 === "") return void 0; for (const [key, question] of Object.entries(roster.questions ?? {})) { if (question == null || typeof question !== "object" || roster.roomsById?.[key]) continue; - if (question.question !== text) continue; + if (question.question !== text2) continue; if (key === bot.id || key === currentPart(bot)) return bot.id; } return void 0; @@ -7820,7 +7823,7 @@ function latestActivity(card, { roster, work, sessions }) { if (entry) options.push(`${workPhrase(entry.tool)}…`); } if (sessions?.byId && card.chat) options.push(previewOf(sessions, partOf(roster, card.chat), [], [], botNames(roster))); - return options.find((text) => typeof text === "string" && text !== "" && text !== card.title && text !== card.reason) ?? ""; + return options.find((text2) => typeof text2 === "string" && text2 !== "" && text2 !== card.title && text2 !== card.reason) ?? ""; } function toolCalls(card, { roster, work }) { const botId = card.bot?.id; @@ -8005,9 +8008,9 @@ function inboxModel({ roster, work = {}, tasks = [], sessions, workflows = [], n const part = currentPart(bot); const at = sessions.byId[part]?.updatedAt; if (!fresh(at)) continue; - const text = previewOf(sessions, part, [], [], botNames(roster)); - if (text === "") continue; - done.push({ id: `chat:${bot.id}`, kind: "chat", status: "done", bot, at, title: text, place: "", chat: bot.id, ...people(roster, bot.id) }); + const text2 = previewOf(sessions, part, [], [], botNames(roster)); + if (text2 === "") continue; + done.push({ id: `chat:${bot.id}`, kind: "chat", status: "done", bot, at, title: text2, place: "", chat: bot.id, ...people(roster, bot.id) }); } } const newest = (a, b) => (b.at ?? Infinity) - (a.at ?? Infinity); @@ -8030,10 +8033,10 @@ function inboxAnswer(actions, card, choice) { return Promise.resolve(actions.answerQuestion(card.sessionId, card.question.id, { selected, custom })); } if (card?.kind === "task" && card.status === "input") { - const text = typeof choice === "string" ? choice.trim() : ""; + const text2 = typeof choice === "string" ? choice.trim() : ""; const questionId = card.task?.inputRequest?.id; - if (text === "" || typeof questionId !== "string") return void 0; - return Promise.resolve(actions.answerTaskInput(card.task.id, questionId, text)); + if (text2 === "" || typeof questionId !== "string") return void 0; + return Promise.resolve(actions.answerTaskInput(card.task.id, questionId, text2)); } if (card?.kind === "task" && card.status === "result") { if (choice === "accept") return Promise.resolve(actions.acceptTaskResult(card.task.id)); @@ -8449,8 +8452,8 @@ function progressOf(task) { // src/shared/board-card.js function taskCardKey(id) { - const text = String(id ?? "").trim(); - return text === "" ? null : `task:${text}`; + const text2 = String(id ?? "").trim(); + return text2 === "" ? null : `task:${text2}`; } function taskCard(task) { const key = taskCardKey(task?.id); @@ -8936,11 +8939,11 @@ function KanbanDialog({ roster, actions, leaving = false }) { } }; const create = async (title) => { - const text = String(title ?? "").trim(); - if (!text || scope?.kind !== "bot") return; + const text2 = String(title ?? "").trim(); + if (!text2 || scope?.kind !== "bot") return; setError(""); try { - replace(await actions.taskCreate({ botId: scope.botId, title: text, column: "not-started" })); + replace(await actions.taskCreate({ botId: scope.botId, title: text2, column: "not-started" })); setDraft(""); } catch (failure) { setError(failure?.message ?? String(failure)); @@ -9056,7 +9059,7 @@ function useMarkedUnread(id, active, marked, actions) { var PinMark = () => (0, import_react16.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react16.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) => { +var workKeys = (text2) => text2 === "" ? [] : text2.split(",").map((key) => { const [id, tool] = key.split("|"); return { id, tool: tool || void 0 }; }); @@ -9989,7 +9992,7 @@ function BusyAckBubble({ sessionId, useRoster, useBusyAcks, useSessionStatus, us const host = useRunningHost2(visible); const shown = (0, import_react17.useRef)(null); if (visible) shown.current = room2 ? { text: memberAck?.text ?? "", at: memberAck?.at ?? Date.now() } : { text: busyAckText(hostAck), at: hostAck?.at ?? Date.now() }; - const text = shown.current?.text ?? ""; + const text2 = shown.current?.text ?? ""; if (!visible || !host || !shown.current || !bot) return null; return (0, import_react_dom4.createPortal)((0, import_react17.createElement)( "div", @@ -9998,8 +10001,8 @@ function BusyAckBubble({ sessionId, useRoster, useBusyAcks, useSessionStatus, us "data-flybot": "busy-ack", role: "status", "aria-live": "polite", - "aria-busy": text ? void 0 : "true", - "data-pending": text ? void 0 : "" + "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)( @@ -10011,7 +10014,7 @@ function BusyAckBubble({ sessionId, useRoster, useBusyAcks, useSessionStatus, us (0, import_react17.createElement)( "div", { className: "bt-msg-line" }, - text ? (0, import_react17.createElement)("div", { className: "bt-bubble" }, text) : (0, import_react17.createElement)( + text2 ? (0, import_react17.createElement)("div", { className: "bt-bubble" }, text2) : (0, import_react17.createElement)( "div", { className: "bt-bubble bt-busy-dots", "aria-hidden": "true" }, (0, import_react17.createElement)("span", null), @@ -10395,10 +10398,10 @@ function useDictation(onText) { let interim = ""; for (let index = event.resultIndex; index < event.results.length; index += 1) { const result = event.results[index]; - const text = result[0]?.transcript ?? ""; + const text2 = result[0]?.transcript ?? ""; if (result.isFinal) { - if (text.trim()) latest.current(text, true); - } else interim += text; + if (text2.trim()) latest.current(text2, true); + } else interim += text2; } if (interim) latest.current(interim, false); }; @@ -10444,8 +10447,8 @@ function ComposerIdentity({ sessionId, useRoster }) { ); } function ComposerVoice({ sessionId, inputActions, actions, useUi }) { - const dictation = useDictation((text, final) => { - if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); + const dictation = useDictation((text2, final) => { + if (final && inputActions) inputActions.insertText(text2, inputActions.captureInsertion()); }); const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); (0, import_react19.useEffect)(() => { @@ -10813,8 +10816,8 @@ async function speechCatalog(speech) { function waveBytes(samples, rate = 16e3) { const bytes = new Uint8Array(44 + samples.length * 2); const view = new DataView(bytes.buffer); - const ascii = (at, text) => { - for (let index = 0; index < text.length; index += 1) bytes[at + index] = text.charCodeAt(index); + const ascii = (at, text2) => { + for (let index = 0; index < text2.length; index += 1) bytes[at + index] = text2.charCodeAt(index); }; ascii(0, "RIFF"); view.setUint32(4, 36 + samples.length * 2, true); @@ -10835,9 +10838,9 @@ function waveBytes(samples, rate = 16e3) { return bytes; } function base64Of(bytes) { - let text = ""; - for (let index = 0; index < bytes.length; index += 32768) text += String.fromCharCode(...bytes.subarray(index, index + 32768)); - return btoa(text); + let text2 = ""; + for (let index = 0; index < bytes.length; index += 32768) text2 += String.fromCharCode(...bytes.subarray(index, index + 32768)); + return btoa(text2); } var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); async function openCapture() { @@ -11145,7 +11148,7 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { void actions.refreshModels?.(); }).finally(() => setBusy("")); }; - const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); + const 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)( "div", @@ -11177,7 +11180,7 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { "div", { className: "bt-msg-line" }, (0, import_react20.createElement)(BotMark, { bot, size: 22 }), - (0, import_react20.createElement)("div", { className: "bt-bubble" }, text) + (0, import_react20.createElement)("div", { className: "bt-bubble" }, text2) ) ) ), @@ -11217,9 +11220,9 @@ 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")); } function missingModelKey(message) { - const text = String(message ?? ""); - if (!/\bno api key\b|\bmissing (?:an? )?api key\b|\bapi key (?:is )?(?:missing|not set|not configured)\b/i.test(text)) return void 0; - return { route: /provider route "([^"]+)"/i.exec(text)?.[1] ?? "" }; + const text2 = String(message ?? ""); + if (!/\bno api key\b|\bmissing (?:an? )?api key\b|\bapi key (?:is )?(?:missing|not set|not configured)\b/i.test(text2)) return void 0; + return { route: /provider route "([^"]+)"/i.exec(text2)?.[1] ?? "" }; } function modelKeyChoices(actions, route) { if (/^deepseek/i.test(route)) return setupChoices(actions); @@ -11230,8 +11233,8 @@ function TurnErrorCell({ node, t: chatT, actions }) { const code = typeof data.code === "string" && data.code ? data.code : void 0; const raw = String(data.message ?? ""); const shell = (key, fallback) => { - const text = typeof chatT === "function" ? chatT(key) : void 0; - return typeof text === "string" && text !== "" && text !== key ? text : fallback; + const text2 = typeof chatT === "function" ? chatT(key) : void 0; + return typeof text2 === "string" && text2 !== "" && text2 !== key ? text2 : fallback; }; const missing = missingModelKey(raw); const title = code === "ACCOUNT_SIGNED_OUT" ? shell("message.accountStopped", t("This turn failed")) : shell("message.turnError", t("This turn failed")); @@ -11617,8 +11620,8 @@ function timingText(task) { return `${t("Every day at {time}", { time: clock2(task.time) })}${zone}`; case "weekly": { const days = (task.weekdays ?? []).join(); - const text = days === "1,2,3,4,5" ? t("Every weekday at {time}", { time: clock2(task.time) }) : days === "6,7" ? t("Every weekend at {time}", { time: clock2(task.time) }) : days === "1,2,3,4,5,6,7" ? t("Every day at {time}", { time: clock2(task.time) }) : t("Every {days} at {time}", { days: weekdayNames(task.weekdays ?? []), time: clock2(task.time) }); - return `${text}${zone}`; + const text2 = days === "1,2,3,4,5" ? t("Every weekday at {time}", { time: clock2(task.time) }) : days === "6,7" ? t("Every weekend at {time}", { time: clock2(task.time) }) : days === "1,2,3,4,5,6,7" ? t("Every day at {time}", { time: clock2(task.time) }) : t("Every {days} at {time}", { days: weekdayNames(task.weekdays ?? []), time: clock2(task.time) }); + return `${text2}${zone}`; } case "cron": return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; @@ -12108,10 +12111,10 @@ var SCHEDULE_CSS = ` // src/client/capability-settings.js var import_react24 = 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) => { let value; try { - value = JSON.parse(text || "{}"); + value = JSON.parse(text2 || "{}"); } catch { throw new Error(t("{label} must be valid JSON.", { label })); } @@ -12538,11 +12541,11 @@ function Entries({ bot, view, remove }) { ); } function changeLine(change) { - const text = change.text.length > 120 ? `${change.text.slice(0, 120)}…` : change.text; - if (!change.ok) return t("Not changed: {text} ({reason})", { text, reason: hostText(change.error ?? "") }); - if (change.op === "remove") return change.scope === "team" ? t("Removed from team memory: {text}", { text }) : t("Removed: {text}", { text }); - if (change.op === "edit") return t("Updated: {text}", { text }); - return change.scope === "team" ? t("Added to team memory: {text}", { text }) : t("Added: {text}", { text }); + const text2 = change.text.length > 120 ? `${change.text.slice(0, 120)}…` : change.text; + if (!change.ok) return t("Not changed: {text} ({reason})", { text: text2, reason: hostText(change.error ?? "") }); + if (change.op === "remove") return change.scope === "team" ? t("Removed from team memory: {text}", { text: text2 }) : t("Removed: {text}", { text: text2 }); + if (change.op === "edit") return t("Updated: {text}", { text: text2 }); + 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)( @@ -12613,7 +12616,7 @@ function MemoryDialog({ bot, actions, leaving = false }) { const [page, setPage] = (0, import_react26.useState)("summary"); const [menu, setMenu] = (0, import_react26.useState)(false); const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2); - const [text, setText] = (0, import_react26.useState)(""); + 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); @@ -12665,7 +12668,7 @@ function MemoryDialog({ bot, actions, leaving = false }) { }; const ask = async (event) => { event.preventDefault(); - const asked = text.trim(); + const asked = text2.trim(); if (asked === "" || answer?.pending) return; setText(""); setAnswer({ asked, pending: true }); @@ -12731,7 +12734,7 @@ function MemoryDialog({ bot, actions, leaving = false }) { (0, import_react26.createElement)("input", { ref: inputRef, className: "bt-mem-input", - value: text, + value: text2, maxLength: 2e3, disabled: view === null, placeholder: t("Ask or update"), @@ -12740,7 +12743,7 @@ function MemoryDialog({ bot, actions, leaving = false }) { }), (0, import_react26.createElement)( "button", - { type: "submit", className: "bt-mem-send", disabled: busy || text.trim() === "", "aria-label": t("Send") }, + { 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) ) ) @@ -13675,11 +13678,11 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { const entry = outline[index2]; if (hidden.includes(entry.turn)) continue; for (const raw of [entry.response, entry.prompt]) { - const text = paletteText(raw, names); - const at = text.toLowerCase().indexOf(wanted); + const text2 = paletteText(raw, names); + const at = text2.toLowerCase().indexOf(wanted); if (at < 0) continue; const from = Math.max(0, at - 24); - hits.push([id, entry.turn, (from > 0 ? "…" : "") + text.slice(from, at + wanted.length + 60)]); + hits.push([id, entry.turn, (from > 0 ? "…" : "") + text2.slice(from, at + wanted.length + 60)]); break; } } @@ -13865,10 +13868,10 @@ function createPayload({ name, look, image, model, effort }) { if (effort) payload.effort = effort; return payload; } -async function afterCreate({ id, text, actions }) { +async function afterCreate({ id, text: text2, actions }) { actions.openSession(id); actions.showDetails(id, "settings"); - const line2 = String(text ?? "").trim(); + const line2 = String(text2 ?? "").trim(); if (line2 === "") return; try { await actions.send(id, line2); @@ -14130,27 +14133,27 @@ function NewChat({ mode, roster, actions }) { }; const makeGroup = async () => { if (admin === null || picked.length === 0) return; - const text = message.trim(); + const text2 = message.trim(); const { members, remotes } = splitRoomPicks(roster, [admin, ...picked]); const participants = remotes.length === 0 ? void 0 : [ ...members.map((id) => ({ kind: "bot", id, role: id === admin ? "admin" : "member" })), ...remotes ]; const room2 = await actions.createRoom(members, { admin, ...participants ? { participants } : {} }); - if (text) await actions.send(room2.id, text); + if (text2) await actions.send(room2.id, text2); actions.openSession(room2.id); close(); }; const createGroup = () => run(makeGroup); const sendMessage = () => run(async () => { - const text = message.trim(); + const text2 = message.trim(); if (group && admin !== null) { await makeGroup(); return; } - if (text === "" || picked.length === 0) return; + if (text2 === "" || picked.length === 0) return; if (picked.length === 1) { - await actions.send(picked[0], text); + await actions.send(picked[0], text2); actions.openSession(picked[0]); close(); return; @@ -14161,7 +14164,7 @@ function NewChat({ mode, roster, actions }) { ...remotes ]; const room2 = await actions.createRoom(members, participants ? { participants } : {}); - await actions.send(room2.id, text); + await actions.send(room2.id, text2); actions.openSession(room2.id); close(); }); @@ -14375,10 +14378,10 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); // src/client/exchange.js var import_react33 = require("react"); var SHEET_EXIT_MS = 220; -function exchangeText(text) { - if (typeof text !== "string") return ""; +function exchangeText(text2) { + if (typeof text2 !== "string") return ""; const CLOSE = ""; - let body = text; + let body = text2; for (let previous; previous !== body; ) { previous = body; const open = body.indexOf(" { - setHeard(text); + const submit = (text2) => { + setHeard(text2); setPhase("thinking"); waiting.current = reply.current.split("|")[0]; - void actions.send(sessionId, text.trim()).catch(() => setPhase("listening")); + void actions.send(sessionId, text2.trim()).catch(() => setPhase("listening")); }; - const dictation = useDictation((text, final) => { - setHeard(text); + const dictation = useDictation((text2, final) => { + setHeard(text2); if (!final) return; dictation.stop(); - submit(text); + submit(text2); }); (0, import_react34.useEffect)(() => { if (!note) return void 0; @@ -14601,9 +14604,9 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } setPhase("transcribing"); try { const wave = await current.finish(catalog.maxDurationSeconds); - const text = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : ""; + const text2 = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : ""; if (turn.current !== own) return; - if (text) submit(text); + if (text2) submit(text2); else setPhase("listening"); } catch (error) { if (turn.current !== own) return; @@ -14652,10 +14655,10 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } }, [speech, catalog, listen, cycle]); (0, import_react34.useEffect)(() => { if (phase !== "thinking" || running || waiting.current === null) return; - const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; - if (count === waiting.current || !text.trim()) return; + const [count, text2] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; + if (count === waiting.current || !text2.trim()) return; waiting.current = null; - const spoken = speechOf(text, botNames(roster)); + const spoken = speechOf(text2, botNames(roster)); setSaid(spoken); setHeard(""); if (!window.speechSynthesis || !spoken) { @@ -15404,8 +15407,8 @@ function summarize(view, range, keep = () => true, seriesOf = () => "all") { return result; } function trimmed2(value) { - const text = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2); - return text.includes(".") ? text.replace(/\.?0+$/, "") : text; + const text2 = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2); + return text2.includes(".") ? text2.replace(/\.?0+$/, "") : text2; } function compact(count) { for (const [size, unit] of [[1e9, "B"], [1e6, "M"], [1e3, "K"]]) { @@ -15537,14 +15540,14 @@ function kindLabel(kind, botFocus) { return labels[kind] ?? kind; } function Tabs({ label, value, options, onChange, boxRef }) { - const names = options.map(([, text]) => text).join("\n"); + const names = options.map(([, text2]) => text2).join("\n"); const glide = useGlide(`${value} ${names}`, { box: boxRef }); return (0, import_react36.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, text, icon3]) => (0, import_react36.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react36.createElement)(icon3) : null, text)) + 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)) ); } function Stats({ range, total, previous }) { @@ -16430,15 +16433,15 @@ function AppPicker({ apps, onChange, search, statuses = {} }) { const names = (0, import_react37.useRef)(new Map(APPS)); const asked = (0, import_react37.useRef)(0); (0, import_react37.useEffect)(() => { - const text = query.trim(); - if (text === "" || search === void 0) { + const text2 = query.trim(); + if (text2 === "" || search === void 0) { setFound(null); setFailed(null); return void 0; } const ask = ++asked.current; const timer = setTimeout(() => { - search(text).then((list) => { + search(text2).then((list) => { if (ask !== asked.current) return; for (const app of list) names.current.set(app.slug, app.name); setFound(list); @@ -16672,15 +16675,15 @@ var FeedbackIcon = () => (0, import_react38.createElement)( (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" }) ); -function download(name, text) { - const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" })); +function download(name, text2) { + const url = URL.createObjectURL(new Blob([text2], { type: "text/plain;charset=utf-8" })); const anchor = document.createElement("a"); anchor.href = url; anchor.download = name; anchor.click(); setTimeout(() => URL.revokeObjectURL(url), 1e3); } -var copy = (text) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text).then(() => true, () => false); +var copy = (text2) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text2).then(() => true, () => false); function whenText2(time) { const date = new Date(time); const today = (/* @__PURE__ */ new Date()).toDateString() === date.toDateString(); @@ -16728,8 +16731,8 @@ function FeedbackPage({ actions, run, Section, Row: Row4 }) { if (!inline && !copied) setPreview(true); })); }; - const downloadLog = () => run(actions.diagnosticsLog().then((text) => { - download("ds-bot-diagnostics.log", text || "No entries.\n"); + const downloadLog = () => run(actions.diagnosticsLog().then((text2) => { + download("ds-bot-diagnostics.log", text2 || "No entries.\n"); setNote(t("Downloaded the log. Keys, tokens, and your home folder are hidden in it.")); })); const clearLog = () => run(actions.diagnosticsClear().then(clearClientErrors).then(load).then(() => setNote(t("Cleared the error log.")))); @@ -17239,13 +17242,13 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false "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, text]) => (0, import_react40.createElement)("button", { + pages.map(([key, text2]) => (0, import_react40.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(text))), + }, (0, import_react40.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", { @@ -17610,16 +17613,16 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow (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")) ); } -function About({ text, title = "About" }) { +function About({ text: text2, title = "About" }) { const [open, setOpen] = (0, import_react42.useState)(false); - const long = text.split("\n").length > 5 || text.length > 240; + const long = text2.split("\n").length > 5 || text2.length > 240; const body = (0, import_react42.useRef)(null); useMorph(body, open); return (0, import_react42.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 }, text), + (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 ); } @@ -17806,12 +17809,12 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a return false; }); const share = () => { - const text = transcript; - if (!text) { + const text2 = transcript; + if (!text2) { actions.notify?.(t("Nothing to copy yet")); return; } - void copiedToClipboard(text).then((written) => { + void copiedToClipboard(text2).then((written) => { if (!written) { actions.reportError?.(new Error(t("Could not copy the conversation")), "share"); return; @@ -18187,11 +18190,11 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) }, [tabId, landed]); const go = (event) => { event.preventDefault(); - const text = draft ?? ""; - if (text.trim() === "") return; + const text2 = draft ?? ""; + if (text2.trim() === "") return; setDraft(null); input.current?.blur(); - void actions.browserNavigate(tabId, text).then((opened) => { + void actions.browserNavigate(tabId, text2).then((opened) => { if (opened) actions.browserFocus(tabId); }); }; @@ -18735,19 +18738,19 @@ function readPage(limits) { const auto = String(el.getAttribute("autocomplete") || "").toLowerCase(); return el.type === "password" || /(^|\s)(cc-|one-time-code)/.test(auto) || SECRET_HINT.test(`${el.getAttribute("name") || ""} ${el.id || ""}`); }; - const keep = (text2, end = "") => { - if (text2 === "") return ""; - stash.push(text2); + const keep = (text3, end = "") => { + if (text3 === "") return ""; + stash.push(text3); return ` ${stash.length - 1}${end} `; }; - const expand = (text2) => text2.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); - const flow = (text2) => text2.split("\n").map((line2) => line2.replace(/[ \t\f\v\u00a0\u2028\u2029]+/g, " ").trim().replace(/^· /, "")).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\u0003\n\n(?=\u0001\d+\u0003)/g, "\n").trim(); - const block = (text2) => expand(flow(text2)); - const oneLine = (text2) => block(text2).replace(/\s*\n+\s*/g, " ").trim(); + const expand = (text3) => text3.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); + const flow = (text3) => text3.split("\n").map((line2) => line2.replace(/[ \t\f\v\u00a0\u2028\u2029]+/g, " ").trim().replace(/^· /, "")).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\u0003\n\n(?=\u0001\d+\u0003)/g, "\n").trim(); + const block = (text3) => expand(flow(text3)); + const oneLine = (text3) => block(text3).replace(/\s*\n+\s*/g, " ").trim(); const styleOf = (el) => (el.ownerDocument.defaultView || window).getComputedStyle(el); const childrenOf = (el) => { if (el.shadowRoot) return el.shadowRoot.childNodes; @@ -18781,8 +18784,8 @@ function readPage(limits) { items.push(item); return { item, number: items.length, secret }; }; - const nameOf = (el, text2, secret) => collapse( - el.getAttribute("aria-label") || text2 || (el.localName === "input" && !secret && NO_VALUE_TYPES.has(el.type) ? el.value : "") || el.getAttribute("placeholder") || el.title || el.getAttribute("alt") || el.querySelector?.("img[alt]")?.alt || el.getAttribute("name") || "" + const nameOf = (el, text3, secret) => collapse( + el.getAttribute("aria-label") || text3 || (el.localName === "input" && !secret && NO_VALUE_TYPES.has(el.type) ? el.value : "") || el.getAttribute("placeholder") || el.title || el.getAttribute("alt") || el.querySelector?.("img[alt]")?.alt || el.getAttribute("name") || "" ).slice(0, 80); const fenced = (code, lang) => { const longest = Math.max(2, ...(code.match(/`{3,}/g) || []).map((run) => run.length)); @@ -18851,12 +18854,12 @@ ${children(cell, ctx)} for (const cell of row.cells) { place(); if (styleOf(cell).display === "none") continue; - const text2 = cellText(cell, ctx); + const text3 = cellText(cell, ctx); const span = Math.max(1, Math.min(limits.tableCols, Number(cell.colSpan) || 1)); const down = Math.max(1, Math.min(limits.tableRows, Number(cell.rowSpan) || 1)); for (let offset = 0; offset < span; offset += 1) { - line2[column] = offset === 0 ? text2 : ""; - if (down > 1) carry[column] = { text: offset === 0 ? text2 : "", left: down - 1 }; + line2[column] = offset === 0 ? text3 : ""; + if (down > 1) carry[column] = { text: offset === 0 ? text3 : "", left: down - 1 }; column += 1; } } @@ -18921,9 +18924,9 @@ ${body2 || "(no text)"} if (node.nodeType === 3) { if (!ctx.shown) return ""; const data = node.data.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, ""); - const text2 = ctx.pre ? data : data.replace(/[\t\n\r ]+/g, " "); - chars += text2.length; - return text2; + const text3 = ctx.pre ? data : data.replace(/[\t\n\r ]+/g, " "); + chars += text3.length; + return text3; } if (node.nodeType !== 1) return ""; const el = node; @@ -19011,12 +19014,12 @@ ${marked} return marked; } const body = document.body; - let text = ""; + let text2 = ""; if (body) { - text = block(render(body, { depth: 0, frames: 0, shown: true, pre: false })).replace(/\u00a0/g, " "); + text2 = block(render(body, { depth: 0, frames: 0, shown: true, pre: false })).replace(/\u00a0/g, " "); } - if (text.length > limits.chars) { - text = text.slice(0, limits.chars); + if (text2.length > limits.chars) { + text2 = text2.slice(0, limits.chars); stop ||= `the text passed ${limits.chars} characters`; } const page = { @@ -19026,7 +19029,7 @@ ${marked} description: document.querySelector('meta[name="description"]')?.content || "", selection: String(getSelection() || "").slice(0, 4e3), format: "markdown", - text, + text: text2, items, stats: { ...stats, ms: Math.round(performance.now() - started) } }; @@ -19054,15 +19057,15 @@ var METADATA_HOST = /^(169\.254\.\d+\.\d+|\[fd00:ec2::254\]|metadata\.google\.in var BLOCKED = "Cloud metadata addresses do not open here."; var blockedAddress = (url) => URL.canParse(url) && METADATA_HOST.test(new URL(url).hostname); function browserAddress(input) { - const text = String(input ?? "").trim(); - if (text === "") return null; - if (/^[a-z][a-z\d+.-]*:\/\//i.test(text)) return /^https?:\/\//i.test(text) && URL.canParse(text) ? new URL(text).href : null; - if (/^(about|javascript|data|file|blob|chrome|devtools|view-source):/i.test(text)) return null; - const host = text.split(/[/?#]/)[0]; + const text2 = String(input ?? "").trim(); + if (text2 === "") return null; + if (/^[a-z][a-z\d+.-]*:\/\//i.test(text2)) return /^https?:\/\//i.test(text2) && URL.canParse(text2) ? new URL(text2).href : null; + if (/^(about|javascript|data|file|blob|chrome|devtools|view-source):/i.test(text2)) return null; + const host = text2.split(/[/?#]/)[0]; const local = /^(localhost|127(\.\d+){3}|10(\.\d+){3}|192\.168(\.\d+){2}|172\.(1[6-9]|2\d|3[01])(\.\d+){2}|\[[\da-f:]+\])(:\d+)?$/i.test(host); - const looksLikeHost = !/\s/.test(text) && (local || /^[^\s.]+(\.[^\s.]+)+(:\d+)?$/.test(host)); - if (looksLikeHost && URL.canParse(`http://${text}`)) return new URL(`${local ? "http" : "https"}://${text}`).href; - return `${SEARCH_URL}${encodeURIComponent(text)}`; + const looksLikeHost = !/\s/.test(text2) && (local || /^[^\s.]+(\.[^\s.]+)+(:\d+)?$/.test(host)); + if (looksLikeHost && URL.canParse(`http://${text2}`)) return new URL(`${local ? "http" : "https"}://${text2}`).href; + return `${SEARCH_URL}${encodeURIComponent(text2)}`; } function createBotBrowsers({ bridge, createSource: createSource2, storage = globalThis.localStorage, document: doc = globalThis.document }) { const views = /* @__PURE__ */ new Map(); @@ -19542,8 +19545,8 @@ function appFrameOf(doc) { const frame = col?.parentElement; if (!frame) return null; const name = frame.className; - const text = typeof name === "string" ? name : name?.baseVal ?? ""; - return text.includes("_frame") ? frame : null; + const text2 = typeof name === "string" ? name : name?.baseVal ?? ""; + return text2.includes("_frame") ? frame : null; } function levelShell(doc, from) { const page = doc.scrollingElement || doc.documentElement; @@ -19584,6 +19587,123 @@ function watchShellLevel(doc) { }; } +// src/client/project-board.js +var import_react46 = require("react"); +var PROJECT_BOARD_CSS = ` +.bt-project-board{padding:16px 18px} +.bt-project-group{margin:0 0 14px} +.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} +.bt-project-row{display:flex;gap:10px;align-items:center;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px;cursor:default} +.bt-project-row[data-column="done"]{opacity:.7} +.bt-project-row .bt-project-ref{margin-left:auto;font-size:11px;opacity:.6} +.bt-project-empty{padding:22px 4px;opacity:.7} +.bt-project-actions{display:flex;gap:8px;margin:0 0 14px} +`; +var PROJECT_SOURCES = ["bot", "group", "workflow"]; +var SOURCE_LABELS = { bot: "Bots", group: "Groups", workflow: "Workflows" }; +var text = (value) => typeof value === "string" ? value.trim() : ""; +function projectBoardView({ project, rows, canManage } = {}) { + const list = Array.isArray(rows) ? rows.filter((row) => row !== null && typeof row === "object" && text(row.id) !== "") : []; + const groups = PROJECT_SOURCES.map((source) => ({ + source, + label: SOURCE_LABELS[source], + rows: list.filter((row) => row.source === source).map((row) => ({ + id: text(row.id), + key: `task:${text(row.id)}`, + column: text(row.column) || "not-started", + assignee: text(row.assignee) || null, + runId: text(row.runId) || null, + nodeId: text(row.nodeId) || null + })) + })).filter((group) => group.rows.length > 0); + return { + projectId: text(project?.id) || null, + title: text(project?.name) || "Project", + canManage: canManage === true, + groups, + count: list.length, + empty: list.length === 0, + emptyText: "Nothing under this Project yet." + }; +} +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)( + "section", + { key: group.source, className: "bt-project-group", "data-source": group.source }, + (0, import_react46.createElement)("h4", null, t(group.label)), + group.rows.map((row) => (0, import_react46.createElement)( + "div", + { + key: row.key, + className: "bt-project-row", + "data-task-id": row.id, + "data-column": row.column, + role: "button", + tabIndex: 0, + onClick: () => onOpen?.(row.id), + onKeyDown: (event) => { + if (event.key === "Enter" || event.key === " ") onOpen?.(row.id); + } + }, + (0, import_react46.createElement)("span", { className: "bt-project-column" }, t(row.column)), + (0, import_react46.createElement)("span", { className: "bt-project-title" }, row.assignee ? `@${row.assignee}` : "—"), + row.runId === null ? null : (0, import_react46.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`) + )) + )); +} +function ProjectBoardPane({ project, rows, canManage, onOpen }) { + const view = projectBoardView({ project, rows, canManage }); + return (0, import_react46.createElement)( + "div", + { className: "bt-project-board", "data-can-manage": view.canManage ? "true" : "false" }, + (0, import_react46.createElement)("h3", null, view.title), + // Management affordances are drawn only for the owner; the refusal itself is the host's. + view.canManage ? (0, import_react46.createElement)("div", { className: "bt-project-actions" }, (0, import_react46.createElement)("button", { type: "button" }, t("Project settings")), (0, import_react46.createElement)("button", { type: "button" }, t("Members"))) : null, + (0, import_react46.createElement)(ProjectBoardRows, { view, onOpen }) + ); +} +function ProjectBoardOverlay() { + const [state, setState] = (0, import_react46.useState)(null); + const close = (0, import_react46.useCallback)(() => { + if (typeof window !== "undefined") window.location.hash = ""; + setState(null); + }, []); + (0, import_react46.useEffect)(() => { + if (typeof window === "undefined") return; + if (!window.location.hash.startsWith("#project")) return; + const wanted = window.location.hash.slice("#project".length).replace(/^=/, ""); + let live = true; + void (async () => { + try { + const listing = await call("projects"); + const projects = listing?.projects ?? []; + const project = wanted === "" ? projects[0] : projects.find((item) => item.id === wanted); + if (project === void 0) { + if (live) setState({ project: null, rows: [], canManage: false }); + return; + } + const [read, board] = await Promise.all([call("project-read", { id: project.id }), call("project-board", { id: project.id })]); + if (live) setState({ project: read?.project ?? project, rows: board?.tasks ?? [], canManage: read?.canManage === true }); + } catch { + if (live) setState({ project: null, rows: [], canManage: false }); + } + })(); + return () => { + live = false; + }; + }, []); + if (state === null) return null; + return (0, import_react46.createElement)( + "div", + { className: "bt-overlay bt-project-overlay", role: "dialog", "aria-label": t("Project board") }, + (0, import_react46.createElement)("button", { type: "button", className: "bt-project-close", onClick: close }, t("Close")), + state.project === null ? (0, import_react46.createElement)("div", { className: "bt-project-empty" }, t("No Project here.")) : (0, import_react46.createElement)(ProjectBoardPane, { project: state.project, rows: state.rows, canManage: state.canManage, onOpen: (id) => { + window.location.hash = `#task=${id}`; + } }) + ); +} + // src/client/friend-lines.js function friendLineNews(bots, known, isOpen, selfId) { const next = { ...known }; @@ -19653,7 +19773,7 @@ function writeMode(mode, storage = globalThis.localStorage) { } // src/client/user-message.js -var import_react46 = require("react"); +var import_react47 = require("react"); var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1); var canDrawUserMessages = () => typeof ui.projectUserText === "function"; function contentParts(content) { @@ -19725,58 +19845,58 @@ function messageAuthor(data, roster, sessionId) { mine: roster?.self?.memberId === authorId }; } -function userTextParts(text, bots, referenceLabels = []) { +function userTextParts(text2, bots, referenceLabels = []) { const parts = []; let cursor = 0; - for (const span of mentionSpans(text, bots, { references: referenceLabels })) { + for (const span of mentionSpans(text2, bots, { references: referenceLabels })) { if (!span.bot) continue; - if (span.start > cursor) parts.push({ text: text.slice(cursor, span.start) }); - parts.push({ bot: span.bot, label: text.slice(span.start + 1, span.end) }); + if (span.start > cursor) parts.push({ text: text2.slice(cursor, span.start) }); + parts.push({ bot: span.bot, label: text2.slice(span.start + 1, span.end) }); cursor = span.end; - if (text.startsWith(" ", cursor)) cursor += 1; + if (text2.startsWith(" ", cursor)) cursor += 1; } - if (cursor < text.length) parts.push({ text: text.slice(cursor) }); + if (cursor < text2.length) parts.push({ text: text2.slice(cursor) }); return parts; } -function UserText({ text, bots, referenceLabels, skillNames, references, actions }) { - const chip = (part) => [(0, import_react46.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`]; - return userTextParts(text, bots, referenceLabels).map((part, index) => { - if (!part.bot) return (0, import_react46.createElement)(import_react46.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}`]; + 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)); const title = `@${part.bot.name}`; - return typeof part.bot.id === "string" ? (0, import_react46.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react46.createElement)("span", { key: index, className: "bt-umention", title }, chip(part)); + 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)); }); } function Attachments({ attachments, renderMessageImages }) { const compact2 = attachments.length > 1; - return (0, import_react46.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => { + return (0, import_react47.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => { if (block.type === "image") { - return renderMessageImages ? (0, import_react46.createElement)(import_react46.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; } const file = block.attachment; const meta = [ui.fileExtension?.(file.name)?.toUpperCase().slice(0, 8), ui.fileSizeText?.(file.bytes)].filter(Boolean).join(" · "); - return (0, import_react46.createElement)( + return (0, import_react47.createElement)( "span", { key: index, className: "bt-ufile", title: file.name }, - ui.FileTypeIcon ? (0, import_react46.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null, - (0, import_react46.createElement)("span", { className: "bt-ufile-text" }, (0, import_react46.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react46.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) ); })); } -function CopyButton({ text }) { - const [copied, setCopied] = (0, import_react46.useState)(false); +function CopyButton({ text: text2 }) { + const [copied, setCopied] = (0, import_react47.useState)(false); const copy2 = () => { - void navigator.clipboard?.writeText(text).then(() => { + void navigator.clipboard?.writeText(text2).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1e3); }); }; - return (0, import_react46.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_react46.createElement)(CheckIcon) : (0, import_react46.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)); } 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 }) { const roster = useRoster((value) => value); const data = node.data ?? {}; - const { text, attachments, rest } = contentParts(data.content); + const { text: text2, attachments, rest } = contentParts(data.content); const referenceLabels = data.referenceLabels ?? []; const references = openFile && openSkill ? { openFile, openSkill } : void 0; const steering = node.kind === "steering"; @@ -19787,7 +19907,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_react46.createElement)( + return (0, import_react47.createElement)( "div", { className: incoming ? "bt-umsg bt-umsg-other" : "bt-umsg", @@ -19797,22 +19917,22 @@ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageIma "data-author-kind": author?.authorKind, "data-author-name": author?.name }, - person ? (0, import_react46.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react46.createElement)(PersonMark, { name: author.name })) : null, - faceMine ? (0, import_react46.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react46.createElement)(PersonMark, { name: myName || void 0 })) : null, - (0, import_react46.createElement)( + 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)( "div", { className: "bt-ustack" }, - faceMine && myName ? (0, import_react46.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react46.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react46.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null, - attachments.length > 0 ? (0, import_react46.createElement)(Attachments, { attachments, renderMessageImages }) : null, - text !== "" || rest.length > 0 ? (0, import_react46.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)( "div", { className: "bt-ubody", title: clock3(data.time) }, - (0, import_react46.createElement)(UserText, { text, bots: mentionableBots(roster, sessionId, { fromFriend: Boolean(incoming && author.ownerName) }), referenceLabels, skillNames: data.skillNames ?? [], references, actions }), - rest.map((block, index) => ui.JsonBlock ? (0, import_react46.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) ) : null, - referenceLabels.length > 0 ? (0, import_react46.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 ), - text === "" ? null : (0, import_react46.createElement)("div", { className: "bt-utools" }, (0, import_react46.createElement)(CopyButton, { text })) + text2 === "" ? null : (0, import_react47.createElement)("div", { className: "bt-utools" }, (0, import_react47.createElement)(CopyButton, { text: text2 })) ); } var USER_MESSAGE_CSS = ` @@ -19910,33 +20030,33 @@ function noticeApprovals({ work, roster, shownSession, openSession, since: since } // src/client/stats-dock.js -var import_react47 = require("react"); +var import_react48 = require("react"); var STATS_DOCK = "conversation.composer.dock"; var STATS_ID = "stats"; var STATS_IDS = [STATS_ID, "activity"]; var noun = (count, one, many) => `${count} ${Number(count) === 1 ? one : many}`; function pluralStats(t2) { return (key, params) => { - const text = t2(key, params); - if (key !== "stats.counts" || !params) return text; + const text2 = t2(key, params); + if (key !== "stats.counts" || !params) return text2; const { turns, steps } = params; - return text === `${turns} turns ${steps} steps` ? `${noun(turns, "turn", "turns")} ${noun(steps, "step", "steps")}` : text; + return text2 === `${turns} turns ${steps} steps` ? `${noun(turns, "turn", "turns")} ${noun(steps, "step", "steps")}` : text2; }; } 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_react47.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]); + const t2 = (0, import_react48.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_react47.createElement)(entry.component, { ...props, t: t2 }); + return (0, import_react48.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_react48 = require("react"); +var import_react49 = require("react"); var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout"]; function apply(ctx) { const flybot = createClientApi2(); @@ -20526,7 +20646,7 @@ function apply(ctx) { } return { ...state, reactions }; }), - quote: (sessionId, text) => ui2.set((state) => ({ ...state, quote: { sessionId, text, token: Date.now() } })), + quote: (sessionId, text2) => ui2.set((state) => ({ ...state, quote: { sessionId, text: text2, token: Date.now() } })), clearQuote: (token) => ui2.set((state) => state.quote?.token === token ? { ...state, quote: null } : state), clearUnread: (id) => setUnread(id, false), setFlags: (id, flags) => after(call("set-flags", { id, ...flags })), @@ -20554,7 +20674,7 @@ function apply(ctx) { setPrefs: (prefs) => after(call("set-prefs", prefs)), setMain: (id, main = true, options = {}) => after(call("set-main", { id, main, ...options })), updateRoom: (id, patch) => after(call("update-room", { id, ...patch })), - send: (sessionId, text) => after(call("send", { sessionId: partFor(sessionId), text })), + send: (sessionId, text2) => after(call("send", { sessionId: partFor(sessionId), text: text2 })), answerQuestion: (sessionId, questionId, answer) => after(call("answer-question", { sessionId: partFor(sessionId), questionId, answer })), answerTaskInput: (id, questionId, answer) => after(call("task-answer-input", { id, questionId, answer })), // The Done list is the acceptance surface (#401): a result is accepted, or sent back with a note @@ -20581,7 +20701,7 @@ function apply(ctx) { connectorBlock: (id, openId) => after(call("connector-block", { id, openId })), connectorDisconnect: (id) => after(call("connector-disconnect", { id })), connectorRetry: (id) => after(call("connector-retry", { id })), - connectorSend: (id, text) => after(call("connector-send", { id, text })), + connectorSend: (id, text2) => after(call("connector-send", { id, text: text2 })), saveCapability: (capability) => after(call("capability-save", capability)), deleteCapability: (name) => after(call("capability-delete", { name })), approveCapability: (name) => after(call("capability-approve", { name })), @@ -20604,7 +20724,7 @@ function apply(ctx) { usage: () => call("usage", { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }), // `summarize` starts a summary when the entries changed; `regenerate` always does. memoryView: (botId, flags = {}) => call("memory-view", { botId, ...flags }), - memoryAsk: (botId, text) => call("memory-ask", { botId, text }), + memoryAsk: (botId, text2) => call("memory-ask", { botId, text: text2 }), memoryForget: (botId, entry) => call("memory-forget", { botId, scope: entry.scope, topic: entry.topic, text: entry.text }), diagnostics: (payload) => call("diagnostics", payload), diagnosticsLog: () => call("diagnostics-log"), @@ -20615,7 +20735,7 @@ function apply(ctx) { ui2.set((state) => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } })); }, // A short confirmation (e.g. "Invite link copied") in the same toast, without the error styling. - notify: (text) => ui2.set((state) => ({ ...state, toast: { text: String(text), tone: "info", token: Date.now() } })), + notify: (text2) => ui2.set((state) => ({ ...state, toast: { text: String(text2), tone: "info", token: Date.now() } })), dismissToast: (token) => ui2.set((state) => state.toast && (token === void 0 || state.toast.token === token) ? { ...state, toast: null } : state) }; if (browsers) { @@ -20834,6 +20954,7 @@ function apply(ctx) { installCompiledStyles(USER_MESSAGE_CSS, "user-messages"), installCompiledStyles(MOBILE_CSS, "mobile"), installCompiledStyles(PANE_MIN_WIDTH_CSS, "panes"), + installCompiledStyles(PROJECT_BOARD_CSS, "project-board"), typeof document !== "undefined" ? watchShellLevel(document) : null, browsers ? installCompiledStyles(BROWSER_CSS, "browser") : null, installTheme(), @@ -20851,7 +20972,8 @@ function apply(ctx) { shadow("shell.overlay", { id: "bot", order: 50, inject: face }, Overlays), shadow("shell.overlay", { id: "bot-signin", order: 55, inject: face }, DeepSeekSignIn), shadow("shell.overlay", { id: "bot-toast", order: 60, inject: face }, ErrorToast), - shadow("shell.overlay", { id: "flybot-approvals", order: 58, inject: face }, FlyBotApprovals) + shadow("shell.overlay", { id: "flybot-approvals", order: 58, inject: face }, FlyBotApprovals), + shadow("shell.overlay", { id: "project-board", order: 57 }, ProjectBoardOverlay) ]; const installConversation = () => [ installCompiledStyles(CONVERSATION_CSS, "conversation"), @@ -20904,7 +21026,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_react48.createElement)(TurnFailures, { ...props, work }) }); + const off = registry().register("flybot.session.dock", { id: "flybot-turn-failures", order: 20, component: (props) => (0, import_react49.createElement)(TurnFailures, { ...props, work }) }); return () => { off(); style.remove(); diff --git a/core/src/client/i18n.js b/core/src/client/i18n.js index c2443864d..8828e6593 100644 --- a/core/src/client/i18n.js +++ b/core/src/client/i18n.js @@ -1307,6 +1307,9 @@ const ZH = { 'Up one level': '上一级', 'No folders inside.': '里面没有文件夹。', 'Some folders are not shown.': '还有一些文件夹没有显示。', + 'Project settings': '项目设置', + 'Project board': '项目看板', + 'No Project here.': '这里没有项目。', } // Host errors stay English, since Bots read them too; they are translated where the diff --git a/core/src/client/index.js b/core/src/client/index.js index f57018f37..399c7db53 100644 --- a/core/src/client/index.js +++ b/core/src/client/index.js @@ -37,6 +37,7 @@ import { readWorkflows } from './workflows.js' 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 { STORAGE, migrateStorage } from './storage.js' import { friendLineNews } from './friend-lines.js' import { groupsFor, readMode, surfaceGroups, writeMode } from './surface.js' @@ -891,6 +892,7 @@ export function apply(ctx) { installCompiledStyles(USER_MESSAGE_CSS, 'user-messages'), installCompiledStyles(MOBILE_CSS, 'mobile'), installCompiledStyles(PANE_MIN_WIDTH_CSS, 'panes'), + installCompiledStyles(PROJECT_BOARD_CSS, 'project-board'), typeof document !== 'undefined' ? watchShellLevel(document) : null, browsers ? installCompiledStyles(BROWSER_CSS, 'browser') : null, installTheme(), @@ -909,6 +911,7 @@ export function apply(ctx) { shadow('shell.overlay', { id: 'bot-signin', order: 55, inject: face }, DeepSeekSignIn), shadow('shell.overlay', { id: 'bot-toast', order: 60, inject: face }, ErrorToast), shadow('shell.overlay', { id: 'flybot-approvals', order: 58, inject: face }, FlyBotApprovals), + shadow('shell.overlay', { id: 'project-board', order: 57 }, ProjectBoardOverlay), ] const installConversation = () => [ installCompiledStyles(CONVERSATION_CSS, 'conversation'), diff --git a/core/src/client/project-board.js b/core/src/client/project-board.js new file mode 100644 index 000000000..192f6fc19 --- /dev/null +++ b/core/src/client/project-board.js @@ -0,0 +1,117 @@ +// The Project board view (#1710): one Project, the rows its read model already aggregated +// (host `project-board`), grouped by where the work lives. The rows are the tasks themselves — +// `row.id` is the underlying task id — so opening one opens that task and nothing is copied here. +import { createElement as h, useCallback, useEffect, useState } from 'react' +import { call } from './api.js' +import { t } from './i18n.js' + +export const PROJECT_BOARD_CSS = ` +.bt-project-board{padding:16px 18px} +.bt-project-group{margin:0 0 14px} +.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} +.bt-project-row{display:flex;gap:10px;align-items:center;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px;cursor:default} +.bt-project-row[data-column="done"]{opacity:.7} +.bt-project-row .bt-project-ref{margin-left:auto;font-size:11px;opacity:.6} +.bt-project-empty{padding:22px 4px;opacity:.7} +.bt-project-actions{display:flex;gap:8px;margin:0 0 14px} +` + +// Sources in the order the board reads them; a source with no rows is left out so three empty +// groups never crowd out the one that has work. +export const PROJECT_SOURCES = ['bot', 'group', 'workflow'] +export const SOURCE_LABELS = { bot: 'Bots', group: 'Groups', workflow: 'Workflows' } + +const text = value => (typeof value === 'string' ? value.trim() : '') + +// Pure: no fetch, no React, no store. `rows` is the host's read model, `canManage` comes from +// `project-read` and only decides what the caller may *ask* for — the host refuses regardless. +export function projectBoardView({ project, rows, canManage } = {}) { + const list = Array.isArray(rows) ? rows.filter(row => row !== null && typeof row === 'object' && text(row.id) !== '') : [] + const groups = PROJECT_SOURCES + .map(source => ({ + source, + label: SOURCE_LABELS[source], + rows: list + .filter(row => row.source === source) + .map(row => ({ + id: text(row.id), + key: `task:${text(row.id)}`, + column: text(row.column) || 'not-started', + assignee: text(row.assignee) || null, + runId: text(row.runId) || null, + nodeId: text(row.nodeId) || null, + })), + })) + .filter(group => group.rows.length > 0) + return { + projectId: text(project?.id) || null, + title: text(project?.name) || 'Project', + canManage: canManage === true, + groups, + count: list.length, + empty: list.length === 0, + emptyText: 'Nothing under this Project yet.', + } +} + +function ProjectBoardRows({ view, onOpen }) { + // An empty Project is a state, not an error: the view says so and renders no groups. + if (view.empty) return h('div', { className: 'bt-project-empty' }, t(view.emptyText)) + return view.groups.map(group => h('section', { key: group.source, className: 'bt-project-group', 'data-source': group.source }, + h('h4', null, t(group.label)), + group.rows.map(row => h('div', { + key: row.key, + className: 'bt-project-row', + 'data-task-id': row.id, + 'data-column': row.column, + role: 'button', + tabIndex: 0, + onClick: () => onOpen?.(row.id), + onKeyDown: event => { if (event.key === 'Enter' || event.key === ' ') onOpen?.(row.id) }, + }, + h('span', { className: 'bt-project-column' }, t(row.column)), + h('span', { className: 'bt-project-title' }, row.assignee ? `@${row.assignee}` : '—'), + row.runId === null ? null : h('span', { className: 'bt-project-ref' }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`))))) +} + +export function ProjectBoardPane({ project, rows, canManage, onOpen }) { + const view = projectBoardView({ project, rows, canManage }) + return h('div', { className: 'bt-project-board', 'data-can-manage': view.canManage ? 'true' : 'false' }, + h('h3', null, view.title), + // Management affordances are drawn only for the owner; the refusal itself is the host's. + view.canManage ? h('div', { className: 'bt-project-actions' }, h('button', { type: 'button' }, t('Project settings')), h('button', { type: 'button' }, t('Members'))) : null, + h(ProjectBoardRows, { view, onOpen })) +} + +// The pane entry point (#1710): `#project=` opens one Project's board, `#project` the first. +// Reading is a POST through the same host channel every other view uses. +export function ProjectBoardOverlay() { + const [state, setState] = useState(null) + const close = useCallback(() => { + if (typeof window !== 'undefined') window.location.hash = '' + setState(null) + }, []) + useEffect(() => { + if (typeof window === 'undefined') return + if (!window.location.hash.startsWith('#project')) return + const wanted = window.location.hash.slice('#project'.length).replace(/^=/, '') + let live = true + void (async () => { + try { + const listing = await call('projects') + const projects = listing?.projects ?? [] + const project = wanted === '' ? projects[0] : projects.find(item => item.id === wanted) + if (project === undefined) { if (live) setState({ project: null, rows: [], canManage: false }); return } + const [read, board] = await Promise.all([call('project-read', { id: project.id }), call('project-board', { id: project.id })]) + if (live) setState({ project: read?.project ?? project, rows: board?.tasks ?? [], canManage: read?.canManage === true }) + } catch { if (live) setState({ project: null, rows: [], canManage: false }) } + })() + return () => { live = false } + }, []) + if (state === null) return null + return h('div', { className: 'bt-overlay bt-project-overlay', role: 'dialog', 'aria-label': t('Project board') }, + h('button', { type: 'button', className: 'bt-project-close', onClick: close }, t('Close')), + state.project === null + ? h('div', { className: 'bt-project-empty' }, t('No Project here.')) + : h(ProjectBoardPane, { project: state.project, rows: state.rows, canManage: state.canManage, onOpen: id => { window.location.hash = `#task=${id}` } })) +} diff --git a/core/src/host/channel.js b/core/src/host/channel.js index dc2e03879..60817a898 100644 --- a/core/src/host/channel.js +++ b/core/src/host/channel.js @@ -673,7 +673,7 @@ export function install(rt) { case 'project-of': case 'project-board': await rt.load() - return ok(await rt.projectEndpoint(endpoint, body)) + return ok(await rt.projectEndpoint(endpoint, body, actor)) case 'share-brief': await rt.load() await rt.setBriefShare(String(body.id ?? ''), String(body.botId ?? ''), body.share === true, actor) diff --git a/core/src/host/store.js b/core/src/host/store.js index 172f54f1c..7a08cbc96 100644 --- a/core/src/host/store.js +++ b/core/src/host/store.js @@ -433,16 +433,42 @@ export function install(rt) { if (project !== null) project.updatedAt = Math.max(project.updatedAt, Date.now()) return project } - const projectEndpoint = async (endpoint, body = {}) => { + // Who may act on a Project (#1710). The record names its owner member; an install with no + // session falls back to the first member, exactly like `requesterOf` in tasks.js. The host + // decides this, never the page: a caller that is not the owner may read and nothing else. + const projectActorId = actor => projectText(actor?.memberId, 120) || Object.keys(state.members ?? {})[0] || null + const projectCanManage = (project, actor) => { + if (project === null || typeof project !== 'object') return false + const owner = projectText(project.ownerUserId, 120) + if (owner === '') return true + const actorId = projectActorId(actor) + return actorId !== null && actorId === owner + } + const requireProjectOwner = (project, actor) => { + if (project === null) return + if (projectCanManage(project, actor)) return + throw projectRefusal('Only the owner of this Project may change it', 'bot/forbidden') + } + + const projectEndpoint = async (endpoint, body = {}, actor = null) => { switch (endpoint) { case 'projects': return { projects: listProjects() } case 'project-create': { - const project = createProject(body) + // The caller is stamped as the owner; a member id from the body cannot mint a + // Project someone else manages. + const project = createProject({ ...body, ownerUserId: projectActorId(actor) ?? undefined }) await save() return { project } } - case 'project-read': return { project: readProject(projectText(body.id, 200)) } + case 'project-read': { + const project = readProject(projectText(body.id, 200)) + return { project, canManage: projectCanManage(project, actor) } + } case 'project-bind': { + const target = body.projectId === undefined || body.projectId === null + ? projectOfMember(projectText(body.kind, 20), projectText(body.id, 200)) + : requireProject(projectText(body.projectId, 200)) + requireProjectOwner(target, actor) const project = bindProject(body) await save() return { project } diff --git a/core/test/project-board-view.test.js b/core/test/project-board-view.test.js new file mode 100644 index 000000000..f42af09b2 --- /dev/null +++ b/core/test/project-board-view.test.js @@ -0,0 +1,107 @@ +// The Project board view (#1710): one view over the read model Stage 2.2 shipped. It groups the +// rows by where the work lives, carries the run/node a workflow row came from, and keeps the row +// id equal to the underlying task id, so opening a row opens that task and no row is a copy. +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/project-board.js', import.meta.url))], + bundle: true, format: 'cjs', platform: 'node', write: false, + external: ['react', 'react-dom'], +}) +const module = { exports: {} } +runInNewContext(bundle.outputFiles[0].text, { + module, exports: module.exports, setTimeout, clearTimeout, setInterval, clearInterval, Date, Promise, console, + require: (name) => { + const platform = { + react: { + createElement: (type, props, ...children) => ({ type, props: props ?? {}, children: children.flat(Infinity) }), + useCallback: fn => fn, + useEffect: () => {}, + useState: initial => [initial, () => {}], + }, + } + if (!(name in platform)) throw new Error(`no platform module ${name}`) + return platform[name] + }, +}) +const { ProjectBoardPane, projectBoardView } = module.exports + +const rows = [ + { id: 'task-b1', source: 'bot', column: 'in-progress', assignee: 'Dev4', updatedAt: 3 }, + { id: 'task-g1', source: 'group', column: 'done', assignee: 'Chief', updatedAt: 2 }, + { id: 'task-w1', source: 'workflow', column: 'not-started', assignee: null, runId: 'run-7', nodeId: 'build', updatedAt: 1 }, +] + +const all = (node, out = []) => { + if (Array.isArray(node)) { for (const item of node) all(item, out); return out } + if (node === null || typeof node !== 'object') return out + // The fake createElement does not render components; the test asks them for their tree. + if (typeof node.type === 'function') return all(node.type(node.props), out) + out.push(node) + all(node.children, out) + return out +} +// Arrays minted inside the vm have another realm's prototype, so compare plain copies. +const plain = value => JSON.parse(JSON.stringify(value)) +const textOf = node => (typeof node === 'string' ? node : node && typeof node === 'object' ? (node.children ?? []).map(textOf).join('') : '') + +test('the board groups rows from all three sources and keeps each task id', () => { + const view = plain(projectBoardView({ project: { id: 'project-1', name: 'FlyBot' }, rows, canManage: true })) + assert.equal(view.projectId, 'project-1') + assert.equal(view.title, 'FlyBot') + assert.equal(view.canManage, true) + assert.equal(view.empty, false) + assert.equal(view.count, 3) + assert.deepEqual(view.groups.map(group => group.source), ['bot', 'group', 'workflow']) + assert.deepEqual(view.groups.map(group => group.rows.length), [1, 1, 1]) + const ids = view.groups.flatMap(group => group.rows.map(row => row.id)) + assert.deepEqual(ids, ['task-b1', 'task-g1', 'task-w1'], 'the row id is the task id') + assert.deepEqual(view.groups.flatMap(group => group.rows.map(row => row.key)), ['task:task-b1', 'task:task-g1', 'task:task-w1']) +}) + +test('a workflow row carries its run and node; other rows carry none', () => { + const view = plain(projectBoardView({ project: { id: 'p', name: 'P' }, rows, canManage: false })) + const [botRow] = view.groups[0].rows + const workflowRow = view.groups[2].rows[0] + assert.equal(botRow.runId, null) + assert.equal(botRow.nodeId, null) + assert.equal(workflowRow.runId, 'run-7') + assert.equal(workflowRow.nodeId, 'build') + assert.equal(workflowRow.assignee, null) + assert.equal(botRow.column, 'in-progress') +}) + +test('a Project with nothing under it is an empty state, not an error', () => { + const view = plain(projectBoardView({ project: { id: 'p', name: 'P' }, rows: [], canManage: false })) + assert.equal(view.empty, true) + assert.deepEqual(view.groups, []) + assert.equal(view.count, 0) + const missing = plain(projectBoardView({})) + assert.equal(missing.empty, true) + assert.equal(missing.projectId, null) + assert.equal(missing.canManage, false, 'no canManage answer is read as read-only') +}) + +test('the pane draws the grouped rows and only the owner sees the management controls', () => { + const owner = all(ProjectBoardPane({ project: { id: 'p', name: 'FlyBot' }, rows, canManage: true })) + const member = all(ProjectBoardPane({ project: { id: 'p', name: 'FlyBot' }, rows, canManage: false })) + assert.match(textOf(owner[0]), /FlyBot/) + const ownerRows = owner.filter(node => node.props?.['data-task-id'] !== undefined) + assert.deepEqual(ownerRows.map(node => node.props['data-task-id']), ['task-b1', 'task-g1', 'task-w1']) + const workflow = ownerRows[2] + assert.match(textOf(workflow), /run-7 · build/) + assert.equal(owner.some(node => textOf(node) === 'Project settings'), true) + const memberRows = member.filter(node => node.props?.['data-task-id'] !== undefined) + assert.deepEqual(memberRows.map(node => node.props['data-task-id']), ['task-b1', 'task-g1', 'task-w1'], 'a member still reads the board') + assert.equal(member.some(node => textOf(node) === 'Project settings'), false) +}) + +test('an empty Project renders the empty text and no row', () => { + const nodes = all(ProjectBoardPane({ project: { id: 'p', name: 'P' }, rows: [], canManage: false })) + assert.equal(nodes.filter(node => node.props?.['data-task-id'] !== undefined).length, 0) + assert.match(nodes.map(textOf).join(' '), /Nothing under this Project yet/) +}) diff --git a/core/test/projects.test.js b/core/test/projects.test.js index 4c6e29242..fd3457c9f 100644 --- a/core/test/projects.test.js +++ b/core/test/projects.test.js @@ -5,15 +5,31 @@ import assert from 'node:assert/strict' import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { createHash } from 'node:crypto' import { test } from 'node:test' import { apply } from '../index.js' import { fakeContext } from './fake-ctx.js' -const seed = () => ({ +const hashToken = token => createHash('sha256').update(token).digest('hex') + +// #1710: a second member and two signed-in sessions, so an owner and a non-owner caller both +// exist without a second process. +const seed = (sessions = true) => ({ version: 2, revision: 1, mainBotIds: ['a'], - members: { 'owner-1': { id: 'owner-1', createdAt: 1, designatedChiefId: 'a' } }, + members: { + 'owner-1': { id: 'owner-1', createdAt: 1, designatedChiefId: 'a' }, + 'owner-2': { id: 'owner-2', createdAt: 2, designatedChiefId: null }, + }, + ...(sessions + ? { + authSessions: { + 's-a': { id: 's-a', memberId: 'owner-1', tokenHash: hashToken('tok-a'), createdAt: 1 }, + 's-b': { id: 's-b', memberId: 'owner-2', tokenHash: hashToken('tok-b'), createdAt: 2 }, + }, + } + : {}), bots: { a: { id: 'a', name: 'Chief', role: 'Boss', sessionId: 'a', createdAt: 1, ownerId: 'owner-1' }, b: { id: 'b', name: 'Writer', role: 'Write', sessionId: 'b', createdAt: 2, ownerId: 'owner-1' }, @@ -22,8 +38,8 @@ const seed = () => ({ agents: {}, exchanges: [], exchangeTurns: {}, questions: {}, prefs: {}, grants: {}, }) -async function start(home = mkdtempSync(join(tmpdir(), 'ds-bot-projects-'))) { - if (!existsSync(join(home, 'state.json'))) writeFileSync(join(home, 'state.json'), JSON.stringify(seed())) +async function start(home = mkdtempSync(join(tmpdir(), 'ds-bot-projects-')), { sessions = true } = {}) { + if (!existsSync(join(home, 'state.json'))) writeFileSync(join(home, 'state.json'), JSON.stringify(seed(sessions))) const ctx = fakeContext() const agents = new Map() const agentOf = (sessionId) => { @@ -41,7 +57,11 @@ async function start(home = mkdtempSync(join(tmpdir(), 'ds-bot-projects-'))) { const register = ctx.connection.fetch.register ctx.connection.fetch.register = (options) => { route = options; return register(options) } apply(ctx, { home }) - const call = async (endpoint, payload) => (await route.fetch(new Request('http://localhost/api/bot', { method: 'POST', body: JSON.stringify({ endpoint, payload }) }))).json() + const call = async (endpoint, payload, token = 'tok-a') => (await route.fetch(new Request('http://localhost/api/bot', { + method: 'POST', + headers: token ? { 'x-flybot-member-session': token } : {}, + body: JSON.stringify({ endpoint, payload }), + }))).json() const stop = async ({ keepHome = false } = {}) => { await ctx.dispose() if (!keepHome) rmSync(home, { recursive: true, force: true, maxRetries: 5 }) @@ -167,7 +187,8 @@ test('an id that is not an own key resolves as absent and never writes Object.pr '({}).projectId': ({}).projectId ?? null, 'state.json bots.b has own projectId': Object.hasOwn(saved.bots.b, 'projectId'), }) - assert.deepEqual(protoRead.value, { project: null }, observed) + // #1710: project-read also answers canManage; an absent Project is never manageable. + assert.deepEqual(protoRead.value, { project: null, canManage: false }, observed) assert.equal(protoBind.ok, false, observed) assert.equal(protoBind.error?.code, 'bot/not-found', observed) assert.deepEqual(protoOf.value, { project: null }, observed) @@ -209,3 +230,42 @@ test('a run binds under an own key, not through the prototype chain (#1704)', as assert.equal(value(of).project?.id, project.id, observed) } finally { await stop() } }) + +// #1710: the owner of a Project may manage it, a member reads the same board read-only, and the +// refusal comes from the host action — not from a control the page happens to hide. +test('the caller becomes the owner, and only the owner may change the Project', async () => { + const { call, stop } = await start() + try { + const project = created(await call('project-create', { name: 'Owned', repository: 'acme/owned', ownerUserId: 'owner-2' }, 'tok-a')) + assert.equal(project.ownerUserId, 'owner-1', 'the caller is stamped as the owner, not the body') + + assert.equal(value(await call('project-read', { id: project.id }, 'tok-a')).canManage, true) + assert.equal(value(await call('project-read', { id: project.id }, 'tok-b')).canManage, false) + + const refused = await call('project-bind', { kind: 'bot', id: 'a', projectId: project.id }, 'tok-b') + assert.equal(refused.ok, false) + assert.equal(refused.error.code, 'bot/forbidden') + assert.equal(value(await call('project-of', { kind: 'bot', id: 'a' })).project, null, 'the refusal wrote nothing') + + assert.equal(created(await call('project-bind', { kind: 'bot', id: 'a', projectId: project.id }, 'tok-a')).id, project.id) + } finally { await stop() } +}) + +test('an install with no signed-in session falls back to its first member as the owner', async () => { + const { call, stop } = await start(undefined, { sessions: false }) + try { + const project = created(await call('project-create', { name: 'Solo', repository: 'acme/solo' })) + assert.equal(project.ownerUserId, 'owner-1', 'the single-member install stamps its member') + assert.equal(value(await call('project-read', { id: project.id })).canManage, true) + } finally { await stop() } +}) + +test('a signed-in member may read the Project board even though it may not manage it', async () => { + const { call, stop } = await start() + try { + const project = created(await call('project-create', { name: 'Board', repository: 'acme/board' }, 'tok-a')) + const board = await call('project-board', { id: project.id }, 'tok-b') + assert.equal(board.ok, true, JSON.stringify(board), 'project-board is a known endpoint once someone signed in') + assert.deepEqual(value(board).tasks, []) + } finally { await stop() } +}) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index 440ba288e..eba477e9e 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -266,3 +266,14 @@ A discarded `.catch(() => {})` in the client sources either starts with `void` ( - A friend's line in my Bot's own chat (#420): `friend-lines.js` `friendLineNews` compares each Bot's host stamp `friendLineAt` with the last stamp this browser has shown (`localStorage` `ds-bot:friend-lines`; stamps against stamps, never this browser's clock) and, when a newer one arrives while that chat is not visited, `index.js` marks the Bot unread the way **Mark as Unread** does, so the row shows the blue dot until the chat is opened. A browser with no record yet takes what is there as shown; another member's shared Bot and a friend's Bot chat are skipped. The Main Bot tile shows the same dot (`.bt-tile-unread`; a wait still shows on its avatar). A line whose source has `remote.firstContact` opens with one muted `FirstContact` line (`cells.js`): ` is talking to through Bot Social`. Covered by `core/test/friend-lines.test.js` and `core/test/inbound-ui.test.js`. - The roster poll (`index.js` `pull`) sends `watching: !document.hidden` with each `state` call (#497), so the host knows a window is on screen and the social plugin reads friends' inboxes at its live cadence: a friend's first line, its held reply's card, the Inbox count and the unread badge arrive in that window within seconds, with no reload. - The **NEW** divider (`cells.js` `TurnTailCell` / `TurnProcessCell` / the unread opening) compares turn times with the visit's `seen` (when the chat was last left) and `entered` (when this visit opened). Once the divider has been on screen (`NewMessagePills` in `header.js`, `view.divider === 'in-view'`) and a shown turn begins after the visit opened (`movedOnSince`: a friend's next line, the user's own message; hidden exchange turns do not count), `actions.settleVisit` sets `seen` to `entered` (`settledVisit`) and every divider of the visit goes, as on leaving and coming back. A divider still scrolled out of view, or a chat where nothing new happened, keeps it until the visit ends. Same rule in every chat, including a friend's typed line in the Bot's own chat (#275). Covered by `core/test/turn-tail.test.js`. + +### Project board pane (#1710) + +`src/client/project-board.js` draws one Project over the read model the host already aggregates +(`project-board`). `projectBoardView({ project, rows, canManage })` is pure: it groups the rows by +`source` (`bot` | `group` | `workflow`, empty sources left out), carries `column`, `assignee` and the +`runId`/`nodeId` a workflow row came from, and keeps `row.id` equal to the underlying task id, so +opening a row opens that task and the view never copies a row. An empty Project renders the empty +state, not an error. `canManage` comes from `project-read` and only decides whether the management +affordances are drawn; the refusal itself is the host's. The pane is registered on the existing +`shell.overlay` surface and opens at `#project=`. diff --git a/docs/subsystems/core-host.md b/docs/subsystems/core-host.md index 58b666041..b17a4376a 100644 --- a/docs/subsystems/core-host.md +++ b/docs/subsystems/core-host.md @@ -560,3 +560,16 @@ A discarded `.catch(() => {})` in the host sources either starts with `void` (fi - That placeholder names the file when the Bot's own file tools can open it: `attachmentFilePath` takes the attachment store's `imageHostPath` through the file sandbox's `processPathFromHostPath` (the same mapping `dsh-spill-policy` applies to spooled tool output) and falls back to the local object layout `/attachments/v1/objects//`. Either candidate must `stat` as a regular file, so a Bot is never sent after a file that is not there (a host path the sandbox cannot open is worse than no path), and only a `sha256:<64 hex>` id has an object path at all. - Nothing is copied into a Bot's working folder: a copy would need a lifecycle and a cleanup policy of its own, and the readable path already lets a Bot move or submit the bytes itself. - Covered by `core/test/images.test.js` and, through the Session hook and the group relay, `core/test/session-identity.test.js`. + +### The Project board and who may act on it (#1710) + +`projectEndpoint` takes the authenticated actor. `project-create` stamps the caller's member id as +`ownerUserId` (an `ownerUserId` in the body cannot mint a Project someone else manages); an install +with no signed-in session falls back to the first member, the same rule `tasks.js` uses for +`requesterOf`. `project-read` answers `{ project, canManage }`, where `canManage` is true only for +the owner — or for every caller on a Project whose record names no owner. `project-bind` is refused +by the host with `bot/forbidden` unless the caller may manage the Project the binding names (or the +one it is leaving, when unbinding); the refusal writes nothing, so hiding a button is never the +gate. Reading the board (`project-board`) stays open to a non-owner: the same rows, read-only. +`project-board` is listed in `authz.js`'s `TASK_ENDPOINTS`, so a signed-in deployment does not +refuse it as an unknown endpoint. diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch index bea3c42ce..3534bc8e3 100644 --- a/patches/ds-bot/0001-core-fixes.patch +++ b/patches/ds-bot/0001-core-fixes.patch @@ -638,7 +638,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': '新对话', @@ -706,7 +706,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: '管理员', @@ -728,7 +728,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} 位成员', @@ -805,7 +805,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}', @@ -865,7 +865,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': '没有这个连接器', @@ -934,7 +934,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 @@ -943,7 +943,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}', @@ -968,7 +968,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': '分享对话', @@ -985,7 +985,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.': '从下一条消息开始生效。', @@ -1011,7 +1011,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: '管理', @@ -1019,7 +1019,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': '管理模型', @@ -1028,7 +1028,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': '返回群聊列表', @@ -1041,7 +1041,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': '标为未读', @@ -1049,7 +1049,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', @@ -1058,7 +1058,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 @@ -1078,7 +1078,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,75 @@ const ZH = { +@@ -360,9 +360,75 @@ 'DSH update': 'DSH 更新', 'DSH {version}': 'DSH {version}', 'Restart DSH to start using DSH {version}.': '重启 DSH 后开始使用 DSH {version}。', @@ -1155,7 +1155,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: '更多', @@ -1169,7 +1169,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': '收到消息:', @@ -1179,7 +1179,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': '在做了', @@ -1188,7 +1188,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}」', @@ -1196,7 +1196,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} 等你批准', @@ -1229,7 +1229,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: '提交', @@ -1237,7 +1237,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 @@ -1246,7 +1246,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…': '保存中…', @@ -1254,7 +1254,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: '值', @@ -1263,7 +1263,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} 的记忆', @@ -1271,7 +1271,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 @@ -1286,7 +1286,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.': '这种安装方式不提供更新。', @@ -1300,7 +1300,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', @@ -1309,7 +1309,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.': '没有错误记录。', @@ -1318,7 +1318,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 @@ -1340,7 +1340,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}', @@ -1362,7 +1362,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: '天', @@ -1370,7 +1370,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 里打开「自动化任务」', @@ -1393,7 +1393,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': '选一个时间', @@ -1413,7 +1413,17 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js 'Working folder': '工作文件夹', 'Working folder: {folder}': '工作文件夹:{folder}', Folder: '文件夹', -@@ -853,8 +853,11 @@ const HOST_ERRORS = [ +@@ -838,6 +838,9 @@ + 'Up one level': '上一级', + 'No folders inside.': '里面没有文件夹。', + 'Some folders are not shown.': '还有一些文件夹没有显示。', ++ 'Project settings': '项目设置', ++ 'Project board': '项目看板', ++ 'No Project here.': '这里没有项目。', + } + + // Host errors stay English, since Bots read them too; they are translated where the +@@ -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 个字符'], @@ -1425,7 +1435,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js [/^HTTP (\d+)$/, '请求失败(HTTP $1)'], [/^GitHub answered (\d+)$/, 'GitHub 返回了 $1'], [/^Keep the name under (\d+) characters$/, '名字不能超过 $1 个字'], -@@ -883,6 +883,20 @@ const HOST_ZH = { +@@ -883,6 +883,20 @@ 'Unknown Bot': '找不到这个 Bot', 'Unknown conversation': '找不到这个对话', 'A group chat needs at least two Bots': '群聊至少要两个 Bot', @@ -1446,7 +1456,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 d2768b383..fc718dd33 100644 --- a/patches/ds-bot/0002-flybot-api-host.patch +++ b/patches/ds-bot/0002-flybot-api-host.patch @@ -835,7 +835,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,188 @@ +@@ -161,12 +161,214 @@ migrated = true } // Unnamed groups from before names followed their members may still list old names. @@ -971,16 +971,42 @@ diff --git a/src/host/store.js b/src/host/store.js + if (project !== null) project.updatedAt = Math.max(project.updatedAt, Date.now()) + return project + } -+ const projectEndpoint = async (endpoint, body = {}) => { ++ // Who may act on a Project (#1710). The record names its owner member; an install with no ++ // session falls back to the first member, exactly like `requesterOf` in tasks.js. The host ++ // decides this, never the page: a caller that is not the owner may read and nothing else. ++ const projectActorId = actor => projectText(actor?.memberId, 120) || Object.keys(state.members ?? {})[0] || null ++ const projectCanManage = (project, actor) => { ++ if (project === null || typeof project !== 'object') return false ++ const owner = projectText(project.ownerUserId, 120) ++ if (owner === '') return true ++ const actorId = projectActorId(actor) ++ return actorId !== null && actorId === owner ++ } ++ const requireProjectOwner = (project, actor) => { ++ if (project === null) return ++ if (projectCanManage(project, actor)) return ++ throw projectRefusal('Only the owner of this Project may change it', 'bot/forbidden') ++ } ++ ++ const projectEndpoint = async (endpoint, body = {}, actor = null) => { + switch (endpoint) { + case 'projects': return { projects: listProjects() } + case 'project-create': { -+ const project = createProject(body) ++ // The caller is stamped as the owner; a member id from the body cannot mint a ++ // Project someone else manages. ++ const project = createProject({ ...body, ownerUserId: projectActorId(actor) ?? undefined }) + await save() + return { project } + } -+ case 'project-read': return { project: readProject(projectText(body.id, 200)) } ++ case 'project-read': { ++ const project = readProject(projectText(body.id, 200)) ++ return { project, canManage: projectCanManage(project, actor) } ++ } + case 'project-bind': { ++ const target = body.projectId === undefined || body.projectId === null ++ ? projectOfMember(projectText(body.kind, 20), projectText(body.id, 200)) ++ : requireProject(projectText(body.projectId, 200)) ++ requireProjectOwner(target, actor) + const project = bindProject(body) + await save() + return { project } diff --git a/patches/ds-bot/0003-flybot-api-client.patch b/patches/ds-bot/0003-flybot-api-client.patch index cead3e9b7..2b79168c0 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,69 @@ +@@ -1,44 +1,70 @@ +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' @@ -1400,6 +1400,7 @@ diff --git a/src/client/index.js b/src/client/index.js +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 { STORAGE, migrateStorage } from './storage.js' +import { friendLineNews } from './friend-lines.js' import { groupsFor, readMode, surfaceGroups, writeMode } from './surface.js' @@ -1431,7 +1432,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 = {} } @@ -1445,7 +1446,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({}) @@ -1454,7 +1455,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 } }) @@ -1472,7 +1473,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() @@ -1488,7 +1489,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 @@ -1503,7 +1504,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 = () => { @@ -1522,7 +1523,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') }) @@ -1531,7 +1532,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) => { @@ -1539,7 +1540,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. @@ -1555,7 +1556,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'). @@ -1580,7 +1581,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') { @@ -1589,7 +1590,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() } } })) }, @@ -1602,7 +1603,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 })), @@ -1617,7 +1618,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 })), @@ -1652,7 +1653,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() } })) }, @@ -1661,7 +1662,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. @@ -1669,7 +1670,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') @@ -1683,7 +1684,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({}) @@ -1703,7 +1704,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)) @@ -1727,7 +1728,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 } @@ -1740,7 +1741,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 @@ -1755,7 +1756,7 @@ diff --git a/src/client/index.js b/src/client/index.js actions.referenceChat = (sessionId, span) => { const controller = atMenu?.controllerOf(sessionId) if (controller === undefined || span === undefined) return -@@ -752,33 +752,46 @@ export function apply(ctx) { +@@ -752,33 +752,48 @@ installCompiledStyles(SHELL_CSS, 'shell'), installCompiledStyles(SIDEBAR_CSS, 'sidebar'), installCompiledStyles(SECRET_CSS, 'secrets'), @@ -1772,6 +1773,7 @@ diff --git a/src/client/index.js b/src/client/index.js installCompiledStyles(USER_MESSAGE_CSS, 'user-messages'), + installCompiledStyles(MOBILE_CSS, 'mobile'), + installCompiledStyles(PANE_MIN_WIDTH_CSS, 'panes'), ++ installCompiledStyles(PROJECT_BOARD_CSS, 'project-board'), + typeof document !== 'undefined' ? watchShellLevel(document) : null, browsers ? installCompiledStyles(BROWSER_CSS, 'browser') : null, installTheme(), @@ -1791,6 +1793,7 @@ diff --git a/src/client/index.js b/src/client/index.js shadow('shell.overlay', { id: 'bot-signin', order: 55, inject: face }, DeepSeekSignIn), shadow('shell.overlay', { id: 'bot-toast', order: 60, inject: face }, ErrorToast), + shadow('shell.overlay', { id: 'flybot-approvals', order: 58, inject: face }, FlyBotApprovals), ++ shadow('shell.overlay', { id: 'project-board', order: 57 }, ProjectBoardOverlay), ] const installConversation = () => [ installCompiledStyles(CONVERSATION_CSS, 'conversation'), @@ -1805,7 +1808,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), @@ -1833,7 +1836,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), diff --git a/patches/ds-bot/0005-social-seams.patch b/patches/ds-bot/0005-social-seams.patch index 12a1e852a..0402def53 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,151 @@ +@@ -21,32 +21,151 @@ // The DSH interface language of the last browser that asked for the team. let uiLocale @@ -449,7 +449,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 +183,8 @@ +@@ -54,7 +54,8 @@ capabilities: rt.capabilityLibraryView(), secretRequests: state.secretRequests ?? {}, colors: [...COLORS, ...EXTRA_COLORS], @@ -459,7 +459,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 +195,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, @@ -521,7 +521,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js function setPrefs(body) { if (body.theme !== undefined) { -@@ -97,28 +255,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') } @@ -590,7 +590,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 +335,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() @@ -599,7 +599,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js return ok(bot) } case 'update-bot': -@@ -150,18 +344,23 @@ +@@ -150,18 +150,23 @@ case 'delete-bot': await rt.load() await rt.deleteBot(body.id) @@ -627,7 +627,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js return ok(room) } case 'update-room': { -@@ -171,12 +370,70 @@ +@@ -171,12 +171,70 @@ notice: body.notice, mode: body.mode, admin: body.admin, @@ -700,7 +700,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js } case 'folders': case 'folder-pick': -@@ -203,17 +460,19 @@ +@@ -203,17 +203,19 @@ case 'remove-agent': await rt.load() await rt.removeAgent(body.agentId) @@ -723,7 +723,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 +490,14 @@ +@@ -231,8 +231,14 @@ await rt.load() return ok(await rt.secretEndpoint(endpoint, body)) case 'connector-connect': @@ -738,7 +738,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 +506,24 @@ +@@ -241,11 +241,24 @@ case 'bot-capability': await rt.load() return ok(await rt.capabilityEndpoint(endpoint, body)) @@ -764,7 +764,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js case 'memory-view': case 'memory-ask': case 'memory-forget': -@@ -258,8 +536,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') @@ -782,7 +782,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,9 +565,49 @@ +@@ -278,9 +278,49 @@ const text = String(body.text ?? '').trim() if (text === '' || typeof body.sessionId !== 'string') return fail('Nothing to send', 'bot/invalid') await rt.load() @@ -832,7 +832,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js await rt.ensureCapabilities(sessionId) await ctx.sessionController.prompt({ requestId: randomUUID(), -@@ -319,32 +646,129 @@ +@@ -319,32 +319,129 @@ case 'diagnostics-clear': await rt.diagnostics.clear() return ok(true) @@ -863,7 +863,7 @@ diff --git a/src/host/channel.js b/src/host/channel.js + case 'project-of': + case 'project-board': + await rt.load() -+ return ok(await rt.projectEndpoint(endpoint, body)) ++ return ok(await rt.projectEndpoint(endpoint, body, actor)) + case 'share-brief': + await rt.load() + await rt.setBriefShare(String(body.id ?? ''), String(body.botId ?? ''), body.share === true, actor) @@ -967,7 +967,6 @@ diff --git a/src/host/channel.js b/src/host/channel.js + dispose?.() + } +} - diff --git a/src/host/delivery.js b/src/host/delivery.js --- a/src/host/delivery.js +++ b/src/host/delivery.js diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index ded73eb24..b526a34e1 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -3,7 +3,7 @@ FlyBot patch 0008-client-bundle on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Gener diff --git a/client.js b/client.js --- a/client.js +++ b/client.js -@@ -42,6 +42,56 @@ module.exports = __toCommonJS(index_exports); +@@ -42,6 +42,56 @@ // src/client/i18n.js var NAMESPACE = "ds-bot"; var ZH = { @@ -60,7 +60,7 @@ diff --git a/client.js b/client.js // Shared "Main Bot": "主 Bot", Working: "工作中", -@@ -49,6 +49,67 @@ var ZH = { +@@ -49,6 +49,67 @@ "Unread activity": "有未读", Search: "搜索", "New chat": "新对话", @@ -128,7 +128,7 @@ diff --git a/client.js b/client.js "New…": "新建", "Animation": "动画", "Bot animation": "Bot 动画", -@@ -63,6 +63,21 @@ var ZH = { +@@ -63,6 +63,21 @@ Duplicate: "复制一份", Hidden: "已隐藏", Admin: "管理员", @@ -150,7 +150,7 @@ diff --git a/client.js b/client.js Add: "添加", Close: "关闭", Copy: "复制", -@@ -78,21 +78,72 @@ var ZH = { +@@ -78,21 +78,72 @@ " and ": "、", and: "和", "{count} members": "{count} 位成员", @@ -226,7 +226,7 @@ diff --git a/client.js b/client.js Connectors: "连接器", "Search connectors": "搜索连接器", "No connectors": "暂无连接器", -@@ -129,6 +129,59 @@ var ZH = { +@@ -129,6 +129,59 @@ "Connecting…": "正在连接…", "Could not connect: {error}": "连接失败:{error}", "Connected as {account}": "已连接:{account}", @@ -286,7 +286,7 @@ diff --git a/client.js b/client.js Connected: "已连接", Disconnect: "断开", Connect: "连接", -@@ -141,17 +141,64 @@ var ZH = { +@@ -141,17 +141,64 @@ "GitHub addresses must start with https://": "GitHub 地址必须以 https:// 开头", "This DSH cannot load connectors": "这个 DSH 不能加载连接器", "No such connector": "没有这个连接器", @@ -355,7 +355,7 @@ diff --git a/client.js b/client.js // Header "Message {name}": "给 {name} 发消息", Message: "发消息", -@@ -162,8 +162,6 @@ var ZH = { +@@ -162,8 +162,6 @@ "Dismiss new messages": "关闭新消息提示", New: "新", // New chat @@ -364,7 +364,7 @@ diff --git a/client.js b/client.js "Add to group chat": "加入群聊", "Message Bot": "给 Bot 发消息", "To:": "发给:", -@@ -187,9 +187,24 @@ var ZH = { +@@ -187,9 +187,24 @@ Everyone: "所有人", "Every member answers in turn, admin first.": "每位成员轮流回答,管理员先说。", "Members · {count}": "成员 · {count}", @@ -389,7 +389,7 @@ diff --git a/client.js b/client.js "Remove {name} from the group": "把 {name} 移出群聊", "Remove from group": "移出群聊", "Add member": "添加成员", -@@ -205,6 +205,16 @@ var ZH = { +@@ -205,6 +205,16 @@ "Close details": "关闭详情", "Copied conversation": "已复制对话", "Share conversation": "分享对话", @@ -406,7 +406,7 @@ diff --git a/client.js b/client.js "Copy conversation as Markdown": "把对话复制为 Markdown", "This conversation is not part of your Bot team.": "这个对话不属于你的 Bot 团队。", "Admin {name}": "管理员 {name}", -@@ -247,6 +247,25 @@ var ZH = { +@@ -247,6 +247,25 @@ "This model is not available now. Pick another one.": "这个模型现在不可用,请换一个。", "Every message of this Bot runs on it.": "这个 Bot 的每条消息都用它。", "Applies from the next message.": "从下一条消息开始生效。", @@ -432,7 +432,7 @@ diff --git a/client.js b/client.js Profile: "资料", Label: "标签", "Optional. Shown under the name.": "可选,显示在名字下方。", -@@ -270,6 +270,7 @@ var ZH = { +@@ -270,6 +270,7 @@ "Kept above the other chats.": "排在其他对话上面。", "Search still finds it.": "仍然可以搜索到。", Manage: "管理", @@ -440,7 +440,7 @@ diff --git a/client.js b/client.js // Group chat settings "Leave it empty to name the group after its members.": "留空就按成员命名。", "Every member reads it before speaking in this group.": "每位成员在这个群里发言前都会读到。", -@@ -303,6 +303,8 @@ var ZH = { +@@ -303,6 +303,8 @@ "Harness default": "Harness 默认", Saving: "保存中", "Manage models": "管理模型", @@ -449,7 +449,7 @@ diff --git a/client.js b/client.js // Shapes Whale: "鲸鱼", Star: "星星", -@@ -374,9 +374,10 @@ var ZH = { +@@ -374,9 +374,10 @@ "No group chats yet.": "还没有群聊。", "A group chat lets several Bots work on one thing with you.": "在群聊里,几个 Bot 和你一起做一件事。", "Back to group chats": "返回群聊列表", @@ -462,7 +462,7 @@ diff --git a/client.js b/client.js "Harness settings": "Harness 设置", "Back to Bots": "返回 Bot 列表", "Close settings": "关闭设置", -@@ -385,7 +385,6 @@ var ZH = { +@@ -385,7 +385,6 @@ Pin: "置顶", Pinned: "已置顶", "Mark as Unread": "标为未读", @@ -470,7 +470,7 @@ diff --git a/client.js b/client.js "Group settings": "群设置", "Rename group": "重命名群聊", "Copy conversation ID": "复制对话 ID", -@@ -394,6 +394,8 @@ var ZH = { +@@ -394,6 +394,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", @@ -479,7 +479,7 @@ diff --git a/client.js b/client.js "Remove DSH Agent": "移除 DSH Agent", "New session": "新会话", Archive: "归档", -@@ -404,13 +404,13 @@ var ZH = { +@@ -404,13 +404,13 @@ "{n} h": "{n} 小时", "{n} d": "{n} 天", // Plugin updates @@ -499,7 +499,7 @@ diff --git a/client.js b/client.js "You have {version}.": "当前版本 {version}。", "The update did not finish: {reason}": "更新没有完成:{reason}", "What's new": "更新说明", -@@ -420,8 +420,74 @@ var ZH = { +@@ -420,8 +420,74 @@ "DSH update": "DSH 更新", "DSH {version}": "DSH {version}", "Restart DSH to start using DSH {version}.": "重启 DSH 后开始使用 DSH {version}。", @@ -575,7 +575,7 @@ diff --git a/client.js b/client.js // Command palette Messages: "消息", Commands: "命令", -@@ -446,10 +446,13 @@ var ZH = { +@@ -446,10 +446,13 @@ Reply: "回复", "More message actions": "更多操作", More: "更多", @@ -589,7 +589,7 @@ diff --git a/client.js b/client.js "Earlier messages": "更早的消息", "Loading earlier messages…": "正在加载更早的消息…", "Couldn’t load earlier messages · Retry": "更早的消息没加载出来 · 重试", -@@ -459,6 +459,9 @@ var ZH = { +@@ -459,6 +459,9 @@ "Replies from": "群聊回复:", "Reply from": "收到回复:", "Message from": "收到消息:", @@ -599,7 +599,7 @@ diff --git a/client.js b/client.js "Stopped by": "叫停者:", "{count} messages with": "{count} 条消息,来自", "Routine started": "例行任务开始", -@@ -468,6 +468,8 @@ var ZH = { +@@ -468,6 +468,8 @@ "Working out a plan": "理一下思路", "Sorting out the details": "梳理细节", "On it": "在做了", @@ -608,7 +608,7 @@ diff --git a/client.js b/client.js "Busy with it": "正在处理", "Moving ahead": "推进中", "Taking care of it": "处理中", -@@ -495,7 +495,6 @@ var ZH = { +@@ -495,7 +495,6 @@ "Looking back through the chat": "翻看聊天记录", "{name} looked up its own chat": "{name} 查看了自己的对话", "{name} looked up its own chat for “{text}”": "{name} 在自己的对话里查找「{text}」", @@ -616,7 +616,7 @@ diff --git a/client.js b/client.js "{name} didn't answer in time": "{name} 没有及时回应", "{name} paused for your approval": "{name} 停下来等你批准", "{name} started on it": "{name} 开始做了", -@@ -506,12 +506,31 @@ var ZH = { +@@ -506,12 +506,31 @@ "Asked to make the tool call it wrote as text": "工具调用被写成了文字,已让它重新调用", "A Bot": "一个 Bot", "{name} is waiting for your approval": "{name} 等你批准", @@ -649,7 +649,7 @@ diff --git a/client.js b/client.js "Couldn't message {name}": "没能给 {name} 发消息", Messaged: "已发消息给", "Couldn't stop {name}": "没能叫停 {name}", -@@ -533,13 +533,14 @@ var ZH = { +@@ -533,13 +533,14 @@ Selected: "已选", Skip: "跳过", Submit: "提交", @@ -665,7 +665,7 @@ diff --git a/client.js b/client.js "No Bot": "没有 Bot", "Paste or type the key": "粘贴或输入密钥", "Only {name}": "只给 {name}", -@@ -555,7 +555,6 @@ var ZH = { +@@ -555,7 +555,6 @@ "At least {count} characters.": "至少 {count} 个字符。", "Replace value": "替换值", "Saving…": "保存中…", @@ -673,7 +673,7 @@ diff --git a/client.js b/client.js "Allowing…": "允许中…", Allow: "允许", "{bot} asked for {name}": "{bot} 请求了 {name}", -@@ -582,7 +582,7 @@ var ZH = { +@@ -582,7 +582,7 @@ "NAME, e.g. GITHUB_TOKEN": "名字,例如 GITHUB_TOKEN", "What it is for": "用途", Value: "值", @@ -682,7 +682,7 @@ diff --git a/client.js b/client.js "Save key": "保存密钥", "Keys your Bots asked for. Bots never see the values: they use them as ": "你的 Bot 请求过的密钥。Bot 永远看不到值:它们在 shell 里通过 ", " in their shell, and every value is replaced by ": " 使用,而且发给模型之前,每个值都会被替换成 ", -@@ -621,7 +621,6 @@ var ZH = { +@@ -621,7 +621,6 @@ Remove: "删除", "Ask or update": "询问或更新", "Ask about or update {name}'s memory": "询问或更新 {name} 的记忆", @@ -690,7 +690,7 @@ diff --git a/client.js b/client.js "Added: {text}": "已添加:{text}", "Added to team memory: {text}": "已加入团队记忆:{text}", "Updated: {text}": "已更新:{text}", -@@ -747,11 +747,11 @@ var ZH = { +@@ -747,11 +747,11 @@ "Compared with the {count} days before": "与再往前 {count} 天相比", "{share}% of all usage from {span}": "占 {span} 全部用量的 {share}%", // About @@ -705,7 +705,7 @@ diff --git a/client.js b/client.js Updates: "更新", "Check for updates": "检查更新", "Checking…": "正在检查…", -@@ -761,6 +761,13 @@ var ZH = { +@@ -761,6 +761,13 @@ "Update checks are off.": "已关闭更新检查。", "Installed from a local folder; update it there.": "从本地文件夹安装,请在那里更新。", "Updates are not offered for this install.": "这种安装方式不提供更新。", @@ -719,7 +719,7 @@ diff --git a/client.js b/client.js Links: "链接", "Source code": "源代码", "Release notes": "发布说明", -@@ -776,7 +776,7 @@ var ZH = { +@@ -776,7 +776,7 @@ Show: "查看", Hide: "收起", "GitHub issue": "GitHub issue", @@ -728,7 +728,7 @@ diff --git a/client.js b/client.js "Report on GitHub": "在 GitHub 上报告", "Copied the diagnostics.": "已复制诊断信息。", "Could not copy. Select the text below and copy it.": "无法复制。请选中下面的文字手动复制。", -@@ -788,7 +788,7 @@ var ZH = { +@@ -788,7 +788,7 @@ "Recent errors": "最近的错误", "Recent errors · {count}": "最近的错误 · {count}", "No errors recorded.": "没有错误记录。", @@ -737,7 +737,7 @@ diff --git a/client.js b/client.js "Model call": "模型调用", "This window": "这个窗口", "Error log": "错误日志", -@@ -821,12 +821,21 @@ var ZH = { +@@ -821,12 +821,21 @@ "Add at least one more Bot": "再至少添加一个 Bot", Members: "成员", // Connectors and scheduled tasks @@ -759,7 +759,7 @@ diff --git a/client.js b/client.js "Once, {time}": "一次,{time}", "Every day at {time}": "每天 {time}", "Every {days} at {time}": "每{days} {time}", -@@ -834,7 +834,21 @@ var ZH = { +@@ -834,7 +834,21 @@ "Every weekend at {time}": "每个周末 {time}", "On the schedule {expression}": "按计划 {expression}", "Next: {time}": "下次 {time}", @@ -781,7 +781,7 @@ diff --git a/client.js b/client.js "in {name}": "在 {name}", "Click again to delete": "再点一次删除", "Delete task": "删除任务", -@@ -844,6 +844,7 @@ var ZH = { +@@ -844,6 +844,7 @@ minutes: "分钟", hours: "小时", days: "天", @@ -789,7 +789,7 @@ diff --git a/client.js b/client.js "Morning briefing": "早间简报", "What to do": "要做什么", "Sum up what changed since yesterday and what needs me today.": "总结一下昨天以来的变化,以及今天需要我处理的事。", -@@ -856,9 +856,20 @@ var ZH = { +@@ -856,9 +856,20 @@ Unit: "单位", "Create task": "创建任务", "Turn on Automation tasks in DSH": "在 DSH 里打开「自动化任务」", @@ -812,7 +812,7 @@ diff --git a/client.js b/client.js "No scheduled tasks yet.": "还没有定时任务。", "No scheduled tasks yet. Add one": "还没有定时任务,去添加", "Automation tasks are off in DSH": "DSH 的「自动化任务」没有打开", -@@ -869,10 +869,19 @@ var ZH = { +@@ -869,10 +869,19 @@ "Pick a date and time": "选一个日期和时间", "Pick a time in the future": "选一个将来的时间", "Pick a time of day": "选一个时间", @@ -832,7 +832,19 @@ diff --git a/client.js b/client.js "Working folder": "工作文件夹", "Working folder: {folder}": "工作文件夹:{folder}", Folder: "文件夹", -@@ -900,8 +900,11 @@ var HOST_ERRORS = [ +@@ -887,7 +887,10 @@ + "Use this folder": "使用这个文件夹", + "Up one level": "上一级", + "No folders inside.": "里面没有文件夹。", +- "Some folders are not shown.": "还有一些文件夹没有显示。" ++ "Some folders are not shown.": "还有一些文件夹没有显示。", ++ "Project settings": "项目设置", ++ "Project board": "项目看板", ++ "No Project here.": "这里没有项目。" + }; + var HOST_ERRORS = [ + [/^Model (.+) is not available\. Available: (.*)$/, "模型 $1 不可用。可用的:$2"], +@@ -900,8 +900,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 个字符"], @@ -844,7 +856,7 @@ diff --git a/client.js b/client.js [/^HTTP (\d+)$/, "请求失败(HTTP $1)"], [/^GitHub answered (\d+)$/, "GitHub 返回了 $1"], [/^Keep the name under (\d+) characters$/, "名字不能超过 $1 个字"], -@@ -930,6 +930,20 @@ var HOST_ZH = { +@@ -930,6 +930,20 @@ "Unknown Bot": "找不到这个 Bot", "Unknown conversation": "找不到这个对话", "A group chat needs at least two Bots": "群聊至少要两个 Bot", @@ -865,7 +877,7 @@ diff --git a/client.js b/client.js "The admin must be a member of the group": "管理员必须是群成员", "Unknown group chat": "找不到这个群聊", "Hand the admin role to another member instead of leaving the group without one": "请把管理员交给其他成员,不要让群聊没有管理员", -@@ -951,7 +951,9 @@ var HOST_ZH = { +@@ -951,10 +951,12 @@ "The model wrote nothing": "模型没有输出内容", "Write the entry to save.": "请写下要保存的内容。", "This DeepSeek Harness has no folder chooser": "这个 DeepSeek Harness 没有文件夹选择窗口", @@ -875,23 +887,39 @@ diff --git a/client.js b/client.js + "saved by a newer FlyBot; update FlyBot": "由更新的 FlyBot 保存;请更新 FlyBot" }; Object.assign(ZH, HOST_ZH); - var fill = (text, params) => params ? text.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match) : text; -@@ -1016,6 +1016,14 @@ function noteClientError(where, error) { - entries.push({ time: now, level: "error", text, ...code ? { code } : {} }); - if (entries.length > MAX) entries.splice(0, entries.length - MAX); - } +-var fill = (text, params) => params ? text.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match) : text; ++var fill = (text2, params) => params ? text2.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match) : text2; + var service = null; + var bound = null; + function installLocale(locale) { +@@ -1005,15 +1005,23 @@ + } + const message = String(error?.message ?? error ?? "error").slice(0, 300); + const code = typeof error?.code === "string" ? error.code : void 0; +- const text = `${where}: ${message}`; ++ const text2 = `${where}: ${message}`; + const now = Date.now(); + const last = entries.at(-1); +- if (last && last.text === text && now - last.time < REPEAT_MS) { ++ if (last && last.text === text2 && now - last.time < REPEAT_MS) { + last.time = now; + last.count = (last.count ?? 1) + 1; + return; + } +- 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); ++} +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 text = `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] ${text}`); -+ entries.push({ time: Date.now(), level: "warn", text }); -+ if (entries.length > MAX) entries.splice(0, entries.length - MAX); -+} ++ 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); + } var quietly = (where) => (error) => noteClientError(where, error); - var clearClientErrors = () => { - entries.length = 0; -@@ -1042,12 +1042,17 @@ var FAILURES = { +@@ -1042,16 +1042,21 @@ }; function failureLabel(code, message) { if (FAILURES[code]) return t(FAILURES[code]); @@ -910,9 +938,16 @@ diff --git a/client.js b/client.js +var PASTE_NOTE = "FlyBot copied the diagnostics to the clipboard. Paste them here."; +var REST_NOTE = "The recent errors do not fit in the link. FlyBot copied the diagnostics to the clipboard: paste them below this line."; function issueUrl(base, { what = "", diagnostics }) { - const text = what.trim().slice(0, WHAT_MAX); - const title = text.split("\n")[0].slice(0, 80); -@@ -1066,10 +1066,16 @@ var REFUSALS = /* @__PURE__ */ new Set(["bot/refused", "bot/invalid", "bot/read- +- const text = what.trim().slice(0, WHAT_MAX); +- const title = text.split("\n")[0].slice(0, 80); +- const build = (body) => `${base}?${new URLSearchParams({ template: "bug.yml", ...title ? { title } : {}, ...text ? { what: text } : {}, diagnostics: body })}`; ++ const text2 = what.trim().slice(0, WHAT_MAX); ++ const title = text2.split("\n")[0].slice(0, 80); ++ const build = (body) => `${base}?${new URLSearchParams({ template: "bug.yml", ...title ? { title } : {}, ...text2 ? { what: text2 } : {}, diagnostics: body })}`; + const full2 = build(diagnostics); + if (full2.length <= URL_LIMIT) return { url: full2, inline: true }; + const facts = build(`${diagnostics.split("\n\n")[0]} +@@ -1066,10 +1066,16 @@ var call = async (endpoint, payload, signal) => { let response; try { @@ -930,7 +965,7 @@ diff --git a/client.js b/client.js body: JSON.stringify({ endpoint, payload: payload ?? null }), signal }); -@@ -1087,7 +1087,7 @@ var call = async (endpoint, payload, signal) => { +@@ -1087,7 +1087,7 @@ if (!result.ok) { const code = result.error?.code ?? "bot/error"; const message = hostText(result.error?.message ?? "Request failed"); @@ -939,14 +974,14 @@ diff --git a/client.js b/client.js if (!REFUSALS.has(code)) { noteClientError(endpoint, Object.assign(new Error(result.error?.message ?? "Request failed"), { code })); markNoted(error); -@@ -1097,6 +1097,388 @@ var call = async (endpoint, payload, signal) => { +@@ -1097,6 +1097,388 @@ return result.value; }; +// src/shared/remote-label.js +function cleanPeerName(name, fallback = "") { -+ const text = String(name ?? "").replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ").replace(/[[\]"`<>]/g, "").replace(/\s+/g, " ").trim().slice(0, 60); -+ return text || fallback; ++ const text2 = String(name ?? "").replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ").replace(/[[\]"`<>]/g, "").replace(/\s+/g, " ").trim().slice(0, 60); ++ return text2 || fallback; +} +function normRemoteHandle(handle) { + return `@${String(handle ?? "").trim().replace(/^@+/, "").toLowerCase()}`; @@ -1051,10 +1086,10 @@ diff --git a/client.js b/client.js + const handle = String(record?.handle ?? "").replace(/^@+/, "").trim(); + const bare2 = remoteBotName(record, "").replace(/^@+/, "").trim(); + const unusable = (value) => { -+ const text = String(value ?? "").replace(/^@+/, "").trim(); -+ if (!text || /^friend$/i.test(text)) return true; -+ if (text.includes("@") || /\.home\b/i.test(text)) return true; -+ if (handle && text.toLowerCase() === handle.toLowerCase()) return true; ++ const text2 = String(value ?? "").replace(/^@+/, "").trim(); ++ if (!text2 || /^friend$/i.test(text2)) return true; ++ if (text2.includes("@") || /\.home\b/i.test(text2)) return true; ++ if (handle && text2.toLowerCase() === handle.toLowerCase()) return true; + return false; + }; + if (!unusable(bare2)) return bare2; @@ -1115,11 +1150,11 @@ diff --git a/client.js b/client.js + const friends = [...new Set((Array.isArray(details.remotes) ? details.remotes : []).map((remote) => cleanPeerName(remote?.owner || remote?.name, "")).filter(Boolean))]; + return { bots, friends }; +} -+async function copiedToClipboard(text, nav = globalThis.navigator) { ++async function copiedToClipboard(text2, nav = globalThis.navigator) { + const write = nav?.clipboard?.writeText; + if (typeof write !== "function") return false; + try { -+ await write.call(nav.clipboard, text); ++ await write.call(nav.clipboard, text2); + return true; + } catch { + return false; @@ -1267,8 +1302,8 @@ diff --git a/client.js b/client.js + } + return [...homes.values()]; +} -+function remoteMessageText(text) { -+ const value = String(text ?? ""); ++function remoteMessageText(text2) { ++ const value = String(text2 ?? ""); + const match = value.match(/\n?([\s\S]*?)\n?<\/untrusted-message>/); + return match ? match[1] : value; +} @@ -1328,7 +1363,19 @@ diff --git a/client.js b/client.js // src/client/inks.js var INKS = { deepseek: ["#4D6BFE", "#6A84FF"], -@@ -1177,19 +1177,96 @@ function defaultShape(id) { +@@ -1160,9 +1160,9 @@ + var CHARACTER_COLORS = ["black", "brown", "red", "orange", "yellow", "green", "cyan", "blue", "violet", "magenta", "gray", "aurora"]; + var DEFAULT_COLORS = CHARACTER_COLORS.filter((color) => !["black", "gray", "brown"].includes(color) && !GRADIENTS[color]); + var isColor = (color) => isHex(color) || Object.hasOwn(INKS, color); +-function fnv1a(text) { ++function fnv1a(text2) { + let value = 2166136261; +- for (let index = 0; index < text.length; index += 1) value = Math.imul(value ^ text.charCodeAt(index), 16777619); ++ for (let index = 0; index < text2.length; index += 1) value = Math.imul(value ^ text2.charCodeAt(index), 16777619); + return value >>> 0; + } + function defaultColor(id) { +@@ -1177,19 +1177,96 @@ value = Math.imul(value ^ value >>> 13, 3266489909); return DEFAULT_SHAPES[((value ^ value >>> 16) >>> 0) % DEFAULT_SHAPES.length]; } @@ -1428,18 +1475,18 @@ diff --git a/client.js b/client.js ``, `` ].join(""); -@@ -1206,8 +1206,8 @@ function bodyOf(def, paint, flat = false) { +@@ -1206,8 +1206,8 @@ return ""; } } -var eyeMarkup = ([cx, cy, w, h34, tilt]) => ``; -var happyEyeMarkup = ([cx, cy, w, h34, tilt]) => ``; -+var eyeMarkup = ([cx, cy, w, h48, tilt]) => ``; -+var happyEyeMarkup = ([cx, cy, w, h48, tilt]) => ``; ++var eyeMarkup = ([cx, cy, w, h49, tilt]) => ``; ++var happyEyeMarkup = ([cx, cy, w, h49, tilt]) => ``; var maskBox = (size) => `maskUnits="userSpaceOnUse" x="${-size}" y="${-size}" width="${size * 3}" height="${size * 3}"`; var holes = (def, eyes) => `${eyes}`; var MASK_ID = "%MASK%"; -@@ -1242,162 +1242,16 @@ function maskOf(shape) { +@@ -1242,192 +1242,46 @@ } var DEFAULT_MASK = maskOf(FALLBACK_SHAPE); @@ -1583,7 +1630,16 @@ diff --git a/client.js b/client.js - else delete SHAPES[id]; - refresh(); - }; --} ++// src/client/cluster-order.js ++function clusterOrder(people2) { ++ const list = Array.isArray(people2) ? people2 : []; ++ const remote = (bot) => Boolean(bot?.remote); ++ if (!list.some(remote)) return list; ++ const locals = list.filter((bot) => !remote(bot)); ++ const remotes = list.filter(remote); ++ if (list.length <= 4) return [...remotes, ...locals]; ++ return [...locals, ...remotes]; + } -function registerTheme(id, theme) { - if (typeof id !== "string" || !/^[a-z][\w-]{0,31}$/.test(id)) throw new Error("Theme ids are lowercase words"); - for (const mode of ["light", "dark"]) { @@ -1597,27 +1653,57 @@ diff --git a/client.js b/client.js - else delete customThemes[id]; - registry.set((value) => value + 1); - }; -+// src/client/cluster-order.js -+function clusterOrder(people2) { -+ const list = Array.isArray(people2) ? people2 : []; -+ const remote = (bot) => Boolean(bot?.remote); -+ if (!list.some(remote)) return list; -+ const locals = list.filter((bot) => !remote(bot)); -+ const remotes = list.filter(remote); -+ if (list.length <= 4) return [...remotes, ...locals]; -+ return [...locals, ...remotes]; - } +-} -var allThemes = () => ({ ...THEMES, ...customThemes }); // src/shared/mentions.js var ALL_WORDS = ["all", "everyone", "所有人", "全体"]; -@@ -1445,14 +1445,62 @@ function mentionSpans(text, bots, { references = [] } = {}) { + var NAME_GOES_ON = /^(?:[a-z0-9_\-/\\]|\.[a-z0-9_-])/i; + var ADDRESS_BEFORE = /[a-z0-9_./]/i; + var LITERAL = /```[\s\S]*?(?:```|$)|`[^`\n]*`|\[[^\]\n]*\]\([^)\s]*(?:\s+(?:"[^"\n]*"|'[^'\n]*'))?\)/g; +-var fitsAt = (text, at, word) => text.slice(at, at + word.length).toLowerCase() === word.toLowerCase() && !NAME_GOES_ON.test(text.slice(at + word.length)); ++var fitsAt = (text2, at, word) => text2.slice(at, at + word.length).toLowerCase() === word.toLowerCase() && !NAME_GOES_ON.test(text2.slice(at + word.length)); + var overlaps = (spans, start, end) => spans.some((span) => start < span.end && span.start < end); + function nameProblem(name) { +- const text = String(name ?? "").trim(); +- if (text === "") return "A Bot needs a name"; +- if (/[@`\r\n]/.test(text)) return "A Bot name cannot contain @, ` or a line break"; +- if (ALL_WORDS.includes(text.toLowerCase())) return `"${text}" calls everyone in a group, so it cannot be a Bot name`; ++ const text2 = String(name ?? "").trim(); ++ if (text2 === "") return "A Bot needs a name"; ++ if (/[@`\r\n]/.test(text2)) return "A Bot name cannot contain @, ` or a line break"; ++ if (ALL_WORDS.includes(text2.toLowerCase())) return `"${text2}" calls everyone in a group, so it cannot be a Bot name`; + return void 0; + } +-function referenceSpans(text, labels = []) { ++function referenceSpans(text2, labels = []) { + const spans = []; + for (const label of [...labels].sort((a, b) => b.length - a.length)) { + const mention = `@${label}`; +- for (let at = text.indexOf(mention); at !== -1; at = text.indexOf(mention, at + 1)) { ++ for (let at = text2.indexOf(mention); at !== -1; at = text2.indexOf(mention, at + 1)) { + const end = at + mention.length; +- if (at > 0 && ADDRESS_BEFORE.test(text[at - 1]) || NAME_GOES_ON.test(text.slice(end)) || overlaps(spans, at, end)) continue; ++ if (at > 0 && ADDRESS_BEFORE.test(text2[at - 1]) || NAME_GOES_ON.test(text2.slice(end)) || overlaps(spans, at, end)) continue; + spans.push({ start: at, end, label }); + break; + } + } + return spans.sort((a, b) => a.start - b.start); + } +-function mentionSpans(text, bots, { references = [] } = {}) { +- const value = String(text ?? ""); ++function mentionSpans(text2, bots, { references = [] } = {}) { ++ const value = String(text2 ?? ""); + const skip = [ + ...[...value.matchAll(LITERAL)].map((match) => ({ start: match.index, end: match.index + match[0].length })), + ...referenceSpans(value, references) +@@ -1445,24 +1445,86 @@ } return spans; } -- -+function unmatchedMentions(text, bots, { references = [] } = {}) { -+ const value = String(text ?? ""); ++function unmatchedMentions(text2, bots, { references = [] } = {}) { ++ const value = String(text2 ?? ""); + const skip = [ + ...[...value.matchAll(LITERAL)].map((match) => ({ start: match.index, end: match.index + match[0].length })), + ...referenceSpans(value, references) @@ -1643,16 +1729,16 @@ diff --git a/client.js b/client.js +var FENCE_ATTR = /\bfrom\s*=\s*(?:"[^"]*"|'[^']*')\s*>/gi; +var FENCE_TAIL = /<[^<>]*\buntrusted-(?:message|file)\b[^<>]*$/i; +var FENCE_BARE = /(?:\/\s*)?untrusted-(?:message|file)\b\s*>?/gi; -+function stripFence(text) { -+ let out = String(text ?? ""); ++function stripFence(text2) { ++ let out = String(text2 ?? ""); + for (let previous; previous !== out; ) { + previous = out; + out = out.replace(FENCE_TAG, "").replace(FENCE_ATTR, ""); + } + return out.replace(FENCE_TAIL, "").replace(FENCE_BARE, ""); +} -+function visibleText(text) { -+ let out = String(text ?? ""); ++function visibleText(text2) { ++ let out = String(text2 ?? ""); + for (let previous; previous !== out; ) { + previous = out; + const open = out.indexOf(" Number(Boolean(a?.remote)) - Number(Boolean(b?.remote))); let out = ""; let cursor = 0; - for (const span of mentionSpans(text, bots)) { -+ for (const span of mentionSpans(text, ordered)) { ++ for (const span of mentionSpans(text2, ordered)) { if (!span.bot || span.bot.id === selfId) continue; - out += `${text.slice(cursor, span.start)}[${text.slice(span.start + 1, span.end).replace(/[[\]\\]/g, "\\$&")}](${MENTION_ORIGIN}${span.bot.id})`; +- out += `${text.slice(cursor, span.start)}[${text.slice(span.start + 1, span.end).replace(/[[\]\\]/g, "\\$&")}](${MENTION_ORIGIN}${span.bot.id})`; ++ out += `${text2.slice(cursor, span.start)}[${text2.slice(span.start + 1, span.end).replace(/[[\]\\]/g, "\\$&")}](${MENTION_ORIGIN}${span.bot.id})`; cursor = span.end; -@@ -1463,6 +1463,20 @@ function mentionTarget(event) { + } +- return cursor === 0 ? text : out + text.slice(cursor); ++ return cursor === 0 ? text2 : out + text2.slice(cursor); + } + function mentionTarget(event) { const anchor = event.target instanceof Element ? event.target.closest(`a[href^="${MENTION_ORIGIN}"]`) : null; return anchor ? anchor.getAttribute("href").slice(MENTION_ORIGIN.length) : null; } -+function nameMentions(text, roster) { -+ const value = String(text ?? ""); ++function nameMentions(text2, roster) { ++ const value = String(text2 ?? ""); + if (!value.includes("@") || roster === null || typeof roster !== "object") return value; + const records = [...Array.isArray(roster.remoteBots) ? roster.remoteBots : [], ...Array.isArray(roster.bots) ? roster.bots : []]; + const names = /* @__PURE__ */ new Map(); @@ -1697,10 +1790,13 @@ diff --git a/client.js b/client.js function clock(ms) { return new Date(ms).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); } -@@ -1483,6 +1483,15 @@ function relativeShort(ms) { +@@ -1483,9 +1483,18 @@ var contentText = (content) => (content ?? []).filter((block) => block?.type === "text").map((block) => block.text).join("\n"); var HEADER = /^\[(Message from|Reply from|Group chat|Group post from|Bot team setup|Handoff · part|Secret card)[^\]]*\]\n?/; var currentPart = (bot) => bot.sessionId ?? bot.id; +-function reminderPrompts(text) { +- const one = /^reminder_prompt_json: (.*)$/m.exec(text); +- const batch = /^reminders_json: (.*)$/m.exec(text); +function landingPart(roster, last, mainId, { stay = false, view = "", sessions } = {}) { + const bot = last ? roster.byId[last.botId] : void 0; + if (bot && currentPart(bot) !== last.sessionId) return currentPart(bot); @@ -1710,19 +1806,24 @@ diff --git a/client.js b/client.js + const main = roster.byId[mainId]; + return main ? currentPart(main) : mainId; +} - function reminderPrompts(text) { - const one = /^reminder_prompt_json: (.*)$/m.exec(text); - const batch = /^reminders_json: (.*)$/m.exec(text); -@@ -1494,6 +1494,79 @@ function reminderPrompts(text) { ++function reminderPrompts(text2) { ++ const one = /^reminder_prompt_json: (.*)$/m.exec(text2); ++ const batch = /^reminders_json: (.*)$/m.exec(text2); + try { + const prompts = [...one ? [JSON.parse(one[1])] : [], ...batch ? JSON.parse(batch[1]).map((entry) => entry.reminder_prompt) : []]; + return prompts.filter((prompt) => typeof prompt === "string" && prompt.trim() !== ""); +@@ -1493,38 +1493,121 @@ + return []; } } - var stripHeader = (text) => text.replace(HEADER, ""); +-var stripHeader = (text) => text.replace(HEADER, ""); ++var stripHeader = (text2) => text2.replace(HEADER, ""); +var TRACE = /^[ \t]*[^\n→]{1,60}(?:[ \t]*→[ \t]*[^\n→]{1,60})+[ \t]*\n+/; -+function stripTrace(text) { -+ if (typeof text !== "string") return text; -+ const line2 = text.match(TRACE)?.[0]; -+ if (!line2 || /[.!?。!?::,,]/.test(line2.replace(/[^\S\n]*\n+$/, "").replace(/→/g, ""))) return text; -+ return text.slice(line2.length).replace(/^\s+/, ""); ++function stripTrace(text2) { ++ if (typeof text2 !== "string") return text2; ++ const line2 = text2.match(TRACE)?.[0]; ++ if (!line2 || /[.!?。!?::,,]/.test(line2.replace(/[^\S\n]*\n+$/, "").replace(/→/g, ""))) return text2; ++ return text2.slice(line2.length).replace(/^\s+/, ""); +} +var COPY_FENCE = /untrusted-(?:message|file)\b/i; +var escapeName = (name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -1732,8 +1833,8 @@ diff --git a/client.js b/client.js + const hop = `(?:${known.map(escapeName).join("|")})`; + return new RegExp(`^[ \\t]*${hop}(?:[ \\t]*→[ \\t]*${hop})+[ \\t]*$`).test(line2); +}; -+function stripFlatTrace(text, names = []) { -+ const body = String(text ?? ""); ++function stripFlatTrace(text2, names = []) { ++ const body = String(text2 ?? ""); + const known = sortedNames(names); + if (known.length === 0) return body; + const hop = `(?:${known.map(escapeName).join("|")})`; @@ -1741,8 +1842,8 @@ diff --git a/client.js b/client.js + const found = trace.exec(body); + return found ? body.slice(found[0].length) : body; +} -+function copyBodyOf(text, names = []) { -+ let body = String(text ?? ""); ++function copyBodyOf(text2, names = []) { ++ let body = String(text2 ?? ""); + for (let previous; previous !== body; ) { + previous = body; + body = stripHeader(body); @@ -1793,12 +1894,19 @@ diff --git a/client.js b/client.js function latestMemoryChange(news, ids) { const at = (id) => news?.[id]?.seen ? news[id].at ?? 0 : 0; return ids.reduce((best, id) => at(id) > at(best) ? id : best, ids[0] ?? null); -@@ -1506,25 +1506,35 @@ function summaryBlocks(text) { + } +-function summaryBlocks(text) { ++function summaryBlocks(text2) { + const blocks = []; + let paragraph = []; + const flush = () => { + if (paragraph.length > 0) blocks.push({ kind: "p", text: paragraph.join(" ") }); paragraph = []; }; - for (const raw of String(text ?? "").split("\n")) { +- for (const raw of String(text ?? "").split("\n")) { - const line = raw.trim().replace(/\*\*|__/g, ""); - const heading = /^#{1,6}\s+(.+)$/.exec(line); ++ for (const raw of String(text2 ?? "").split("\n")) { + const line2 = raw.trim().replace(/\*\*|__/g, ""); + const heading = /^#{1,6}\s+(.+)$/.exec(line2); if (heading) { @@ -1833,7 +1941,7 @@ diff --git a/client.js b/client.js var historyLoading = (snapshot) => snapshot?.openState === "cold" || snapshot?.openState === "loading"; function toolArgs(root) { if (typeof root.args?.text === "function") { -@@ -1597,17 +1597,46 @@ function commRunOf(order, peers, turn, hiddenTurns = []) { +@@ -1597,17 +1597,46 @@ } return { count, peerIds }; } @@ -1853,17 +1961,17 @@ diff --git a/client.js b/client.js if (hiddenTurns.includes(entry.turn)) continue; - const text = entry.response || entry.prompt || ""; - if (text !== "") return stripHeader(text).replace(/\s+/g, " ").trim(); -+ const text = (unsentTurns.includes(entry.turn) ? "" : entry.response) || entry.prompt || ""; -+ if (text === "") continue; -+ return readableLine(text, names); ++ const text2 = (unsentTurns.includes(entry.turn) ? "" : entry.response) || entry.prompt || ""; ++ if (text2 === "") continue; ++ return readableLine(text2, names); } return ""; } -+function readableLine(text, names = []) { -+ return dropKnownTrace(collapsedLine(text), names); ++function readableLine(text2, names = []) { ++ return dropKnownTrace(collapsedLine(text2), names); +} -+function collapsedLine(text) { -+ return visibleText(stripTrace(stripHeader(`${text}`)).replace(/\s+/g, " ").trim()); ++function collapsedLine(text2) { ++ return visibleText(stripTrace(stripHeader(`${text2}`)).replace(/\s+/g, " ").trim()); +} +function dropKnownTrace(line2, names) { + const known = [...new Set((Array.isArray(names) ? names : []).map((name) => String(name ?? "").trim()).filter((name) => name !== ""))].sort((a, b) => b.length - a.length); @@ -1877,71 +1985,22 @@ diff --git a/client.js b/client.js + return /^[\s,,.。::]*$/.test(rest) ? "" : rest; +} +var botNames = (roster) => [...roster?.bots ?? [], ...roster?.remoteBots ?? [], ...roster?.agents ?? []].map((entry) => entry?.name).filter((name) => typeof name === "string" && name !== ""); -+function speechOf(text, names = []) { -+ return readableLine(text, names).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim(); ++function speechOf(text2, names = []) { ++ return readableLine(text2, names).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim(); +} var formatTokens = (count) => count >= 1e6 ? `${(count / 1e6).toFixed(1)}M` : count >= 1e3 ? `${(count / 1e3).toFixed(1)}K` : String(count); function usageOf(list, ids) { let input = 0; -@@ -1624,491 +1624,1490 @@ function usageOf(list, ids) { +@@ -1624,9 +1624,982 @@ const hit = input > 0 ? Math.round(cacheRead / input * 100) : 0; return t("{tokens} tok · Cache hit {hit}%", { tokens: formatTokens(input + output), hit }); } -- --// src/client/motion.js --var import_react = require("react"); --var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; --function springEasing(stiffness, damping) { -- const w0 = Math.sqrt(stiffness); -- const zeta = damping / (2 * w0); -- const at = (t2) => { -- if (zeta >= 1) return 1 - (1 + w0 * t2) * Math.exp(-w0 * t2); -- const wd = w0 * Math.sqrt(1 - zeta * zeta); -- return 1 - Math.exp(-zeta * w0 * t2) * (Math.cos(wd * t2) + zeta * w0 / wd * Math.sin(wd * t2)); -- }; -- let end = 0.05; -- while (end < 2 && Math.abs(1 - at(end)) > 1e-3) end += 0.01; -- const points = Array.from({ length: 25 }, (_, index) => (index === 24 ? 1 : at(end * index / 24)).toFixed(4)); -- return { duration: Math.round(end * 1e3), easing: `linear(${points.join(",")})`, stiffness, damping }; +function faceRowTitle(title) { -+ const text = String(title ?? ""); -+ const named = text.match(/^Face-to-face · (.+) · (\d{6})$/); ++ const text2 = String(title ?? ""); ++ const named = text2.match(/^Face-to-face · (.+) · (\d{6})$/); + if (named) return `${named[2]} · ${named[1]}`; -+ return text.match(/^Face-to-face · (\d{6})$/)?.[1] ?? text; - } --var SPRING_SHAPE = springEasing(400, 31.2); --var SPRING_TAP = springEasing(900, 48); --var SPRING_LIST = springEasing(484, 35.2); --var SPRING_STRETCH = springEasing(320, 20); --var SPRING_POP = springEasing(500, 22); --var SPRING_LEAD = springEasing(784, 48.2); --var SPRING_TRAIL = springEasing(289, 29.2); --var QUICK_EDGES = { lead: springEasing(900, 51), trail: springEasing(400, 34) }; --var spring = (curve, ...properties) => properties.map((property) => `${property} ${curve.duration}ms ${curve.easing}`).join(","); --function liveSprings(apply2) { -- const values = /* @__PURE__ */ new Map(); -- let frame = 0; -- let last = 0; -- const read = () => Object.fromEntries([...values].map(([key, value]) => [key, value.x])); -- const step = (now) => { -- const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); -- last = now; -- const steps = Math.ceil(dt * 240); -- const h34 = dt / steps; -- let moving = false; -- for (const value of values.values()) { -- for (let index = 0; index < steps; index += 1) { -- value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h34; -- value.x += value.v * h34; -- } -- if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { -- value.x = value.to; -- value.v = 0; -- } else moving = true; -- } -- apply2(read()); -- frame = moving ? requestAnimationFrame(step) : 0; -- }; ++ return text2.match(/^Face-to-face · (\d{6})$/)?.[1] ?? text2; ++} +function modelOf(bot, roster) { + const ref = bot.model ?? bot.appliedModel; + if (!ref) return void 0; @@ -1956,84 +2015,28 @@ diff --git a/client.js b/client.js +function awayGroups(groups, roomsById) { + return (groups ?? []).filter((entry) => roomsById?.[entry.id]?.origin !== "face-to-face"); +} -+ + +-// src/client/motion.js +// src/client/sources.js +function createSource(initial) { + let value = initial; + const listeners2 = /* @__PURE__ */ new Set(); - return { -- target: () => values.size ? Object.fromEntries([...values].map(([key, value]) => [key, value.to])) : null, -- set(targets, curves, jump) { -- for (const [key, to] of Object.entries(targets)) { -- const value = values.get(key); -- const { stiffness: k, damping: c } = curves[key]; -- if (!value || jump) values.set(key, { x: to, v: 0, to, k, c }); -- else Object.assign(value, { to, k, c }); -- } -- if (jump) { -- cancelAnimationFrame(frame); -- frame = 0; -- apply2(read()); -- return; -- } -- if (!frame) { -- last = performance.now(); -- frame = requestAnimationFrame(step); -- } ++ return { + getSnapshot: () => value, + subscribe: (listener) => { + listeners2.add(listener); + return () => { + listeners2.delete(listener); + }; - }, -- stop: () => cancelAnimationFrame(frame) ++ }, + set(next) { + const resolved = typeof next === "function" ? next(value) : next; + if (resolved === value) return; + value = resolved; + for (const listener of [...listeners2]) listener(); + } - }; - } --var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; --function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { -- const ownBox = (0, import_react.useRef)(null); -- const box = given ?? ownBox; -- const thumb = (0, import_react.useRef)(null); -- const own = (0, import_react.useRef)(null); -- if (own.current === null) { -- own.current = { -- key: void 0, -- node: null, -- observer: null, -- springs: liveSprings(({ start, end }) => { -- const knob = thumb.current; -- if (!knob) return; -- knob.style.transform = axis === "x" ? `translateX(${start}px)` : `translateY(${start}px)`; -- knob.style[axis === "x" ? "width" : "height"] = `${Math.max(0, end - start)}px`; -- }) -- }; -- } -- const place = (jump) => { -- const container = box.current; -- const knob = thumb.current; -- if (!container || !knob) return; -- const chosen = container.querySelector(selector); -- if (!chosen) { -- knob.dataset.hidden = ""; -- return; -- } -- const start = axis === "x" ? chosen.offsetLeft : chosen.offsetTop; -- const end = start + (axis === "x" ? chosen.offsetWidth : chosen.offsetHeight); -- const { springs } = own.current; -- const before = springs.target(); -- const hidden = knob.dataset.hidden !== void 0; -- if (jump && !hidden && before?.start === start && before?.end === end && container.dataset.glide !== void 0) return; -- delete knob.dataset.hidden; -- const forward = before === null || start >= before.start; -- springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); -- container.dataset.glide = ""; ++ }; ++} +var EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bots: [], rooms: [], humans: [], self: null, agents: [], exchangeTurns: {}, remoteTypedTurns: {}, remoteAwaiting: {}, questions: {}, secrets: [], capabilities: [], capabilityStatuses: {}, secretRequests: {}, models: [], defaultModel: null, modelsKey: null, prefs: {}, byId: {}, roomsById: {}, agentsById: {}, agentOf: {}, ready: false }; +var isMainOf = (roster, id) => id !== void 0 && (roster.mainBotIds ?? []).includes(id); +function indexRoster(value) { @@ -2046,26 +2049,7 @@ diff --git a/client.js b/client.js + if (prev && prev !== bot && !prev.remote && bot?.remote) return; + if (prev && prev !== bot && prev.id === id) return; + byId[id] = bot; - }; -- (0, import_react.useLayoutEffect)(() => { -- const state = own.current; -- const node = box.current; -- if (node === state.node && key === state.key) return; -- const fresh = node !== state.node; -- state.key = key; -- if (fresh) { -- state.observer?.disconnect(); -- state.node = node; -- state.observer = node && typeof ResizeObserver === "function" ? new ResizeObserver(() => place(true)) : null; -- state.observer?.observe(node); -- } -- place(fresh); -- }); -- (0, import_react.useEffect)(() => () => { -- own.current.observer?.disconnect(); -- own.current.springs.stop(); -- }, []); -- return { box, thumb }; ++ }; + for (const bot of value.bots) { + for (const id of [...bot.parts ?? [], bot.sessionId ?? bot.id, bot.id]) claim(bot, id); + } @@ -2078,75 +2062,20 @@ diff --git a/client.js b/client.js + for (const sessionId of agent.sessions ?? []) agentOf[sessionId] = agent.id; + } + return { ...value, agents: value.agents ?? [], byId, roomsById, agentsById, agentOf, ready: true }; - } --function Segmented({ label, value, className, children }) { -- const glide = useGlide(value); -- return (0, import_react.createElement)( -- "div", -- { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, -- (0, import_react.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- children -- ); ++} +function rosterBot(roster, id) { + if (id === void 0 || id === null || id === "") return void 0; + const list = roster?.bots ?? []; + const own = list.find((bot) => bot && !bot.remote && (bot.id === id || bot.sessionId === id || (bot.parts ?? []).includes(id))); + if (own) return own; + return roster?.byId?.[id]; - } --function useFlip(list, key, selector, id = (node) => node.dataset.flip) { -- const tops = (0, import_react.useRef)(null); -- (0, import_react.useLayoutEffect)(() => { -- const container = list.current; -- if (!container) return; -- const next = /* @__PURE__ */ new Map(); -- const moving = tops.current !== null && !reducedMotion(); -- for (const node of container.querySelectorAll(selector)) { -- const name = id(node); -- const top = node.offsetTop; -- next.set(name, top); -- if (!moving) continue; -- const before = tops.current.get(name); -- if (before === void 0) { -- node.animate([{ opacity: 0, filter: "blur(4px)" }, { opacity: 1, filter: "blur(0)" }], { duration: 180, easing: "ease-out" }); -- } else if (Math.abs(before - top) > 0.5) { -- node.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], { duration: SPRING_LIST.duration, easing: SPRING_LIST.easing }); -- } -- } -- tops.current = next; -- }, [key]); ++} +function botByName(bots, name) { + const wanted = String(name ?? "").toLowerCase(); + if (wanted === "") return void 0; + const list = Array.isArray(bots) ? bots : []; + return list.find((bot) => bot && !bot.remote && String(bot.name ?? "").toLowerCase() === wanted) ?? list.find((bot) => bot && String(bot.name ?? "").toLowerCase() === wanted); - } --function useMorph(box, key) { -- const last = (0, import_react.useRef)({ key, height: null }); -- (0, import_react.useLayoutEffect)(() => { -- const node = box.current; -- const state = last.current; -- if (!node) { -- last.current = { key, height: null }; -- return; -- } -- const running = node.getAnimations().filter((animation2) => animation2.id === "bt-morph"); -- if (key === state.key) { -- if (running.length === 0) state.height = node.offsetHeight; -- return; -- } -- const from = running.length ? node.offsetHeight : state.height; -- for (const animation2 of running) animation2.cancel(); -- const to = node.offsetHeight; -- last.current = { key, height: to }; -- if (from === null || Math.abs(from - to) < 1 || reducedMotion()) return; -- node.dataset.morph = ""; -- const animation = node.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, id: "bt-morph" }); -- animation.onfinish = () => { -- delete node.dataset.morph; -- }; -- animation.oncancel = () => { -- if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph; ++} +function isAgentSession(roster, cache, sessionId) { + if (sessionId === void 0 || sessionId === null || sessionId === "") return false; + if (roster.ready) return roster.agentOf[sessionId] !== void 0; @@ -2163,7 +2092,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"; @@ -2180,8 +2109,7 @@ diff --git a/client.js b/client.js + const clamp = (value) => Math.max(-1, Math.min(1, value)); + node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); + node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); - }; -- }); ++ }; + const onMove = (event) => { + point = { x: event.clientX, y: event.clientY }; + if (!frame) frame = requestAnimationFrame(place); @@ -2199,9 +2127,7 @@ diff --git a/client.js b/client.js + document.documentElement.removeEventListener("pointerleave", onLeave); + }; + }, [enabled]); - } --function useLinger(value, ms) { -- const [kept, setKept] = (0, import_react.useState)(value); ++} +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 ?? (bot?.remote ? lookAvoidingLocals(bot) : lookOf(bot)); @@ -2281,22 +2207,17 @@ diff --git a/client.js b/client.js +function useSettled(state) { + const prev = (0, import_react.useRef)(state); + const [settled, setSettled] = (0, import_react.useState)(false); - (0, import_react.useEffect)(() => { -- if (value) { -- setKept(value); ++ (0, import_react.useEffect)(() => { + const was = prev.current; + prev.current = state; + if (state !== "idle") { + setSettled(false); - return void 0; - } -- const timer = setTimeout(() => setKept(value), reducedMotion() ? 0 : ms); ++ return void 0; ++ } + if (was === "idle" || was === "alert") return void 0; + setSettled(true); + const timer = setTimeout(() => setSettled(false), 1800); - return () => clearTimeout(timer); -- }, [value]); -- return [value || kept, !value && Boolean(kept)]; ++ return () => clearTimeout(timer); + }, [state]); + return settled; +} @@ -2389,112 +2310,12 @@ diff --git a/client.js b/client.js + return (0, import_react.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); + }) + ); - } - --// src/client/styles.js --var SHELL_CSS = ` --body{--bt-ink:#141414;--bt-ink-2:rgba(20,20,20,.6);--bt-ink-3:rgba(20,20,20,.36);--bt-bubble:#eeeeee;--bt-sidebar:#f7f7f7;--bt-main:#fcfcfc;--bt-line:rgba(20,20,20,.08);--bt-line-2:rgba(20,20,20,.15);--bt-line-weak:rgba(20,20,20,.1);--bt-hover:rgba(119,119,119,.09);--bt-active:rgba(119,119,119,.17);--bt-card:#ffffff;--bt-accent:#4d6bfe;--bt-green:#00c972;--bt-star:#ff9800;--bt-user:#070707;--bt-user-ink:#fcfcfc;--bt-tag-bg:rgba(20,20,20,.06);--bt-tag-ink:rgba(20,20,20,.56)} --body[data-ds-dark-theme]{--bt-ink:#f2f2f2;--bt-ink-2:rgba(242,242,242,.6);--bt-ink-3:rgba(242,242,242,.36);--bt-bubble:#262626;--bt-sidebar:#1b1b1b;--bt-main:#141414;--bt-line:rgba(255,255,255,.08);--bt-line-2:rgba(255,255,255,.15);--bt-line-weak:rgba(255,255,255,.1);--bt-hover:rgba(160,160,160,.12);--bt-active:rgba(160,160,160,.2);--bt-card:#1f1f1f;--bt-user:#f2f2f2;--bt-user-ink:#141414;--bt-tag-bg:rgba(255,255,255,.09);--bt-tag-ink:rgba(242,242,242,.62)} --${Object.entries(INKS).map(([name, [light]]) => `body{--bt-ink-${name}:${light}}`).join("")} --${Object.entries(INKS).map(([name, [, dark]]) => `body[data-ds-dark-theme]{--bt-ink-${name}:${dark}}`).join("")} --.bt-mark{display:inline-block;flex:none;position:relative;line-height:0} --.bt-mark svg{width:100%;height:100%;overflow:visible} --.bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)} --.bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box} --.bt-mark .bt-b{transform-origin:50% 100%} --.bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center} --/* Motion levels (Settings → Bots → Animation) set body[data-bt-motion]: quiet moves a -- mark only for its state; normal, the default, adds petting, pokes and eyes that follow -- the pointer; lively adds blinks and fidgets at rest and a question riding on the head. */ --body[data-bt-motion=lively] .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{animation:bt-blink-idle 5s linear infinite;animation-delay:var(--bt-delay,0s)} --body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle]:is([data-fidget=hop],[data-fidget=sway],[data-fidget=stretch]) :is(.bt-b,.bt-mark-img){animation:var(--bt-fidget) 9s ease-in-out var(--bt-fidget-delay,0s) infinite} --body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=hop]{--bt-fidget:bt-fidget-hop} --body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=sway]{--bt-fidget:bt-fidget-sway} --body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=stretch]{--bt-fidget:bt-fidget-stretch} --body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=look] .bt-eyes{animation:bt-fidget-look 9s ease-in-out var(--bt-fidget-delay,0s) infinite} --@keyframes bt-fidget-hop{0%,8%,100%{transform:none}3%{transform:translateY(-10%) scale(.97,1.04)}5.5%{transform:translateY(0) scale(1.05,.95)}} --@keyframes bt-fidget-sway{0%,10%,100%{transform:none}2.5%{transform:rotate(-8deg)}5%{transform:rotate(7deg)}7.5%{transform:rotate(-3deg)}} --@keyframes bt-fidget-stretch{0%,9%,100%{transform:none}4%{transform:scale(.94,1.08)}7%{transform:scale(1.03,.97)}} --@keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}} --body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite} --body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none} --@keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}} --/* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its -- size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes -- closed in a happy arc, and springs back when the pointer leaves. */ --:is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster)::before{content:'';position:absolute;inset:-20%;border-radius:50%} --.bt-mark[data-live] .bt-b,.bt-mark[data-live] .bt-mark-img{transition:rotate .5s cubic-bezier(.34,1.56,.64,1),translate .5s cubic-bezier(.34,1.56,.64,1),scale .3s ease-out} --.bt-mark[data-live] .bt-eye{transition:scale .14s ease-in} --.bt-mark[data-live] .bt-eye-happy{transition:opacity .12s ease-out} --body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]):not(.bt-mark-poke) :is(.bt-b,.bt-mark-img){rotate:8deg;translate:5% -1%;animation:bt-nuzzle .78s ease-in-out .32s infinite} --body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{scale:1 0;animation:none} --body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye-happy{opacity:1;transition-delay:.1s} --@keyframes bt-nuzzle{50%{rotate:14deg;translate:8% -3%;scale:1.03 .97}} --.bt-mark[data-gaze] .bt-b{translate:calc(var(--bt-gx,0) * 5%) calc(var(--bt-gy,0) * 4%);transition:translate .18s ease-out} --.bt-mark[data-gaze] .bt-gaze{transform:translate(calc(var(--bt-gx,0) * 12%),calc(var(--bt-gy,0) * 11%));transition:transform .18s ease-out} --body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-b{translate:none} --body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-gaze{transform:none} --.bt-mark[data-state=thinking] .bt-b{animation:bt-ponder 2.2s ease-in-out infinite} --.bt-mark[data-state=thinking] .bt-eyes{animation:bt-squint 2.2s ease-in-out infinite} --.bt-mark[data-state=searching] .bt-b{animation:bt-seek 1s ease-in-out infinite} --.bt-mark[data-state=searching] .bt-eyes{animation:bt-scan 1.6s ease-in-out infinite} --.bt-mark[data-state=working] .bt-b{animation:bt-bob 1.6s ease-in-out infinite} --.bt-mark[data-state=working]:is([data-shape=star],[data-shape=hex],[data-shape=plus],[data-shape=gem],[data-shape=magnet],[data-shape=ring]) .bt-b{animation:bt-wiggle 1.4s ease-in-out infinite;transform-origin:50% 50%} --.bt-mark[data-state=working]:is([data-shape=gumdrop],[data-shape=peanut],[data-shape=cushion],[data-shape=squircle],[data-shape=moon],[data-shape=wave]) .bt-b{animation:bt-squash 1.2s ease-in-out infinite} --.bt-mark[data-state=working] .bt-eye{animation:bt-blink 2.4s ease-in-out infinite} --.bt-mark[data-state=sending] .bt-b{animation:bt-dash 1.1s cubic-bezier(.4,0,.2,1) infinite} --.bt-mark[data-state=orbit] .bt-b{animation:bt-sway 2.6s ease-in-out infinite;transform-origin:50% 50%} --.bt-mark[data-state=orbit] .bt-eyes{animation:bt-roll 2.6s linear infinite} --/* A question for you hops twice when it arrives and the badge keeps it in view after; -- a lively mark keeps hopping. */ --.bt-mark[data-state=alert] .bt-b{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2} --.bt-mark[data-shape=image]:not([data-state=idle]) .bt-mark-img{animation:bt-bob 1.6s ease-in-out infinite} --.bt-mark[data-shape=image][data-state=alert] .bt-mark-img{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2} --body[data-bt-motion=lively] .bt-mark[data-state=alert] :is(.bt-b,.bt-mark-img){animation-iteration-count:infinite} --.bt-mark[data-state=done] .bt-b{animation:bt-done 1.8s linear 1} --.bt-mark[data-shape=image][data-state=done] .bt-mark-img{animation:bt-done 1.8s linear 1;transform-origin:50% 100%} --@keyframes bt-done{0%{transform:none;animation-timing-function:cubic-bezier(.4,0,.2,1)}13.3%{transform:scale(1.12,.88);animation-timing-function:cubic-bezier(.4,0,.2,1)}25.6%{transform:scale(.93,1.09);animation-timing-function:cubic-bezier(.4,0,.2,1)}37.8%{transform:scale(1.04,.97);animation-timing-function:cubic-bezier(.4,0,.2,1)}51.1%,100%{transform:none}} --.bt-mark-poke .bt-b{animation:bt-poke .8s cubic-bezier(.3,.7,.4,1) 1!important;transform-origin:50% 60%!important} --.bt-mark-poke .bt-mark-img{animation:bt-poke-img .6s ease-out 1!important} --.bt-mark[data-poke]{cursor:pointer} --body[data-bt-motion=quiet] .bt-mark-poke :is(.bt-b,.bt-mark-img){animation:none!important} --body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} --@keyframes bt-bob{0%,100%{transform:translateY(0) rotate(0)}30%{transform:translateY(-4%) rotate(-5deg)}65%{transform:translateY(0) rotate(4deg)}} --@keyframes bt-wiggle{0%,100%{transform:rotate(0) scale(1)}25%{transform:rotate(-12deg) scale(1.04)}75%{transform:rotate(12deg) scale(1.04)}} --@keyframes bt-squash{0%,100%{transform:scale(1,1)}30%{transform:scale(1.08,.9)}55%{transform:translateY(-4%) scale(.94,1.08)}80%{transform:scale(1.02,.98)}} --@keyframes bt-ponder{0%,100%{transform:translateY(0) rotate(4deg)}50%{transform:translateY(-3%) rotate(7deg)}} --@keyframes bt-squint{0%,100%{transform:translate(0,-4%) scaleY(.7)}50%{transform:translate(5%,-5%) scaleY(.78)}} --@keyframes bt-seek{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-5%) rotate(-1deg)}} --@keyframes bt-scan{0%,100%{transform:translateX(-12%)}50%{transform:translateX(12%)}} --@keyframes bt-dash{0%,100%{transform:translateX(0) rotate(0)}35%{transform:translateX(9%) rotate(7deg)}55%{transform:translateX(-2%) rotate(-3deg)}} --@keyframes bt-sway{0%,100%{transform:rotate(-10deg)}50%{transform:rotate(10deg)}} --@keyframes bt-roll{0%,100%{transform:translate(12%,0)}25%{transform:translate(0,9%)}50%{transform:translate(-12%,0)}75%{transform:translate(0,-9%)}} --@keyframes bt-hop{0%,55%,100%{transform:translateY(0) scale(1,1)}20%{transform:translateY(-14%) scale(.96,1.05)}40%{transform:translateY(0) scale(1.06,.94)}} --@keyframes bt-poke{0%{transform:none}55%{transform:translateY(-16%) rotate(360deg)}78%{transform:translateY(0) scale(1.08,.92) rotate(360deg)}100%{transform:rotate(360deg)}} --@keyframes bt-poke-img{0%,100%{transform:scale(1)}40%{transform:scale(1.14) rotate(-6deg)}} --/* Idle clip: two .26s blinks (lid to 3%, then a 6% overshoot) at 1.48s and 3.46s. */ --@keyframes bt-blink-idle{0%,29.6%,35%,69.2%,74.6%,100%{transform:scaleY(1)}32.2%,71.8%{transform:scaleY(.03)}33.8%,73.4%{transform:scaleY(1.06)}} --@keyframes bt-blink{0%,42%,50%,100%{transform:scaleY(1)}46%{transform:scaleY(.03)}48%{transform:scaleY(1.06)}} --@media (prefers-reduced-motion:reduce){.bt-mark *{animation:none!important;transition:none!important}} --.bt-badge{position:absolute;right:0;bottom:0;width:12px;height:12px;border-radius:50%;corner-shape:round;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-sidebar)} --.bt-badge-admin{background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar))} --.bt-badge-admin svg{width:7px;height:7px} --.bt-badge-main{right:calc(var(--bt-size,36px) * -.07);bottom:calc(var(--bt-size,36px) * -.07);width:clamp(14px,calc(var(--bt-size,36px) * .36),22px);height:clamp(14px,calc(var(--bt-size,36px) * .36),22px);border-radius:0;box-shadow:none} --.bt-badge-main svg{width:100%;height:100%;overflow:visible} --.bt-badge-main path{fill:var(--bt-star);stroke:var(--bt-sidebar);stroke-width:2.6;stroke-linejoin:round;paint-order:stroke} --.bt-badge-alert{background:color-mix(in srgb,var(--bt-accent) 78%,var(--bt-sidebar));color:var(--bt-accent-ink,#fff);font-size:8.5px;line-height:12px;font-weight:600;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} --.bt-badge-working{background:color-mix(in srgb,var(--bt-green) 82%,var(--bt-sidebar));width:8px;height:8px;right:1px;bottom:1px} --.bt-cluster{position:relative;display:inline-block;flex:none;line-height:0} --.bt-cluster>*{position:absolute} --.bt-cluster-more,.bt-cluster-you{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-active);color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;font-weight:500;line-height:1;font-variant-numeric:tabular-nums} --.bt-cluster-you{background:color-mix(in srgb,var(--bt-ink) 12%,transparent)} --.bt-stack{display:inline-flex;align-items:center;flex:none;line-height:0} --.bt-stack>*+*{margin-left:calc(var(--bt-stack-size) * -.3)} --.bt-stack-more{font-size:11px;line-height:16px;color:var(--bt-ink-3);margin-left:3px!important} ++} ++ +// src/client/room-invite-preview.js +var INVITE_PREVIEW_NAME_MAX = 60; -+function stripInviteMarkers(text) { -+ let out = String(text ?? ""); ++function stripInviteMarkers(text2) { ++ let out = String(text2 ?? ""); + for (let previous; previous !== out; ) { + previous = out; + out = out.replace(/<\/?\s*untrusted-(?:file|message)/gi, ""); @@ -2507,9 +2328,9 @@ diff --git a/client.js b/client.js +function inviteMemberCount(value) { + if (typeof value === "number") return Number.isFinite(value) ? value : NaN; + if (typeof value !== "string") return NaN; -+ const text = value.trim(); -+ if (!text) return NaN; -+ const count = Number(text); ++ const text2 = value.trim(); ++ if (!text2) return NaN; ++ const count = Number(text2); + return Number.isFinite(count) ? count : NaN; +} +function invitePreviewOf(seen) { @@ -2531,29 +2352,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: h48, preview, kind = "face-link-preview", busy = false, joinLabel = "Join", clearLabel = "Clear", showClear = true, onJoin, onClear, now } = {}) { -+ if (!preview || typeof h48 !== "function") return null; ++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; + 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 h48( ++ return h49( + "div", + { className: "fs-preview", "data-flybot": at, role: "status" }, -+ h48("div", { className: "fs-preview-name", "data-flybot": child("name") }, preview.name || "Face-to-face room"), -+ preview.lead ? h48("div", { className: "fs-preview-meta", "data-flybot": child("lead") }, `Lead: ${preview.lead}`) : null, -+ preview.people ? h48("div", { className: "fs-preview-meta", "data-flybot": child("members") }, preview.people) : null, -+ preview.expiresAt ? h48("div", { className: "fs-preview-meta", "data-flybot": child("expiry") }, countdown) : null, -+ onJoin || onClear ? h48( ++ 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( + "div", + { className: "fs-preview-foot", "data-flybot": child("foot") }, -+ showClear && onClear ? h48("button", { ++ showClear && onClear ? h49("button", { + type: "button", + className: "fs-btn fs-btn-sm", + "data-flybot": child("clear"), + disabled: !!busy, + onClick: onClear + }, clearLabel) : null, -+ onJoin ? h48("button", { ++ onJoin ? h49("button", { + type: "button", + className: "fs-btn fs-btn-sm fs-btn-primary", + "data-flybot": child("join"), @@ -2571,42 +2392,7 @@ diff --git a/client.js b/client.js + const total = Math.max(1, Math.ceil(left / 1e3)); + return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")} left to join`; +} - --.bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} --.bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0} --.bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap} --.bt-brand-whale{display:inline-flex;color:var(--bt-accent)} --.bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease} --.bt-round:hover{background:var(--bt-hover)} --.bt-round:active{transform:scale(.94)} --.bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto} --.bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px} --.bt-tile{display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease} --.bt-tile:hover{background:var(--bt-hover)} --.bt-tile[aria-current=page]{background:var(--bt-active)} --.bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis} --.bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease} --.bt-row:hover{background:var(--bt-hover)} --.bt-row[aria-current=page]{background:var(--bt-active)} --.bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} --.bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} --.bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} --.bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} --.bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} --.bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)} --.bt-unread-wait{background:var(--bt-star)} --.bt-rail{align-items:center;padding:0 0 8px} --.bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px} --.bt-side-foot{display:flex;padding-top:8px} --.bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease} --.bt-connect:hover{background:var(--bt-hover)} --.bt-connect svg{flex:none;width:15px;height:15px} --.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0} --.bt-connect-warn{flex:none;width:16px;height:16px;border-radius:50%;background:#E5484D;color:#fff;font-size:11px;line-height:16px;font-weight:700;display:inline-flex;align-items:center;justify-content:center} --.bt-connect-tip{position:fixed;box-sizing:border-box;width:max-content;max-width:260px;padding:8px 12px;border-radius:10px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 8px 24px -4px rgba(0,0,0,.14);color:var(--bt-ink);font-size:12px;line-height:17px;white-space:normal;text-align:center;z-index:70;transform-origin:bottom left;animation:bt-tip-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --@keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}} --@media (prefers-reduced-motion:reduce){.bt-connect-tip{animation:none}} ++ +// src/client/approval-claims.js +var claims = /* @__PURE__ */ new Map(); +var claimListeners = /* @__PURE__ */ new Set(); @@ -2632,56 +2418,13 @@ diff --git a/client.js b/client.js + claimListeners.delete(fn); + }; +} - --.bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none} --.bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}} --.bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)} --.bt-pill:active{transform:scale(.965)} --.bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500} --.bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}} --.bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}} --.bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}} --.bt-pill-mem-in::before{content:"";flex:none;width:1px;height:14px;margin-right:4px;background:var(--bt-line-2)} --.bt-pill[data-memory=updated] .bt-pill-mem-in{opacity:1;filter:none;transform:none;transition-delay:70ms} --.bt-pill-mem-ico{flex:none;display:inline-flex;width:16px;height:16px;color:var(--bt-accent)} --.bt-pill-mem-ico svg{width:16px;height:16px} --.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:bt-mem-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 90ms both} --@keyframes bt-mem-pop{from{transform:scale(.4) rotate(-14deg)}to{transform:none}} --.bt-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0} --@media (prefers-reduced-motion:reduce){.bt-pill-mem,.bt-pill-mem-in{transition:none!important}.bt-pill-mem-in{filter:none;transform:none}.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:none}} --.bt-swap{color:var(--bt-ink-3);font-size:12px} --.bt-pill-go{flex:none;display:inline-flex;align-items:center;justify-content:flex-end;width:0;margin-left:-8px;overflow:hidden;opacity:0;filter:blur(6px);transform:translateX(-6px);color:var(--bt-ink-2);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},margin-left ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},opacity .18s ease,filter .18s ease} --.bt-pill-go svg{flex:none;transition:transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --.bt-pill:hover .bt-pill-go,.bt-pill:focus-visible .bt-pill-go,.bt-pill[aria-expanded=true] .bt-pill-go{width:18px;margin-left:0;opacity:1;filter:none;transform:none} --.bt-pill[aria-expanded=true] .bt-pill-go svg{transform:rotate(180deg)} --.bt-pill[data-memory=updated]:is(:hover,:focus-visible,[aria-expanded=true]) .bt-pill-go{width:0;margin-left:-8px;opacity:0;filter:blur(6px);transform:translateX(-6px)} --@media (prefers-reduced-motion:reduce){.bt-pill,.bt-pill-go,.bt-pill-go svg{transition:none!important}.bt-pill-go{filter:none}.bt-pill:active{transform:none}} ++ +// src/shared/approval-waiting.js +var WAITING_STATUSES = Object.freeze(["pending", "pending_approval"]); +function isApprovalWaiting(status) { + return WAITING_STATUSES.includes(status); +} - --.bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0} --.bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere} --.bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px} --.bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px} --.bt-bubble [class*="_markdown"]>*+*{margin-top:10px} --.bt-bubble [class*="_markdown"]{color:inherit!important;font-size:inherit!important;line-height:inherit!important} --.bt-bubble p{margin:0} --.bt-bubble p + p{margin-top:6px} --.bt-bubble ul,.bt-bubble ol{margin:4px 0;padding-left:20px} --.bt-bubble a[href^="${MENTION_ORIGIN}"]{text-decoration:none;font-weight:400;cursor:pointer;white-space:nowrap} --.bt-bubble a[href^="${MENTION_ORIGIN}"]::before{content:"";display:inline-block;width:16px;height:16px;margin-right:3px;vertical-align:-3px;background:currentColor;-webkit-mask:${DEFAULT_MASK} center/contain no-repeat;mask:${DEFAULT_MASK} center/contain no-repeat} --.bt-group-msg{display:flex;flex-direction:column;gap:4px;margin:16px 0 0} --.bt-group-msg:first-child{margin-top:0} --.bt-lead{flex:none;display:flex;margin-bottom:1px} --button.bt-lead{border:0;background:none;padding:0;cursor:pointer} --/* Rows without the avatar start where the bubble beside it starts: 22px avatar + 8px gap. */ --.bt-astack[data-lead]>.bt-msg:not(:last-child)>.bt-msg-line,.bt-astack[data-lead] .bt-reacts{margin-left:30px} --.bt-group-name{font-size:12px;line-height:16px;font-weight:500;margin-left:42px} --.bt-x{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;width:24px;height:24px;border-radius:6px;font-size:16px;line-height:24px;padding:0;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}} --.bt-x:hover{background:var(--bt-hover);color:var(--bt-ink)} ++ +// src/shared/held-retry.js +var TERMINAL = [ + // A friend's Bot that is gone or no longer shared (plugins/social/src/social.js: botGoneError / notSharedError). @@ -2701,79 +2444,7 @@ diff --git a/client.js b/client.js + const said = `${record.error ?? ""} ${record.code ?? ""}`; + return !TERMINAL.some((reason) => reason.test(said)); +}; - --.bt-q{margin:0 auto 8px;width:100%;max-width:var(--dsh-chat-content-width,748px);display:flex;flex-direction:column;gap:12px;--bt-q-edge:color-mix(in srgb,var(--bt-ink) 10%,transparent);--bt-q-rule:color-mix(in srgb,var(--bt-ink) 8%,transparent);--bt-q-rule-2:color-mix(in srgb,var(--bt-ink) 5%,transparent);background:var(--bt-card);box-shadow:0 1px 3px rgba(20,20,20,.04),inset 0 0 0 1px var(--bt-q-edge);border-radius:16px;padding:14px 16px;box-sizing:border-box;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} --body[data-ds-dark-theme] .bt-q{--bt-q-edge:rgba(255,255,255,.16);--bt-q-rule:rgba(255,255,255,.12);--bt-q-rule-2:rgba(255,255,255,.08)} --@keyframes bt-q-in{from{transform:translateY(10px) scale(.97)}} --.bt-q-settled{animation:none} --.bt-q-head{display:flex;align-items:flex-start;gap:8px;min-width:0} --.bt-q-text{flex:1 1 0;min-width:0;display:flex;flex-direction:column} --.bt-q-title{margin:0;font-size:15px;line-height:22px;font-weight:600;overflow-wrap:anywhere} --.bt-q-detail{font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-q-x{flex:none;width:20px;height:20px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}} --.bt-q-x svg{width:14px;height:14px} --.bt-q-x:hover{background:var(--bt-hover);color:var(--bt-ink)} --.bt-q-x:focus-visible,.bt-q-opt:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} --.bt-q-list{display:flex;flex-direction:column;gap:1px;min-width:0;box-sizing:border-box;margin:0 -8px} --.bt-q-opt{position:relative;display:flex;align-items:flex-start;gap:16px;width:100%;min-width:0;box-sizing:border-box;margin:0;border:0;border-radius:12px;background:transparent;padding:10px 8px;font:inherit;color:var(--bt-ink);text-align:start;cursor:pointer;transition:background-color .12s ease} --.bt-q:not(.bt-q-raised)>.bt-q-head:has(+.bt-q-list){padding-bottom:12px;border-bottom:1px solid var(--bt-q-rule)} --.bt-q:not(.bt-q-raised)>.bt-q-head+.bt-q-list{margin-top:-6px} --.bt-q-opt+.bt-q-opt::before{content:"";position:absolute;left:calc(var(--bt-key,24px) + 24px);right:8px;top:-1px;height:1px;background:var(--bt-q-rule-2);transition:opacity .12s ease;pointer-events:none} --button.bt-q-opt:hover::before,button.bt-q-opt:hover+.bt-q-opt::before,.bt-q-opt[aria-pressed=true]::before,.bt-q-opt[aria-pressed=true]+.bt-q-opt::before{opacity:0} --.bt-q-raised .bt-q-opt::before{display:none} --button.bt-q-opt:hover{background:var(--bt-hover)} --button.bt-q-opt:active{background:var(--bt-active)} --.bt-q-opt[aria-pressed=true]{background:var(--bt-active)} --.bt-q:not(.bt-q-raised) :is(button.bt-q-opt:hover,.bt-q-opt[aria-pressed=true]) .bt-key{background:var(--bt-card);color:var(--bt-ink)} --.bt-key{flex:none;box-sizing:border-box;width:var(--bt-key,24px);height:var(--bt-key,24px);border-radius:8px;background:var(--bt-hover);color:var(--bt-ink-2);font-size:12px;line-height:var(--bt-key,24px);font-weight:500;text-align:center;font-variant-numeric:tabular-nums;transition:background-color .12s ease,color .12s ease} --.bt-q-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;padding-top:2px} --/* With a description the key spans the two text lines less 2px above and below. */ --.bt-q-opt:has(.bt-q-desc){--bt-key:34px} --.bt-q-opt:has(.bt-q-desc) .bt-key{margin-top:2px;border-radius:10px;font-size:13px} --.bt-q-opt:has(.bt-q-desc) .bt-q-body{padding-top:0} --.bt-q-label{font-size:14px;line-height:20px;font-weight:500;white-space:normal;overflow-wrap:anywhere} --.bt-q-desc{font-size:12px;line-height:17px;color:var(--bt-ink-2);overflow-wrap:anywhere} --.bt-q-check{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2);animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} --.bt-q-settled .bt-q-check{animation:none} --.bt-q-check svg{width:16px;height:16px} --.bt-q-settled .bt-key{opacity:.4} --.bt-q-settled .bt-q-label{color:color-mix(in srgb,var(--bt-ink) 45%,transparent)} --.bt-q-dismissed .bt-q-title{color:var(--bt-ink-2)} --.bt-q.bt-q-settled{gap:6px;padding:12px 16px} --.bt-q-settled .bt-q-title{font-size:14px;line-height:20px;font-weight:500} --.bt-q.bt-q-settled>.bt-q-head:has(+.bt-q-list){padding-bottom:0;border-bottom:0} --.bt-q.bt-q-settled>.bt-q-head+.bt-q-list{margin-top:0} --.bt-q-settled .bt-q-opt{padding:4px 8px} --.bt-q-badge{flex:none;height:20px;padding:0 7px;border-radius:999px;border:1px dotted var(--bt-line-2);color:var(--bt-ink-2);font-size:12px;line-height:18px;box-sizing:border-box} --.bt-q-custom{display:flex;align-items:flex-start;gap:8px;width:100%;min-width:0} --.bt-q-field{flex:1 1 0;min-width:0;display:flex;box-sizing:border-box;min-height:32px;padding:5px 11px;border-radius:8px;border:.5px solid var(--bt-line-2);background:none;cursor:text;transition:border-color .12s ease} --.bt-q-field:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)} --.bt-q-input{flex:1;width:100%;min-width:0;max-height:120px;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;resize:none;overflow-y:hidden;white-space:pre-wrap;overflow-wrap:anywhere} --.bt-q-input:not(:placeholder-shown){field-sizing:content} --.bt-q-input::placeholder{color:var(--bt-ink-3)} --.bt-q-submit,.bt-q-btn{flex:none;height:36px;padding:0 14px;border:0;border-radius:999px;font:inherit;font-size:14px;font-weight:500;cursor:pointer;transition:opacity .12s ease,background-color .12s ease,${spring(SPRING_TAP, "scale")}} --.bt-q-submit{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards,bt-veil-in .16s ease-out backwards} --@keyframes bt-q-submit-in{from{transform:translateX(8px) scale(.9)}} --.bt-q-submit:hover:not(:disabled){opacity:.86} --.bt-q-submit:disabled{opacity:.4;cursor:default} --.bt-q-btn{background:var(--bt-hover);color:var(--bt-ink)} --.bt-q-btn:hover{background:var(--bt-active)} --.bt-q-foot{display:flex;justify-content:flex-end;gap:8px} --.bt-q-raised{width:calc(var(--dsh-chat-content-width,748px) - 32px);max-width:calc(100% - 32px);gap:0;padding:0;box-shadow:0 2px 8px rgba(20,20,20,.035),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 5%,transparent)} --.bt-q-raised .bt-q-head{padding:14px 16px 4px 20px} --.bt-q-raised .bt-q-head:last-child{padding-bottom:16px} --.bt-q-raised .bt-q-list{gap:1px;margin:0;padding:8px} --.bt-q-raised .bt-q-opt{gap:4px;padding:7px;border-radius:16px;color:var(--bt-ink-2)} --.bt-q-raised button.bt-q-opt:hover{color:var(--bt-ink)} --.bt-q-raised .bt-q-opt[aria-pressed=true]{background:transparent;color:var(--bt-ink)} --.bt-q-raised .bt-q-opt[aria-pressed=true]:hover{background:var(--bt-hover)} --.bt-q-raised .bt-q-opt[aria-pressed=true] .bt-key,.bt-q-ownrow:focus-within .bt-key,.bt-q-ownrow[data-filled] .bt-key{background:var(--bt-user);color:var(--bt-user-ink)} --.bt-q-raised .bt-q-body{padding:2px 6px 0} --.bt-q-raised .bt-q-desc{color:color-mix(in srgb,var(--bt-ink) 61%,transparent)} --.bt-q-ownrow{cursor:text} --.bt-q-ownrow .bt-q-input{color:var(--bt-ink)} --.bt-q-raised .bt-q-foot{padding:0 8px 8px} --@media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}} ++ +// src/client/flybot-slots.js +var import_react2 = require("react"); +var call2 = (endpoint, payload) => call(`flybot/${endpoint}`, payload); @@ -3067,32 +2738,7 @@ diff --git a/client.js b/client.js +.bt-flybot-approval-files { margin: 0 0 8px; padding-left: 18px; font-size: 12px; } +.bt-file-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 0 30px; } +.bt-file-chip { font-size: 12px; padding: 3px 8px; border-radius: 8px; border: 1px solid var(--bt-line, rgba(0,0,0,.15)); background: transparent; cursor: pointer; } - --.bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out} --.bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box} --.bt-to-label{color:var(--bt-ink-2)} --.bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px} --.bt-token{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 6px 0 4px;border-radius:999px;background:var(--bt-hover);font-size:13px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} --.bt-token button{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;padding:0 2px} --.bt-admin-star{flex:none;display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px;border-radius:50%;corner-shape:round;background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar));vertical-align:-1px} --.bt-to-star{display:inline-flex;margin-right:-2px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} --.bt-to .bt-to-create{flex:none;height:28px;padding:0 12px;font-size:13px;animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} --.bt-drop{position:absolute;top:48px;left:32px;width:min(550px,calc(100% - 64px));display:flex;flex-direction:column;padding:6px;max-height:min(420px,60vh);overflow-y:auto;background:var(--bt-card);border:1px solid var(--bt-line-2);border-radius:12px;box-shadow:0 6px 20px -4px rgba(0,0,0,.12);box-sizing:border-box;z-index:1;transform-origin:24px 0;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out} --.bt-option{display:flex;align-items:center;gap:10px;border:0;background:none;border-radius:8px;padding:7px 8px;min-height:36px;box-sizing:border-box;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;text-align:left;width:100%} --.bt-option[aria-selected=true]{background:var(--bt-active)} --.bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)} --.bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} --.bt-option-hint{font-size:11px;color:var(--bt-ink-2);flex:none;animation:bt-veil-in .16s ease-out} --.bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none} --.bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)} --.bt-nc-spacer{flex:1} --.bt-nc-compose{padding:0 16px 14px;display:flex;justify-content:center} --.bt-nc-card{display:flex;align-items:flex-end;gap:6px;width:100%;max-width:var(--dsh-chat-content-width,748px);min-height:44px;padding:4px 6px;border-radius:22px;border:1px solid var(--bt-line-2);background:var(--bt-main);box-shadow:0 2px 8px -2px rgba(0,0,0,.06);box-sizing:border-box} --.bt-nc-plus{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2);background:var(--bt-hover);flex:none} --.bt-nc-card textarea{flex:1;resize:none;border:0;outline:none;background:none;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;padding:7px 4px;min-height:34px;max-height:140px;box-sizing:border-box} --.bt-nc-send{width:34px;height:34px;border-radius:50%;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex:none} --.bt-nc-send:disabled{opacity:.3;cursor:default} --.bt-create{flex:1;display:flex;flex-direction:column;min-width:0} ++ +.bt-flybot-section { margin-top: 8px; border-top: 1px solid var(--bt-line, rgba(0,0,0,.08)); padding-top: 6px; } +/* A section whose component renders null leaves the wrapper empty, and an empty wrapper must not draw + the separator: #818's "no border" held for the component but not for the box around it. Keyed on @@ -3123,10 +2769,10 @@ diff --git a/client.js b/client.js +`; + +// src/shared/failure-copy.js -+var providerCode = (text) => { -+ const json = /"code"\s*:\s*"([A-Za-z0-9_.-]+)"/.exec(text); ++var providerCode = (text2) => { ++ const json = /"code"\s*:\s*"([A-Za-z0-9_.-]+)"/.exec(text2); + if (json) return json[1]; -+ const plain = /^([A-Za-z][A-Za-z0-9_.-]{2,40}):\s/.exec(text); ++ const plain = /^([A-Za-z][A-Za-z0-9_.-]{2,40}):\s/.exec(text2); + return plain ? plain[1] : ""; +}; +var KNOWN = { @@ -3137,13 +2783,13 @@ diff --git a/client.js b/client.js + InsufficientQuota: "The account is out of quota.", + RateLimitExceeded: "Too many requests right now. Try again in a moment." +}; -+var fill2 = (text, params) => params === void 0 ? text : String(text).replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match); ++var fill2 = (text2, params) => params === void 0 ? text2 : String(text2).replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match); +function failureSentence(raw, translate = (key) => key) { -+ const text = String(raw ?? "").trim(); -+ if (text === "") return text; -+ const key = KNOWN[providerCode(text)]; -+ if (key === void 0) return text; -+ const feature = /does not support\s+([a-z][a-z0-9 _-]{2,40})/i.exec(text)?.[1]?.trim(); ++ const text2 = String(raw ?? "").trim(); ++ if (text2 === "") return text2; ++ const key = KNOWN[providerCode(text2)]; ++ if (key === void 0) return text2; ++ const feature = /does not support\s+([a-z][a-z0-9 _-]{2,40})/i.exec(text2)?.[1]?.trim(); + if (key !== KNOWN.UnsupportedModel || feature === void 0) return fill2(translate(key), void 0); + const withFeature = "This model does not support {feature}. Pick another model in the Bot's settings."; + return fill2(translate(withFeature, { feature }), { feature }); @@ -3326,236 +2972,112 @@ diff --git a/client.js b/client.js + +// src/client/motion.js +var import_react4 = require("react"); -+var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; -+function springEasing(stiffness, damping) { -+ const w0 = Math.sqrt(stiffness); -+ const zeta = damping / (2 * w0); -+ const at = (t2) => { -+ if (zeta >= 1) return 1 - (1 + w0 * t2) * Math.exp(-w0 * t2); -+ const wd = w0 * Math.sqrt(1 - zeta * zeta); -+ return 1 - Math.exp(-zeta * w0 * t2) * (Math.cos(wd * t2) + zeta * w0 / wd * Math.sin(wd * t2)); -+ }; -+ let end = 0.05; -+ while (end < 2 && Math.abs(1 - at(end)) > 1e-3) end += 0.01; -+ const points = Array.from({ length: 25 }, (_, index) => (index === 24 ? 1 : at(end * index / 24)).toFixed(4)); -+ return { duration: Math.round(end * 1e3), easing: `linear(${points.join(",")})`, stiffness, damping }; -+} -+var SPRING_SHAPE = springEasing(400, 31.2); -+var SPRING_TAP = springEasing(900, 48); -+var SPRING_LIST = springEasing(484, 35.2); -+var SPRING_STRETCH = springEasing(320, 20); -+var SPRING_POP = springEasing(500, 22); -+var SPRING_LEAD = springEasing(784, 48.2); -+var SPRING_TRAIL = springEasing(289, 29.2); -+var QUICK_EDGES = { lead: springEasing(900, 51), trail: springEasing(400, 34) }; -+var spring = (curve, ...properties) => properties.map((property) => `${property} ${curve.duration}ms ${curve.easing}`).join(","); -+function liveSprings(apply2) { -+ const values = /* @__PURE__ */ new Map(); -+ let frame = 0; -+ let last = 0; -+ const read = () => Object.fromEntries([...values].map(([key, value]) => [key, value.x])); -+ const step = (now) => { -+ const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); -+ last = now; -+ const steps = Math.ceil(dt * 240); -+ const h48 = 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) * h48; -+ value.x += value.v * h48; -+ } -+ if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { -+ value.x = value.to; -+ value.v = 0; -+ } else moving = true; -+ } -+ apply2(read()); -+ frame = moving ? requestAnimationFrame(step) : 0; -+ }; -+ return { -+ target: () => values.size ? Object.fromEntries([...values].map(([key, value]) => [key, value.to])) : null, -+ set(targets, curves, jump) { -+ for (const [key, to] of Object.entries(targets)) { -+ const value = values.get(key); -+ const { stiffness: k, damping: c } = curves[key]; -+ if (!value || jump) values.set(key, { x: to, v: 0, to, k, c }); -+ else Object.assign(value, { to, k, c }); -+ } -+ if (jump) { -+ cancelAnimationFrame(frame); -+ frame = 0; -+ apply2(read()); -+ return; -+ } -+ if (!frame) { -+ last = performance.now(); -+ frame = requestAnimationFrame(step); -+ } -+ }, -+ stop: () => cancelAnimationFrame(frame) -+ }; -+} -+var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; -+function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { + var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; + function springEasing(stiffness, damping) { + const w0 = Math.sqrt(stiffness); +@@ -1659,12 +1659,12 @@ + const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); + last = now; + const steps = Math.ceil(dt * 240); +- const h34 = dt / steps; ++ const h49 = 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; + } + if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { + value.x = value.to; +@@ -1699,10 +1699,10 @@ + } + var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; + function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { +- const ownBox = (0, import_react.useRef)(null); + const ownBox = (0, import_react4.useRef)(null); -+ const box = given ?? ownBox; + const box = given ?? ownBox; +- const thumb = (0, import_react.useRef)(null); +- const own = (0, import_react.useRef)(null); + const thumb = (0, import_react4.useRef)(null); + const own = (0, import_react4.useRef)(null); -+ if (own.current === null) { -+ own.current = { -+ key: void 0, -+ node: null, -+ observer: null, -+ springs: liveSprings(({ start, end }) => { -+ const knob = thumb.current; -+ if (!knob) return; -+ knob.style.transform = axis === "x" ? `translateX(${start}px)` : `translateY(${start}px)`; -+ knob.style[axis === "x" ? "width" : "height"] = `${Math.max(0, end - start)}px`; -+ }) -+ }; -+ } -+ const place = (jump) => { -+ const container = box.current; -+ const knob = thumb.current; -+ if (!container || !knob) return; -+ const chosen = container.querySelector(selector); -+ if (!chosen) { -+ knob.dataset.hidden = ""; -+ return; -+ } -+ const start = axis === "x" ? chosen.offsetLeft : chosen.offsetTop; -+ const end = start + (axis === "x" ? chosen.offsetWidth : chosen.offsetHeight); -+ const { springs } = own.current; -+ const before = springs.target(); -+ const hidden = knob.dataset.hidden !== void 0; -+ if (jump && !hidden && before?.start === start && before?.end === end && container.dataset.glide !== void 0) return; -+ delete knob.dataset.hidden; -+ const forward = before === null || start >= before.start; -+ springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); -+ container.dataset.glide = ""; -+ }; + if (own.current === null) { + own.current = { + key: void 0, +@@ -1736,7 +1736,7 @@ + springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); + container.dataset.glide = ""; + }; +- (0, import_react.useLayoutEffect)(() => { + (0, import_react4.useLayoutEffect)(() => { -+ const state = own.current; -+ const node = box.current; -+ if (node === state.node && key === state.key) return; -+ const fresh = node !== state.node; -+ state.key = key; -+ if (fresh) { -+ state.observer?.disconnect(); -+ state.node = node; -+ state.observer = node && typeof ResizeObserver === "function" ? new ResizeObserver(() => place(true)) : null; -+ state.observer?.observe(node); -+ } -+ place(fresh); -+ }); + const state = own.current; + const node = box.current; + if (node === state.node && key === state.key) return; +@@ -1750,7 +1750,7 @@ + } + place(fresh); + }); +- (0, import_react.useEffect)(() => () => { + (0, import_react4.useEffect)(() => () => { -+ own.current.observer?.disconnect(); -+ own.current.springs.stop(); -+ }, []); -+ return { box, thumb }; -+} -+function Segmented({ label, value, className, children }) { -+ const glide = useGlide(value); + own.current.observer?.disconnect(); + own.current.springs.stop(); + }, []); +@@ -1758,16 +1758,16 @@ + } + function Segmented({ label, value, className, children }) { + const glide = useGlide(value); +- return (0, import_react.createElement)( + return (0, import_react4.createElement)( -+ "div", -+ { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, + "div", + { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, +- (0, import_react.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), + (0, import_react4.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -+ children -+ ); -+} -+function useFlip(list, key, selector, id = (node) => node.dataset.flip) { + children + ); + } + function useFlip(list, key, selector, id = (node) => node.dataset.flip) { +- const tops = (0, import_react.useRef)(null); +- (0, import_react.useLayoutEffect)(() => { + const tops = (0, import_react4.useRef)(null); + (0, import_react4.useLayoutEffect)(() => { -+ const container = list.current; -+ if (!container) return; -+ const next = /* @__PURE__ */ new Map(); -+ const moving = tops.current !== null && !reducedMotion(); -+ for (const node of container.querySelectorAll(selector)) { -+ const name = id(node); -+ const top = node.offsetTop; -+ next.set(name, top); -+ if (!moving) continue; -+ const before = tops.current.get(name); -+ if (before === void 0) { -+ node.animate([{ opacity: 0, filter: "blur(4px)" }, { opacity: 1, filter: "blur(0)" }], { duration: 180, easing: "ease-out" }); -+ } else if (Math.abs(before - top) > 0.5) { -+ node.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], { duration: SPRING_LIST.duration, easing: SPRING_LIST.easing }); -+ } -+ } -+ tops.current = next; -+ }, [key]); -+} -+function useMorph(box, key) { + const container = list.current; + if (!container) return; + const next = /* @__PURE__ */ new Map(); +@@ -1788,8 +1788,8 @@ + }, [key]); + } + function useMorph(box, key) { +- const last = (0, import_react.useRef)({ key, height: null }); +- (0, import_react.useLayoutEffect)(() => { + const last = (0, import_react4.useRef)({ key, height: null }); + (0, import_react4.useLayoutEffect)(() => { -+ const node = box.current; -+ const state = last.current; -+ if (!node) { -+ last.current = { key, height: null }; -+ return; -+ } -+ const running = node.getAnimations().filter((animation2) => animation2.id === "bt-morph"); -+ if (key === state.key) { -+ if (running.length === 0) state.height = node.offsetHeight; -+ return; -+ } -+ const from = running.length ? node.offsetHeight : state.height; -+ for (const animation2 of running) animation2.cancel(); -+ const to = node.offsetHeight; -+ last.current = { key, height: to }; -+ if (from === null || Math.abs(from - to) < 1 || reducedMotion()) return; -+ node.dataset.morph = ""; -+ const animation = node.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, id: "bt-morph" }); -+ animation.onfinish = () => { -+ delete node.dataset.morph; -+ }; -+ animation.oncancel = () => { -+ if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph; -+ }; -+ }); -+} -+function useLinger(value, ms) { + const node = box.current; + const state = last.current; + if (!node) { +@@ -1817,8 +1817,8 @@ + }); + } + function useLinger(value, ms) { +- const [kept, setKept] = (0, import_react.useState)(value); +- (0, import_react.useEffect)(() => { + const [kept, setKept] = (0, import_react4.useState)(value); + (0, import_react4.useEffect)(() => { -+ if (value) { -+ setKept(value); -+ return void 0; -+ } -+ const timer = setTimeout(() => setKept(value), reducedMotion() ? 0 : ms); -+ return () => clearTimeout(timer); -+ }, [value]); -+ return [value || kept, !value && Boolean(kept)]; -+} -+ -+// src/client/styles.js -+var SHELL_CSS = ` -+body{--bt-ink:#141414;--bt-ink-2:rgba(20,20,20,.6);--bt-ink-3:rgba(20,20,20,.36);--bt-bubble:#eeeeee;--bt-sidebar:#f7f7f7;--bt-main:#fcfcfc;--bt-line:rgba(20,20,20,.08);--bt-line-2:rgba(20,20,20,.15);--bt-line-weak:rgba(20,20,20,.1);--bt-hover:rgba(119,119,119,.09);--bt-active:rgba(119,119,119,.17);--bt-card:#ffffff;--bt-accent:#4d6bfe;--bt-green:#00c972;--bt-star:#ff9800;--bt-user:#070707;--bt-user-ink:#fcfcfc;--bt-tag-bg:rgba(20,20,20,.06);--bt-tag-ink:rgba(20,20,20,.56)} -+body[data-ds-dark-theme]{--bt-ink:#f2f2f2;--bt-ink-2:rgba(242,242,242,.6);--bt-ink-3:rgba(242,242,242,.36);--bt-bubble:#262626;--bt-sidebar:#1b1b1b;--bt-main:#141414;--bt-line:rgba(255,255,255,.08);--bt-line-2:rgba(255,255,255,.15);--bt-line-weak:rgba(255,255,255,.1);--bt-hover:rgba(160,160,160,.12);--bt-active:rgba(160,160,160,.2);--bt-card:#1f1f1f;--bt-user:#f2f2f2;--bt-user-ink:#141414;--bt-tag-bg:rgba(255,255,255,.09);--bt-tag-ink:rgba(242,242,242,.62)} -+${Object.entries(INKS).map(([name, [light]]) => `body{--bt-ink-${name}:${light}}`).join("")} -+${Object.entries(INKS).map(([name, [, dark]]) => `body[data-ds-dark-theme]{--bt-ink-${name}:${dark}}`).join("")} -+.bt-mark{display:inline-block;flex:none;position:relative;line-height:0} -+.bt-mark svg{width:100%;height:100%;overflow:visible} -+.bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)} + if (value) { + setKept(value); + return void 0; +@@ -1838,6 +1838,7 @@ + .bt-mark{display:inline-block;flex:none;position:relative;line-height:0} + .bt-mark svg{width:100%;height:100%;overflow:visible} + .bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)} +.bt-mark-emoji{width:100%;height:100%;border-radius:50%;display:flex;align-items:center;justify-content:center;line-height:1;user-select:none} -+.bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box} -+.bt-mark .bt-b{transform-origin:50% 100%} -+.bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center} -+/* Motion levels (Settings → Bots → Animation) set body[data-bt-motion]: quiet moves a -+ mark only for its state; normal, the default, adds petting, pokes and eyes that follow -+ the pointer; lively adds blinks and fidgets at rest and a question riding on the head. */ -+body[data-bt-motion=lively] .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{animation:bt-blink-idle 5s linear infinite;animation-delay:var(--bt-delay,0s)} -+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle]:is([data-fidget=hop],[data-fidget=sway],[data-fidget=stretch]) :is(.bt-b,.bt-mark-img){animation:var(--bt-fidget) 9s ease-in-out var(--bt-fidget-delay,0s) infinite} -+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=hop]{--bt-fidget:bt-fidget-hop} -+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=sway]{--bt-fidget:bt-fidget-sway} -+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=stretch]{--bt-fidget:bt-fidget-stretch} -+body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=look] .bt-eyes{animation:bt-fidget-look 9s ease-in-out var(--bt-fidget-delay,0s) infinite} -+@keyframes bt-fidget-hop{0%,8%,100%{transform:none}3%{transform:translateY(-10%) scale(.97,1.04)}5.5%{transform:translateY(0) scale(1.05,.95)}} -+@keyframes bt-fidget-sway{0%,10%,100%{transform:none}2.5%{transform:rotate(-8deg)}5%{transform:rotate(7deg)}7.5%{transform:rotate(-3deg)}} -+@keyframes bt-fidget-stretch{0%,9%,100%{transform:none}4%{transform:scale(.94,1.08)}7%{transform:scale(1.03,.97)}} -+@keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}} -+body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite} -+body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none} + .bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box} + .bt-mark .bt-b{transform-origin:50% 100%} + .bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center} +@@ -1856,6 +1856,14 @@ + @keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}} + body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite} + body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none} +.bt-tree-item{position:relative;display:flex;align-items:center;min-width:0} +.bt-tree-item>.bt-row{flex:1 1 auto;min-width:0} +.bt-tree-toggle{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:8px;background:transparent;color:var(--bt-ink-3);font-size:16px;line-height:1;cursor:pointer} @@ -3564,83 +3086,14 @@ diff --git a/client.js b/client.js +.bt-tree-chev{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;font-size:16px;line-height:1;transition:transform .12s ease} +.bt-tree-toggle[aria-expanded=false] .bt-tree-chev{transform:rotate(-90deg)} +.bt-tree-leaf{cursor:default} -+@keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}} -+/* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its -+ size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes -+ closed in a happy arc, and springs back when the pointer leaves. */ -+:is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster)::before{content:'';position:absolute;inset:-20%;border-radius:50%} -+.bt-mark[data-live] .bt-b,.bt-mark[data-live] .bt-mark-img{transition:rotate .5s cubic-bezier(.34,1.56,.64,1),translate .5s cubic-bezier(.34,1.56,.64,1),scale .3s ease-out} -+.bt-mark[data-live] .bt-eye{transition:scale .14s ease-in} -+.bt-mark[data-live] .bt-eye-happy{transition:opacity .12s ease-out} -+body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]):not(.bt-mark-poke) :is(.bt-b,.bt-mark-img){rotate:8deg;translate:5% -1%;animation:bt-nuzzle .78s ease-in-out .32s infinite} -+body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye{scale:1 0;animation:none} -+body:not([data-bt-motion=quiet]) :is(.bt-row,.bt-tile)>:is(.bt-mark,.bt-cluster):hover .bt-mark[data-live]:is([data-state=idle],[data-state=alert]) .bt-eye-happy{opacity:1;transition-delay:.1s} -+@keyframes bt-nuzzle{50%{rotate:14deg;translate:8% -3%;scale:1.03 .97}} -+.bt-mark[data-gaze] .bt-b{translate:calc(var(--bt-gx,0) * 5%) calc(var(--bt-gy,0) * 4%);transition:translate .18s ease-out} -+.bt-mark[data-gaze] .bt-gaze{transform:translate(calc(var(--bt-gx,0) * 12%),calc(var(--bt-gy,0) * 11%));transition:transform .18s ease-out} -+body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-b{translate:none} -+body[data-bt-motion=quiet] .bt-mark[data-gaze] .bt-gaze{transform:none} -+.bt-mark[data-state=thinking] .bt-b{animation:bt-ponder 2.2s ease-in-out infinite} -+.bt-mark[data-state=thinking] .bt-eyes{animation:bt-squint 2.2s ease-in-out infinite} -+.bt-mark[data-state=searching] .bt-b{animation:bt-seek 1s ease-in-out infinite} -+.bt-mark[data-state=searching] .bt-eyes{animation:bt-scan 1.6s ease-in-out infinite} -+.bt-mark[data-state=working] .bt-b{animation:bt-bob 1.6s ease-in-out infinite} -+.bt-mark[data-state=working]:is([data-shape=star],[data-shape=hex],[data-shape=plus],[data-shape=gem],[data-shape=magnet],[data-shape=ring]) .bt-b{animation:bt-wiggle 1.4s ease-in-out infinite;transform-origin:50% 50%} -+.bt-mark[data-state=working]:is([data-shape=gumdrop],[data-shape=peanut],[data-shape=cushion],[data-shape=squircle],[data-shape=moon],[data-shape=wave]) .bt-b{animation:bt-squash 1.2s ease-in-out infinite} -+.bt-mark[data-state=working] .bt-eye{animation:bt-blink 2.4s ease-in-out infinite} -+.bt-mark[data-state=sending] .bt-b{animation:bt-dash 1.1s cubic-bezier(.4,0,.2,1) infinite} -+.bt-mark[data-state=orbit] .bt-b{animation:bt-sway 2.6s ease-in-out infinite;transform-origin:50% 50%} -+.bt-mark[data-state=orbit] .bt-eyes{animation:bt-roll 2.6s linear infinite} -+/* A question for you hops twice when it arrives and the badge keeps it in view after; -+ a lively mark keeps hopping. */ -+.bt-mark[data-state=alert] .bt-b{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2} -+.bt-mark[data-shape=image]:not([data-state=idle]) .bt-mark-img{animation:bt-bob 1.6s ease-in-out infinite} -+.bt-mark[data-shape=image][data-state=alert] .bt-mark-img{animation:bt-hop 1.3s cubic-bezier(.3,.7,.4,1) 2} -+body[data-bt-motion=lively] .bt-mark[data-state=alert] :is(.bt-b,.bt-mark-img){animation-iteration-count:infinite} -+.bt-mark[data-state=done] .bt-b{animation:bt-done 1.8s linear 1} -+.bt-mark[data-shape=image][data-state=done] .bt-mark-img{animation:bt-done 1.8s linear 1;transform-origin:50% 100%} -+@keyframes bt-done{0%{transform:none;animation-timing-function:cubic-bezier(.4,0,.2,1)}13.3%{transform:scale(1.12,.88);animation-timing-function:cubic-bezier(.4,0,.2,1)}25.6%{transform:scale(.93,1.09);animation-timing-function:cubic-bezier(.4,0,.2,1)}37.8%{transform:scale(1.04,.97);animation-timing-function:cubic-bezier(.4,0,.2,1)}51.1%,100%{transform:none}} -+.bt-mark-poke .bt-b{animation:bt-poke .8s cubic-bezier(.3,.7,.4,1) 1!important;transform-origin:50% 60%!important} -+.bt-mark-poke .bt-mark-img{animation:bt-poke-img .6s ease-out 1!important} -+.bt-mark[data-poke]{cursor:pointer} -+body[data-bt-motion=quiet] .bt-mark-poke :is(.bt-b,.bt-mark-img){animation:none!important} -+body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} -+@keyframes bt-bob{0%,100%{transform:translateY(0) rotate(0)}30%{transform:translateY(-4%) rotate(-5deg)}65%{transform:translateY(0) rotate(4deg)}} -+@keyframes bt-wiggle{0%,100%{transform:rotate(0) scale(1)}25%{transform:rotate(-12deg) scale(1.04)}75%{transform:rotate(12deg) scale(1.04)}} -+@keyframes bt-squash{0%,100%{transform:scale(1,1)}30%{transform:scale(1.08,.9)}55%{transform:translateY(-4%) scale(.94,1.08)}80%{transform:scale(1.02,.98)}} -+@keyframes bt-ponder{0%,100%{transform:translateY(0) rotate(4deg)}50%{transform:translateY(-3%) rotate(7deg)}} -+@keyframes bt-squint{0%,100%{transform:translate(0,-4%) scaleY(.7)}50%{transform:translate(5%,-5%) scaleY(.78)}} -+@keyframes bt-seek{0%,100%{transform:translateY(0) rotate(-4deg)}50%{transform:translateY(-5%) rotate(-1deg)}} -+@keyframes bt-scan{0%,100%{transform:translateX(-12%)}50%{transform:translateX(12%)}} -+@keyframes bt-dash{0%,100%{transform:translateX(0) rotate(0)}35%{transform:translateX(9%) rotate(7deg)}55%{transform:translateX(-2%) rotate(-3deg)}} -+@keyframes bt-sway{0%,100%{transform:rotate(-10deg)}50%{transform:rotate(10deg)}} -+@keyframes bt-roll{0%,100%{transform:translate(12%,0)}25%{transform:translate(0,9%)}50%{transform:translate(-12%,0)}75%{transform:translate(0,-9%)}} -+@keyframes bt-hop{0%,55%,100%{transform:translateY(0) scale(1,1)}20%{transform:translateY(-14%) scale(.96,1.05)}40%{transform:translateY(0) scale(1.06,.94)}} -+@keyframes bt-poke{0%{transform:none}55%{transform:translateY(-16%) rotate(360deg)}78%{transform:translateY(0) scale(1.08,.92) rotate(360deg)}100%{transform:rotate(360deg)}} -+@keyframes bt-poke-img{0%,100%{transform:scale(1)}40%{transform:scale(1.14) rotate(-6deg)}} -+/* Idle clip: two .26s blinks (lid to 3%, then a 6% overshoot) at 1.48s and 3.46s. */ -+@keyframes bt-blink-idle{0%,29.6%,35%,69.2%,74.6%,100%{transform:scaleY(1)}32.2%,71.8%{transform:scaleY(.03)}33.8%,73.4%{transform:scaleY(1.06)}} -+@keyframes bt-blink{0%,42%,50%,100%{transform:scaleY(1)}46%{transform:scaleY(.03)}48%{transform:scaleY(1.06)}} -+@media (prefers-reduced-motion:reduce){.bt-mark *{animation:none!important;transition:none!important}} -+.bt-badge{position:absolute;right:0;bottom:0;width:12px;height:12px;border-radius:50%;corner-shape:round;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-sidebar)} -+.bt-badge-admin{background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar))} -+.bt-badge-admin svg{width:7px;height:7px} -+.bt-badge-main{right:calc(var(--bt-size,36px) * -.07);bottom:calc(var(--bt-size,36px) * -.07);width:clamp(14px,calc(var(--bt-size,36px) * .36),22px);height:clamp(14px,calc(var(--bt-size,36px) * .36),22px);border-radius:0;box-shadow:none} -+.bt-badge-main svg{width:100%;height:100%;overflow:visible} -+.bt-badge-main path{fill:var(--bt-star);stroke:var(--bt-sidebar);stroke-width:2.6;stroke-linejoin:round;paint-order:stroke} -+.bt-badge-alert{background:color-mix(in srgb,var(--bt-accent) 78%,var(--bt-sidebar));color:var(--bt-accent-ink,#fff);font-size:8.5px;line-height:12px;font-weight:600;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} -+.bt-badge-working{background:color-mix(in srgb,var(--bt-green) 82%,var(--bt-sidebar));width:8px;height:8px;right:1px;bottom:1px} -+.bt-cluster{position:relative;display:inline-block;flex:none;line-height:0} -+.bt-cluster>*{position:absolute} -+.bt-cluster-more,.bt-cluster-you{display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:var(--bt-active);color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;font-weight:500;line-height:1;font-variant-numeric:tabular-nums} -+.bt-cluster-you{background:color-mix(in srgb,var(--bt-ink) 12%,transparent)} -+.bt-stack{display:inline-flex;align-items:center;flex:none;line-height:0} -+.bt-stack>*+*{margin-left:calc(var(--bt-stack-size) * -.3)} -+.bt-stack-more{font-size:11px;line-height:16px;color:var(--bt-ink-3);margin-left:3px!important} -+ -+.bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} -+.bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0} -+.bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap} + @keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}} + /* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its + size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes +@@ -1933,15 +1933,23 @@ + .bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} + .bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0} + .bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap} +-.bt-brand-whale{display:inline-flex;color:var(--bt-accent)} +.bt-brand-mark{display:inline-flex;line-height:0} +.bt-flybot-mark{display:block;flex:none} +.bt-flybot-mark .bt-flybot-wing{fill:#8FB2FF} @@ -3649,195 +3102,84 @@ diff --git a/client.js b/client.js +[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-wing{fill:#A9C2FF} +[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-body{fill:#6E87FF} +[data-ds-dark-theme] .bt-flybot-mark .bt-flybot-eye{fill:#0F1530} -+.bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease} -+.bt-round:hover{background:var(--bt-hover)} -+.bt-round:active{transform:scale(.94)} -+.bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto} -+.bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px} + .bt-round{width:36px;height:36px;border-radius:50%;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background-color .12s ease,transform .12s ease} + .bt-round:hover{background:var(--bt-hover)} + .bt-round:active{transform:scale(.94)} + .bt-list{display:flex;flex-direction:column;gap:3px;overflow-y:auto;min-height:0;flex:1 1 auto} + .bt-tile-wrap{display:flex;justify-content:center;padding:20px 0 15px} +-.bt-tile{display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease} +.bt-tile{position:relative;display:flex;flex-direction:column;align-items:center;gap:4px;width:95px;padding:6px 8px 4px;border-radius:12px;border:0;background:transparent;color:var(--bt-ink);cursor:pointer;font-family:inherit;transition:background-color .12s ease} -+.bt-tile:hover{background:var(--bt-hover)} -+.bt-tile[aria-current=page]{background:var(--bt-active)} + .bt-tile:hover{background:var(--bt-hover)} + .bt-tile[aria-current=page]{background:var(--bt-active)} +.bt-tile-unread{position:absolute;top:8px;right:12px} -+.bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis} -+.bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease} -+.bt-row:hover{background:var(--bt-hover)} -+.bt-row[aria-current=page]{background:var(--bt-active)} -+.bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} -+.bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} -+.bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis} + .bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease} +@@ -1950,27 +1950,32 @@ + .bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} + .bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} + .bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bt-row-title.bt-row-title-face{white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;line-clamp:3;overflow:hidden} +.bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2)} -+.bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -+.bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -+.bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)} -+.bt-unread-wait{background:var(--bt-star)} + .bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)} + .bt-unread-wait{background:var(--bt-star)} +.bt-shared-from,.bt-shared-wait{display:flex;flex-direction:column;gap:6px;padding:8px 0} +.bt-shared-wait .bt-set-row{min-width:0} +.bt-shared-wait .bt-set-copy{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block} -+.bt-rail{align-items:center;padding:0 0 8px} -+.bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px} -+.bt-side-foot{display:flex;padding-top:8px} -+.bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease} -+.bt-connect:hover{background:var(--bt-hover)} -+.bt-connect svg{flex:none;width:15px;height:15px} + .bt-rail{align-items:center;padding:0 0 8px} + .bt-rail .bt-row{justify-content:center;padding:6px 0;min-height:44px} + .bt-side-foot{display:flex;padding-top:8px} + .bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease} + .bt-connect:hover{background:var(--bt-hover)} + .bt-connect svg{flex:none;width:15px;height:15px} +-.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0} +-.bt-connect-warn{flex:none;width:16px;height:16px;border-radius:50%;background:#E5484D;color:#fff;font-size:11px;line-height:16px;font-weight:700;display:inline-flex;align-items:center;justify-content:center} +-.bt-connect-tip{position:fixed;box-sizing:border-box;width:max-content;max-width:260px;padding:8px 12px;border-radius:10px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 8px 24px -4px rgba(0,0,0,.14);color:var(--bt-ink);font-size:12px;line-height:17px;white-space:normal;text-align:center;z-index:70;transform-origin:bottom left;animation:bt-tip-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +.bt-connect-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0}ms ${SPRING_SHAPE.easing}} -+@keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}} + @keyframes bt-tip-in{from{opacity:0;transform:scale(.96);filter:blur(4px)}} +-@media (prefers-reduced-motion:reduce){.bt-connect-tip{animation:none}} +@media (prefers-reduced-motion:reduce){} -+ -+.bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none} -+.bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}} -+.bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)} -+.bt-pill:active{transform:scale(.965)} -+.bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500} + + .bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none} + .bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}} + .bt-pill:hover{background:color-mix(in srgb,var(--bt-main),#777 9%)} + .bt-pill:active{transform:scale(.965)} + .bt-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500} +.bt-pill:has(.bt-pill-name-face){height:auto;min-height:40px;align-items:flex-start} +.bt-pill-name.bt-pill-name-face{min-width:0;white-space:normal;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden} -+.bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}} -+.bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}} -+.bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}} -+.bt-pill-mem-in::before{content:"";flex:none;width:1px;height:14px;margin-right:4px;background:var(--bt-line-2)} -+.bt-pill[data-memory=updated] .bt-pill-mem-in{opacity:1;filter:none;transform:none;transition-delay:70ms} -+.bt-pill-mem-ico{flex:none;display:inline-flex;width:16px;height:16px;color:var(--bt-accent)} -+.bt-pill-mem-ico svg{width:16px;height:16px} -+.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:bt-mem-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 90ms both} -+@keyframes bt-mem-pop{from{transform:scale(.4) rotate(-14deg)}to{transform:none}} -+.bt-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0} -+@media (prefers-reduced-motion:reduce){.bt-pill-mem,.bt-pill-mem-in{transition:none!important}.bt-pill-mem-in{filter:none;transform:none}.bt-pill[data-memory=updated] .bt-pill-mem-ico{animation:none}} -+.bt-swap{color:var(--bt-ink-3);font-size:12px} -+.bt-pill-go{flex:none;display:inline-flex;align-items:center;justify-content:flex-end;width:0;margin-left:-8px;overflow:hidden;opacity:0;filter:blur(6px);transform:translateX(-6px);color:var(--bt-ink-2);transition:width ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},margin-left ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},opacity .18s ease,filter .18s ease} -+.bt-pill-go svg{flex:none;transition:transform ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} -+.bt-pill:hover .bt-pill-go,.bt-pill:focus-visible .bt-pill-go,.bt-pill[aria-expanded=true] .bt-pill-go{width:18px;margin-left:0;opacity:1;filter:none;transform:none} -+.bt-pill[aria-expanded=true] .bt-pill-go svg{transform:rotate(180deg)} -+.bt-pill[data-memory=updated]:is(:hover,:focus-visible,[aria-expanded=true]) .bt-pill-go{width:0;margin-left:-8px;opacity:0;filter:blur(6px);transform:translateX(-6px)} -+@media (prefers-reduced-motion:reduce){.bt-pill,.bt-pill-go,.bt-pill-go svg{transition:none!important}.bt-pill-go{filter:none}.bt-pill:active{transform:none}} -+ -+.bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0} -+.bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere} + .bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}} + .bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}} + .bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}} +@@ -1992,6 +1992,7 @@ + + .bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0} + .bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere} +.bt-event.bt-not-sent{gap:6px;color:var(--bt-ink-3)} -+.bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px} -+.bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px} -+.bt-bubble [class*="_markdown"]>*+*{margin-top:10px} -+.bt-bubble [class*="_markdown"]{color:inherit!important;font-size:inherit!important;line-height:inherit!important} -+.bt-bubble p{margin:0} -+.bt-bubble p + p{margin-top:6px} -+.bt-bubble ul,.bt-bubble ol{margin:4px 0;padding-left:20px} -+.bt-bubble a[href^="${MENTION_ORIGIN}"]{text-decoration:none;font-weight:400;cursor:pointer;white-space:nowrap} -+.bt-bubble a[href^="${MENTION_ORIGIN}"]::before{content:"";display:inline-block;width:16px;height:16px;margin-right:3px;vertical-align:-3px;background:currentColor;-webkit-mask:${DEFAULT_MASK} center/contain no-repeat;mask:${DEFAULT_MASK} center/contain no-repeat} -+.bt-group-msg{display:flex;flex-direction:column;gap:4px;margin:16px 0 0} -+.bt-group-msg:first-child{margin-top:0} -+.bt-lead{flex:none;display:flex;margin-bottom:1px} -+button.bt-lead{border:0;background:none;padding:0;cursor:pointer} -+/* Rows without the avatar start where the bubble beside it starts: 22px avatar + 8px gap. */ -+.bt-astack[data-lead]>.bt-msg:not(:last-child)>.bt-msg-line,.bt-astack[data-lead] .bt-reacts{margin-left:30px} -+.bt-group-name{font-size:12px;line-height:16px;font-weight:500;margin-left:42px} -+.bt-x{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;width:24px;height:24px;border-radius:6px;font-size:16px;line-height:24px;padding:0;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}} -+.bt-x:hover{background:var(--bt-hover);color:var(--bt-ink)} -+ -+.bt-q{margin:0 auto 8px;width:100%;max-width:var(--dsh-chat-content-width,748px);display:flex;flex-direction:column;gap:12px;--bt-q-edge:color-mix(in srgb,var(--bt-ink) 10%,transparent);--bt-q-rule:color-mix(in srgb,var(--bt-ink) 8%,transparent);--bt-q-rule-2:color-mix(in srgb,var(--bt-ink) 5%,transparent);background:var(--bt-card);box-shadow:0 1px 3px rgba(20,20,20,.04),inset 0 0 0 1px var(--bt-q-edge);border-radius:16px;padding:14px 16px;box-sizing:border-box;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} -+body[data-ds-dark-theme] .bt-q{--bt-q-edge:rgba(255,255,255,.16);--bt-q-rule:rgba(255,255,255,.12);--bt-q-rule-2:rgba(255,255,255,.08)} -+@keyframes bt-q-in{from{transform:translateY(10px) scale(.97)}} -+.bt-q-settled{animation:none} -+.bt-q-head{display:flex;align-items:flex-start;gap:8px;min-width:0} -+.bt-q-text{flex:1 1 0;min-width:0;display:flex;flex-direction:column} -+.bt-q-title{margin:0;font-size:15px;line-height:22px;font-weight:600;overflow-wrap:anywhere} -+.bt-q-detail{font-size:13px;line-height:18px;color:var(--bt-ink-2)} -+.bt-q-x{flex:none;width:20px;height:20px;border:0;padding:0;border-radius:6px;background:none;color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background-color .12s ease,color .12s ease,${spring(SPRING_TAP, "scale")}} -+.bt-q-x svg{width:14px;height:14px} -+.bt-q-x:hover{background:var(--bt-hover);color:var(--bt-ink)} -+.bt-q-x:focus-visible,.bt-q-opt:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} -+.bt-q-list{display:flex;flex-direction:column;gap:1px;min-width:0;box-sizing:border-box;margin:0 -8px} -+.bt-q-opt{position:relative;display:flex;align-items:flex-start;gap:16px;width:100%;min-width:0;box-sizing:border-box;margin:0;border:0;border-radius:12px;background:transparent;padding:10px 8px;font:inherit;color:var(--bt-ink);text-align:start;cursor:pointer;transition:background-color .12s ease} -+.bt-q:not(.bt-q-raised)>.bt-q-head:has(+.bt-q-list){padding-bottom:12px;border-bottom:1px solid var(--bt-q-rule)} -+.bt-q:not(.bt-q-raised)>.bt-q-head+.bt-q-list{margin-top:-6px} -+.bt-q-opt+.bt-q-opt::before{content:"";position:absolute;left:calc(var(--bt-key,24px) + 24px);right:8px;top:-1px;height:1px;background:var(--bt-q-rule-2);transition:opacity .12s ease;pointer-events:none} -+button.bt-q-opt:hover::before,button.bt-q-opt:hover+.bt-q-opt::before,.bt-q-opt[aria-pressed=true]::before,.bt-q-opt[aria-pressed=true]+.bt-q-opt::before{opacity:0} -+.bt-q-raised .bt-q-opt::before{display:none} -+button.bt-q-opt:hover{background:var(--bt-hover)} -+button.bt-q-opt:active{background:var(--bt-active)} -+.bt-q-opt[aria-pressed=true]{background:var(--bt-active)} -+.bt-q:not(.bt-q-raised) :is(button.bt-q-opt:hover,.bt-q-opt[aria-pressed=true]) .bt-key{background:var(--bt-card);color:var(--bt-ink)} -+.bt-key{flex:none;box-sizing:border-box;width:var(--bt-key,24px);height:var(--bt-key,24px);border-radius:8px;background:var(--bt-hover);color:var(--bt-ink-2);font-size:12px;line-height:var(--bt-key,24px);font-weight:500;text-align:center;font-variant-numeric:tabular-nums;transition:background-color .12s ease,color .12s ease} -+.bt-q-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;padding-top:2px} -+/* With a description the key spans the two text lines less 2px above and below. */ -+.bt-q-opt:has(.bt-q-desc){--bt-key:34px} -+.bt-q-opt:has(.bt-q-desc) .bt-key{margin-top:2px;border-radius:10px;font-size:13px} -+.bt-q-opt:has(.bt-q-desc) .bt-q-body{padding-top:0} -+.bt-q-label{font-size:14px;line-height:20px;font-weight:500;white-space:normal;overflow-wrap:anywhere} -+.bt-q-desc{font-size:12px;line-height:17px;color:var(--bt-ink-2);overflow-wrap:anywhere} -+.bt-q-check{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2);animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} -+.bt-q-settled .bt-q-check{animation:none} -+.bt-q-check svg{width:16px;height:16px} -+.bt-q-settled .bt-key{opacity:.4} -+.bt-q-settled .bt-q-label{color:color-mix(in srgb,var(--bt-ink) 45%,transparent)} -+.bt-q-dismissed .bt-q-title{color:var(--bt-ink-2)} -+.bt-q.bt-q-settled{gap:6px;padding:12px 16px} -+.bt-q-settled .bt-q-title{font-size:14px;line-height:20px;font-weight:500} -+.bt-q.bt-q-settled>.bt-q-head:has(+.bt-q-list){padding-bottom:0;border-bottom:0} -+.bt-q.bt-q-settled>.bt-q-head+.bt-q-list{margin-top:0} -+.bt-q-settled .bt-q-opt{padding:4px 8px} -+.bt-q-badge{flex:none;height:20px;padding:0 7px;border-radius:999px;border:1px dotted var(--bt-line-2);color:var(--bt-ink-2);font-size:12px;line-height:18px;box-sizing:border-box} -+.bt-q-custom{display:flex;align-items:flex-start;gap:8px;width:100%;min-width:0} -+.bt-q-field{flex:1 1 0;min-width:0;display:flex;box-sizing:border-box;min-height:32px;padding:5px 11px;border-radius:8px;border:.5px solid var(--bt-line-2);background:none;cursor:text;transition:border-color .12s ease} -+.bt-q-field:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)} -+.bt-q-input{flex:1;width:100%;min-width:0;max-height:120px;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;resize:none;overflow-y:hidden;white-space:pre-wrap;overflow-wrap:anywhere} -+.bt-q-input:not(:placeholder-shown){field-sizing:content} -+.bt-q-input::placeholder{color:var(--bt-ink-3)} -+.bt-q-submit,.bt-q-btn{flex:none;height:36px;padding:0 14px;border:0;border-radius:999px;font:inherit;font-size:14px;font-weight:500;cursor:pointer;transition:opacity .12s ease,background-color .12s ease,${spring(SPRING_TAP, "scale")}} -+.bt-q-submit{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing} backwards,bt-veil-in .16s ease-out backwards} -+@keyframes bt-q-submit-in{from{transform:translateX(8px) scale(.9)}} -+.bt-q-submit:hover:not(:disabled){opacity:.86} -+.bt-q-submit:disabled{opacity:.4;cursor:default} -+.bt-q-btn{background:var(--bt-hover);color:var(--bt-ink)} -+.bt-q-btn:hover{background:var(--bt-active)} -+.bt-q-foot{display:flex;justify-content:flex-end;gap:8px} -+.bt-q-raised{width:calc(var(--dsh-chat-content-width,748px) - 32px);max-width:calc(100% - 32px);gap:0;padding:0;box-shadow:0 2px 8px rgba(20,20,20,.035),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 5%,transparent)} -+.bt-q-raised .bt-q-head{padding:14px 16px 4px 20px} -+.bt-q-raised .bt-q-head:last-child{padding-bottom:16px} -+.bt-q-raised .bt-q-list{gap:1px;margin:0;padding:8px} -+.bt-q-raised .bt-q-opt{gap:4px;padding:7px;border-radius:16px;color:var(--bt-ink-2)} -+.bt-q-raised button.bt-q-opt:hover{color:var(--bt-ink)} -+.bt-q-raised .bt-q-opt[aria-pressed=true]{background:transparent;color:var(--bt-ink)} -+.bt-q-raised .bt-q-opt[aria-pressed=true]:hover{background:var(--bt-hover)} -+.bt-q-raised .bt-q-opt[aria-pressed=true] .bt-key,.bt-q-ownrow:focus-within .bt-key,.bt-q-ownrow[data-filled] .bt-key{background:var(--bt-user);color:var(--bt-user-ink)} -+.bt-q-raised .bt-q-body{padding:2px 6px 0} -+.bt-q-raised .bt-q-desc{color:color-mix(in srgb,var(--bt-ink) 61%,transparent)} -+.bt-q-ownrow{cursor:text} -+.bt-q-ownrow .bt-q-input{color:var(--bt-ink)} -+.bt-q-raised .bt-q-foot{padding:0 8px 8px} -+@media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}} -+ -+.bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out} + .bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px} + .bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px} + .bt-bubble [class*="_markdown"]>*+*{margin-top:10px} +@@ -2085,6 +2085,7 @@ + @media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}} + + .bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out} +.bt-pane[data-menu]{background:transparent}.bt-pane[data-menu] .bt-newchat{flex:none;align-self:flex-start;width:min(280px,calc(100% - 16px));max-height:calc(100% - 16px);margin:8px;overflow:auto;background:var(--bt-card);border:1px solid var(--bt-line-2);border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,.14),0 2px 6px rgba(0,0,0,.06)}.bt-newchat[data-menu] .bt-drop{position:relative;top:0;left:0;width:auto;max-height:none;margin:0;box-shadow:none;border:0;max-width:none} -+.bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box} -+.bt-to-label{color:var(--bt-ink-2)} -+.bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px} -+.bt-token{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 6px 0 4px;border-radius:999px;background:var(--bt-hover);font-size:13px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} -+.bt-token button{border:0;background:none;color:var(--bt-ink-2);cursor:pointer;padding:0 2px} -+.bt-admin-star{flex:none;display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px;border-radius:50%;corner-shape:round;background:color-mix(in srgb,var(--bt-star) 82%,var(--bt-sidebar));vertical-align:-1px} -+.bt-to-star{display:inline-flex;margin-right:-2px;animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}} -+.bt-to .bt-to-create{flex:none;height:28px;padding:0 12px;font-size:13px;animation:bt-q-submit-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} -+.bt-drop{position:absolute;top:48px;left:32px;width:min(550px,calc(100% - 64px));display:flex;flex-direction:column;padding:6px;max-height:min(420px,60vh);overflow-y:auto;background:var(--bt-card);border:1px solid var(--bt-line-2);border-radius:12px;box-shadow:0 6px 20px -4px rgba(0,0,0,.12);box-sizing:border-box;z-index:1;transform-origin:24px 0;animation:bt-pop-drop ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .14s ease-out} -+.bt-option{display:flex;align-items:center;gap:10px;border:0;background:none;border-radius:8px;padding:7px 8px;min-height:36px;box-sizing:border-box;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;text-align:left;width:100%} -+.bt-option[aria-selected=true]{background:var(--bt-active)} -+.bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)} -+.bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box} + .bt-to-label{color:var(--bt-ink-2)} + .bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px} +@@ -2098,7 +2098,9 @@ + .bt-option[aria-selected=true]{background:var(--bt-active)} + .bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)} + .bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +-.bt-option-hint{font-size:11px;color:var(--bt-ink-2);flex:none;animation:bt-veil-in .16s ease-out} +.bt-option-hint{font-size:11px;color:var(--bt-ink-2);flex:0 1 auto;min-width:0;max-width:50%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;animation:bt-veil-in .16s ease-out} +.bt-option-copy{flex:1;min-width:0;display:flex;flex-direction:column} +.bt-option-sub{font-size:11px;line-height:15px;color:var(--bt-ink-2);white-space:normal;overflow-wrap:anywhere} -+.bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none} -+.bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)} -+.bt-nc-spacer{flex:1} -+.bt-nc-compose{padding:0 16px 14px;display:flex;justify-content:center} -+.bt-nc-card{display:flex;align-items:flex-end;gap:6px;width:100%;max-width:var(--dsh-chat-content-width,748px);min-height:44px;padding:4px 6px;border-radius:22px;border:1px solid var(--bt-line-2);background:var(--bt-main);box-shadow:0 2px 8px -2px rgba(0,0,0,.06);box-sizing:border-box} -+.bt-nc-plus{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2);background:var(--bt-hover);flex:none} -+.bt-nc-card textarea{flex:1;resize:none;border:0;outline:none;background:none;color:var(--bt-ink);font:inherit;font-size:14px;line-height:20px;padding:7px 4px;min-height:34px;max-height:140px;box-sizing:border-box} -+.bt-nc-send{width:34px;height:34px;border-radius:50%;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex:none} -+.bt-nc-send:disabled{opacity:.3;cursor:default} -+.bt-create{flex:1;display:flex;flex-direction:column;min-width:0} - .bt-create-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);box-sizing:border-box} - .bt-create-gap{width:28px} - .bt-create-title{font-size:14px;line-height:20px;font-weight:500} -@@ -2127,6 +2127,10 @@ button.bt-q-opt:active{background:var(--bt-active)} + .bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none} + .bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)} + .bt-nc-spacer{flex:1} +@@ -2127,6 +2127,10 @@ .bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer} .bt-send:disabled{opacity:.35;cursor:default} .bt-error{padding:0 14px 10px;color:#e02135;font-size:12px} @@ -3848,7 +3190,7 @@ diff --git a/client.js b/client.js .bt-pane{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;z-index:45;pointer-events:auto;background:var(--bt-main);color:var(--bt-ink);display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} .bt-dialog{flex:1;display:flex;flex-direction:column;min-width:0} -@@ -2150,11 +2150,11 @@ button.bt-q-opt:active{background:var(--bt-active)} +@@ -2150,11 +2150,11 @@ .bt-soft:hover{background:var(--bt-active)} .bt-time{display:block;text-align:center;font-size:12px;line-height:16px;color:var(--bt-ink-2);padding:6px 0} @@ -3862,7 +3204,7 @@ diff --git a/client.js b/client.js .bt-panel-grip{position:absolute;left:-5px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:3;touch-action:none} .bt-panel-grip:hover,.bt-panel[data-dragging] .bt-panel-grip{background:linear-gradient(to right,transparent 4px,var(--bt-line-2) 4px,var(--bt-line-2) 6px,transparent 6px)} .bt-drag-shield{position:fixed;inset:0;z-index:45;cursor:col-resize} -@@ -2163,6 +2163,8 @@ button.bt-q-opt:active{background:var(--bt-active)} +@@ -2163,6 +2163,8 @@ .bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0} .bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px} .bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} @@ -3871,7 +3213,7 @@ diff --git a/client.js b/client.js .bt-drawer-name:hover{background:var(--bt-hover)} .bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500} .bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center} -@@ -2197,7 +2197,7 @@ button.bt-q-opt:active{background:var(--bt-active)} +@@ -2197,7 +2197,7 @@ .bt-avatar-anchor{position:relative} .bt-avatar-trigger{position:relative;width:64px;height:64px;border:0;border-radius:50%;background:none;cursor:pointer;padding:0} .bt-avatar-pencil{position:absolute;right:-2px;bottom:-2px;width:22px;height:22px;border-radius:50%;background:var(--bt-main);border:1px solid var(--bt-line-2);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;box-shadow:0 1px 4px rgba(0,0,0,.12)} @@ -3880,7 +3222,7 @@ diff --git a/client.js b/client.js .bt-bs .bt-avedit{transform-origin:32px 0} .bt-avedit-top{display:flex;align-items:center;justify-content:space-between} .bt-avedit-tabs{position:relative;display:flex;gap:2px;background:var(--bt-hover);border-radius:999px;padding:2px} -@@ -2237,9 +2237,23 @@ button.bt-q-opt:active{background:var(--bt-active)} +@@ -2237,9 +2237,23 @@ .bt-seg button.bt-perm-danger,.bt-seg button.bt-perm-danger[aria-pressed=true]{color:#e02135} /* Settings dialog: a 198px page list beside the page. */ @@ -3905,7 +3247,7 @@ diff --git a/client.js b/client.js .bt-settings{--bt-set-w:min(800px,calc(100vw - 48px));--bt-set-h:min(620px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));position:relative;width:var(--bt-set-w);height:var(--bt-set-h);background:var(--bt-main);border-radius:16px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;overflow:hidden;color:var(--bt-ink);transition:${spring(SPRING_SHAPE, "width", "height")};animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} .bt-settings[data-wide]{--bt-set-w:min(1100px,calc(100vw - 48px));--bt-set-h:min(780px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))))} :is(.bt-settings-layer,.bt-mem-layer)[data-leaving]{pointer-events:none} -@@ -2255,6 +2255,7 @@ body[data-ds-dark-theme] .bt-scrim{background:rgba(0,0,0,.5)} +@@ -2255,6 +2255,7 @@ .bt-settings-nav>.bt-thumb{left:8px;right:8px;border-radius:8px;background:var(--bt-active)} .bt-settings-nav[data-glide] .bt-settings-item[aria-current=page]{background:transparent} .bt-settings-shell{margin-top:auto} @@ -3913,7 +3255,7 @@ diff --git a/client.js b/client.js /* Usage while another page is open: out of the flow at Usage's own width, so it keeps its layout, and not rendered. */ .bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none} -@@ -2288,13 +2288,60 @@ button.bt-set-row:hover{background:var(--bt-hover)} +@@ -2288,13 +2288,60 @@ .bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)} .bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px} .bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px} @@ -3974,7 +3316,7 @@ diff --git a/client.js b/client.js /* Settings → Connectors. */ .bt-connector{display:flex;flex-direction:column;gap:10px;padding:12px} -@@ -2308,8 +2308,17 @@ button.bt-set-row:hover{background:var(--bt-hover)} +@@ -2308,8 +2308,17 @@ .bt-connector-actions{flex:none;display:flex;gap:6px} .bt-connector-form{display:flex;flex-direction:column;gap:8px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} .bt-connector-form-row{display:flex;align-items:center;gap:6px} @@ -3992,7 +3334,7 @@ diff --git a/client.js b/client.js /* Shell chrome reshaped for the team view. Selectors use stable data attributes where the shell offers them and CSS-module name suffixes elsewhere. */ -@@ -2361,7 +2361,7 @@ button.bt-set-row:hover{background:var(--bt-hover)} +@@ -2361,7 +2361,7 @@ .bt-tool:hover,.bt-tool[aria-expanded=true]{background:var(--bt-hover);color:var(--bt-ink)} .bt-tool:active{background:var(--bt-active)} .bt-tool:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} @@ -4001,7 +3343,7 @@ diff --git a/client.js b/client.js .bt-react-strip button{width:32px;height:32px;border:0;border-radius:50%;background:none;font-size:18px;line-height:1;cursor:pointer;transition:background-color .12s ease,${spring(SPRING_POP, "transform")};animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} .bt-react-strip button:nth-child(2){animation-delay:20ms} .bt-react-strip button:nth-child(3){animation-delay:40ms} -@@ -2380,7 +2380,7 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side +@@ -2380,7 +2380,7 @@ .bt-you-wrap{display:inline-flex;padding:0} .bt-connect-foot{width:100%;height:36px} @@ -4010,7 +3352,7 @@ diff --git a/client.js b/client.js .bt-menu button{display:flex;align-items:center;gap:4px;width:100%;min-height:30px;text-align:left;border:0;background:none;border-radius:6px;padding:6px 8px;font:inherit;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;box-sizing:border-box} .bt-menu button:hover{background:var(--bt-hover)} .bt-menu button svg{flex:none;width:16px;height:16px;margin:0 1px} -@@ -2390,7 +2390,7 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side +@@ -2390,7 +2390,7 @@ .bt-menu[data-leaving]{pointer-events:none;animation:bt-pop-out .12s cubic-bezier(.4,0,1,1) forwards} .bt-menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .bt-menu-hint{flex:none;color:var(--bt-ink-3);font-size:12px;font-variant-numeric:tabular-nums} @@ -4019,7 +3361,7 @@ diff --git a/client.js b/client.js .bt-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.46);animation:bt-fade .22s ease-out} .bt-palette{position:absolute;top:50%;left:50%;width:min(560px,92vw);transform:translate(-50%,-50%);display:flex;flex-direction:column;border-radius:12px;background:var(--bt-main);border:1px solid var(--bt-line-2);box-shadow:0 24px 48px -12px rgba(0,0,0,.25);overflow:hidden;color:var(--bt-ink);animation:bt-palette-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} .bt-palette-layer[data-leaving]{pointer-events:none} -@@ -2427,7 +2427,8 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side +@@ -2427,7 +2427,8 @@ .bt-palette-empty-label{font-size:14px;font-weight:500;color:var(--bt-ink-2)} .bt-palette-empty-hint{font-size:13px;line-height:16px;color:var(--bt-ink-3)} @media (prefers-reduced-motion:reduce){.bt-palette{animation:bt-fade .1s ease-out}} @@ -4029,7 +3371,7 @@ diff --git a/client.js b/client.js .bt-account{flex:none;width:36px;height:36px;border:0;padding:0;background:none;border-radius:50%;cursor:pointer;display:inline-flex} .bt-account:hover .bt-you,.bt-account[aria-expanded=true] .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))} /* Popover keyframes move translate and scale, which compose with a transform the popover -@@ -2494,7 +2494,7 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} +@@ -2494,7 +2494,7 @@ .bt-mem-row>svg{flex:none;color:var(--bt-ink-2)} .bt-mem-row-copy{flex:1;text-align:left} .bt-mem-row-go{display:inline-flex;align-items:center;gap:2px;color:var(--bt-ink-3);font-size:12px} @@ -4038,7 +3380,7 @@ diff --git a/client.js b/client.js .bt-mem-scrim{animation:bt-fade .2s ease-out} .bt-mem{position:relative;width:min(680px,calc(100vw - 48px));height:min(760px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));background:var(--bt-main);border-radius:20px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;flex-direction:column;overflow:hidden;color:var(--bt-ink);animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} .bt-mem-head{position:relative;display:flex;align-items:center;gap:8px;height:60px;flex:none;padding:0 14px 0 24px;border-bottom:.5px solid var(--bt-line);box-sizing:border-box} -@@ -2599,6 +2599,17 @@ var CONVERSATION_CSS = ` +@@ -2599,6 +2599,17 @@ .bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)} .bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px} .bt-think summary::-webkit-details-marker{display:none} @@ -4056,7 +3398,7 @@ diff --git a/client.js b/client.js .bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)} .bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap} .bt-event>span{padding:0 2px} -@@ -2612,7 +2612,6 @@ var CONVERSATION_CSS = ` +@@ -2612,7 +2612,6 @@ .bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap} [class*="_scroll"]>[class*="_column"]::before{content:var(--bt-first-sep,none);display:block;flex:none;height:28px;margin:17px 0 12px;font-size:12px;line-height:28px;text-align:center;color:var(--bt-ink-2);white-space:nowrap} @@ -4064,7 +3406,7 @@ diff --git a/client.js b/client.js .bt-group-replies{display:contents} /* A Bot chat is flat in every work-details mode, without changing the user's mode (it is a DSH-wide setting): step groups lose their header and stay open, and what a -@@ -2622,21 +2622,48 @@ var CONVERSATION_CSS = ` +@@ -2622,21 +2622,48 @@ [data-step-process]>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} :is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important} [class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important} @@ -4121,7 +3463,7 @@ diff --git a/client.js b/client.js [data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important} [data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important} [data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important} -@@ -2646,8 +2646,19 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} +@@ -2646,8 +2646,19 @@ [data-chat-running]{display:block!important;width:100%;min-height:0!important;margin:0!important;padding:0!important} [data-chat-running]>span:not([role=status]){display:none!important} [data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important} @@ -4141,313 +3483,194 @@ diff --git a/client.js b/client.js .bt-typing>.bt-mark,.bt-typing>.bt-cluster{flex-shrink:0} .bt-typing>.bt-mark{cursor:pointer;transition:filter .16s ease-out} .bt-typing>.bt-mark:hover{filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))} -@@ -2676,2104 +2676,4297 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} - [data-bt-enter="new"]{animation:none} - [data-bt-enter="after-collapse"]{animation:bt-fade-in .12s ease-out} - } --@keyframes bt-fade-in{from{opacity:0}to{opacity:1}} --@keyframes bt-fade-out{from{opacity:1}to{opacity:0}} --[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} +@@ -2679,7 +2679,8 @@ + @keyframes bt-fade-in{from{opacity:0}to{opacity:1}} + @keyframes bt-fade-out{from{opacity:1}to{opacity:0}} + [data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} -[data-composer-card]:hover,[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important} --[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important} --[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important} --[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important} --[data-composer-card]>[class*="_row"]{display:contents!important} --[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0} --[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important} --[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0} --[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%} --[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important} --[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important} --[data-composer-placeholder]{font-size:0!important;line-height:0!important} --[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)} --/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */ --[data-composer-card] button[class*="_add"]{display:none!important} --.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} --.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} --.bt-plus:active{scale:.9} --.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}} --.bt-plus[aria-expanded=true] svg{rotate:45deg} --.bt-plus-menu{min-width:180px} --[data-composer-card] [class*="_tools"]{gap:0!important} --[data-composer-card] [class*="_modes"]:empty{display:none!important} --[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important} --[data-composer-card] [class*="_trailing"]{gap:8px} --[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px} --[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} --[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9} --[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9} --@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}} --[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px} --[data-composer-card] button[class*="_primary"]:disabled{display:none!important} --[data-composer-dock]{display:none!important} --.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} --.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)} --.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} --.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite} --@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}} --.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} --.bt-voice:hover{opacity:.9} --.bt-mic:active,.bt-voice:active{scale:.9} --[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid} --body[data-bt-room] .bt-voice{display:none!important} --body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)} --.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out} --[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2} --.bt-q{order:1} --div:has(> nav [class*="_marks"]){display:none!important} --/* Every user-side bubble (sent, steering mid-turn, not yet admitted, question replies) -- takes its surface and its text from the one --bt-user pair: a skin that repaints -- the host's bubble surface alone would otherwise leave the text unreadable. */ --[class*="_userRow"] [class*="_bubble"],[data-chat-flow-kind=question-reply] [class*="_bubble"],[data-chat-flow-kind=command-input] [class*="_bubble"]{background:var(--bt-user)!important;color:var(--bt-user-ink)!important;border-radius:18px!important;padding:7px 12px!important;font-size:var(--dsh-content-font-size,14px)!important;line-height:calc(var(--dsh-content-font-size,14px) + 8px)!important} --[class*="_userRow"] [class*="_bubble"] [class*="_markdown"],[class*="_userRow"] [class*="_bubble"] a{color:inherit} --/* Sent messages get the hover toolbar below; steering and unsent bubbles keep none. */ --[class*="_userRow"] [class*="_actions"]{display:none!important} --[data-chat-flow-kind=user] [class*="_userRow"]{flex-direction:row-reverse;align-items:center;justify-content:flex-start} --[data-chat-flow-kind=user] [class*="_actions"]{display:flex!important;height:24px!important;gap:2px!important;opacity:0!important;visibility:hidden;transition:opacity .12s ease,visibility .12s!important;flex:none} --[data-chat-flow-kind=user] [class*="_userRow"]:hover [class*="_actions"],[data-chat-flow-kind=user] [class*="_userRow"]:focus-within [class*="_actions"]{opacity:1!important;visibility:visible} --[data-chat-flow-kind=user] [class*="_actions"] [class*="_timeStart"]{display:none} --[data-chat-flow-kind=user] [class*="_actions"] button{width:24px;height:24px;border-radius:6px;color:var(--bt-ink-2)} --[data-chat-flow-kind=user] [class*="_actions"] button:hover{background:var(--bt-hover);color:var(--bt-ink)} --[data-chat-flow-kind=user] [class*="_actions"] svg{width:15px;height:15px} --.bt-q-inline{display:flex;margin:4px 0 6px} --[data-chat-flow-kind=assistant-step]+[data-step-process]:has(.bt-q-inline){margin-top:0!important} --[data-chat-flow-kind=question-reply]:has(.bt-q-inline),[data-chat-group-key]:has([data-chat-flow-kind=question-reply] .bt-q-inline),[data-chat-group-key]:has(.bt-q-gone){margin-top:0!important} --.bt-q-inline .bt-q{margin:0;max-width:min(550px,86%);order:0} --.bt-q-dock{width:100%;display:flex;justify-content:center} --.bt-q-dock .bt-q{margin:0 auto 8px} --[class*="_toBottomSlot"]{justify-content:center!important;padding-right:0!important} --[data-conversation-scroll] [class*="_toBottomSlot"]{bottom:calc(var(--dsh-composer-height,152px) + 8px)!important} --[class*="_toBottomSlot"]>button{width:36px!important;height:36px!important;margin-top:-36px!important;background:var(--bt-main)!important;color:var(--bt-ink)!important;box-shadow:0 0 0 .5px var(--bt-line-2),0 2px 8px -2px rgba(0,0,0,.12)!important;animation:bt-tobottom-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out;transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}} --[class*="_toBottomSlot"]>button:active{scale:.9} --[class*="_toBottomSlot"]>button:hover{background:color-mix(in srgb,var(--bt-ink) 4%,var(--bt-main))!important} --[class*="_toBottomSlot"]>button svg{display:none} --[class*="_toBottomSlot"]>button::before{content:"";width:20px;height:20px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat} --@keyframes bt-tobottom-in{from{translate:0 8px;scale:.8}} --[data-bt-news] [class*="_toBottomSlot"]>button{visibility:hidden!important;pointer-events:none!important} --.bt-news{position:fixed;z-index:30;transform:translateX(-50%);display:inline-flex;align-items:center;overflow:hidden;border-radius:999px;background:#0c64c1;color:#fcfcfc;box-shadow:inset 0 0 0 1px rgba(20,20,20,.05),0 1px 3px 0 rgba(0,0,0,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-news-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} --@keyframes bt-news-in{from{translate:0 8px;scale:.9}} --.bt-news[data-direction=up]{animation-name:bt-news-in-up,bt-veil-in} --@keyframes bt-news-in-up{from{translate:0 -8px;scale:.9}} --.bt-news-jump{display:inline-flex;align-items:center;gap:2px;margin:0;padding:4px 26px 4px 4px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer;outline:none} --.bt-news-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px} --.bt-news-ico svg{width:16px;height:16px} --.bt-news-x{position:absolute;right:4px;top:50%;z-index:1;transform:translateY(-50%);width:20px;height:20px;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;opacity:.75;display:inline-flex;align-items:center;justify-content:center;cursor:pointer} --.bt-news-x svg{width:12px;height:12px} --.bt-news-x:hover{opacity:1;background:rgba(20,20,20,.5)} --@media (prefers-reduced-motion:reduce){.bt-news,[class*="_toBottomSlot"]>button{animation:none}} --.bt-new-sep{display:flex;align-items:center;gap:8px;margin:14px 0 8px;padding:2px 0;font-size:11px;line-height:14px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:#0c64c1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} --.bt-new-sep::before,.bt-new-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,#0c64c1 20%,transparent)} --.bt-open-new{display:flex;flex-direction:column} --.bt-open-new .bt-new-sep{margin:18px 0 0} --.bt-open-new .bt-time-sep{height:16px;margin:32px 0 2px} --.bt-earlier{display:flex;flex-direction:column;gap:6px} --.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none} --.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px} --.bt-urow{display:flex;justify-content:flex-end} --.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} --.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} --.bt-ububble p{margin:0;line-height:22px} --.bt-uimages{display:block;font-size:12px;opacity:.7} --.bt-earlier .bt-event{margin:4px 0} --.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} --.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} --`; --function installStyles(text, tag) { -- const style = document.createElement("style"); -- style.dataset.dshBot = tag; -- style.textContent = text; -- document.head.appendChild(style); -- return style; -+@keyframes bt-fade-in{from{opacity:0}to{opacity:1}} -+@keyframes bt-fade-out{from{opacity:1}to{opacity:0}} -+[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} +[data-composer-card]:hover{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important} +[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 55%,transparent)!important} -+[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important} -+[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important} -+[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important} -+[data-composer-card]>[class*="_row"]{display:contents!important} -+[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0} -+[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important} -+[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0} -+[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%} -+[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important} -+[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important} -+[data-composer-placeholder]{font-size:0!important;line-height:0!important} + [data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important} + [class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important} + [class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important} +@@ -2691,6 +2691,8 @@ + [data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important} + [data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important} + [data-composer-placeholder]{font-size:0!important;line-height:0!important} +/* A friend's Bot chat: no local run stats (they are relay turns, always 0; #419). */ +body[data-bt-friend-chat] [data-composer-stats]{display:none!important} -+[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)} -+/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */ -+[data-composer-card] button[class*="_add"]{display:none!important} -+.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} -+.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} -+.bt-plus:active{scale:.9} -+.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}} -+.bt-plus[aria-expanded=true] svg{rotate:45deg} -+.bt-plus-menu{min-width:180px} -+[data-composer-card] [class*="_tools"]{gap:0!important} -+[data-composer-card] [class*="_modes"]:empty{display:none!important} -+[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important} -+[data-composer-card] [class*="_trailing"]{gap:8px} -+[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px} -+[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} -+[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9} -+[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9} -+@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}} -+[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px} -+[data-composer-card] button[class*="_primary"]:disabled{display:none!important} -+[data-composer-dock]{display:none!important} -+.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} -+.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)} -+.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} -+.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite} -+@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}} -+.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} -+.bt-voice:hover{opacity:.9} -+.bt-mic:active,.bt-voice:active{scale:.9} -+[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid} -+body[data-bt-room] .bt-voice{display:none!important} + [data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)} + /* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */ + [data-composer-card] button[class*="_add"]{display:none!important} +@@ -2722,7 +2722,9 @@ + .bt-mic:active,.bt-voice:active{scale:.9} + [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid} + body[data-bt-room] .bt-voice{display:none!important} +body[data-bt-room] [data-composer-card] [class*="_activity"]{display:none!important} -+body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)} + body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)} +body[data-bt-workflow] [data-composer-card]{display:none!important} -+.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out} -+[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2} -+.bt-q{order:1} -+div:has(> nav [class*="_marks"]){display:none!important} -+/* Every user-side bubble (sent, steering mid-turn, not yet admitted, question replies) -+ takes its surface and its text from the one --bt-user pair: a skin that repaints -+ the host's bubble surface alone would otherwise leave the text unreadable. */ -+[class*="_userRow"] [class*="_bubble"],[data-chat-flow-kind=question-reply] [class*="_bubble"],[data-chat-flow-kind=command-input] [class*="_bubble"]{background:var(--bt-user)!important;color:var(--bt-user-ink)!important;border-radius:18px!important;padding:7px 12px!important;font-size:var(--dsh-content-font-size,14px)!important;line-height:calc(var(--dsh-content-font-size,14px) + 8px)!important} -+[class*="_userRow"] [class*="_bubble"] [class*="_markdown"],[class*="_userRow"] [class*="_bubble"] a{color:inherit} -+/* Sent messages get the hover toolbar below; steering and unsent bubbles keep none. */ -+[class*="_userRow"] [class*="_actions"]{display:none!important} -+[data-chat-flow-kind=user] [class*="_userRow"]{flex-direction:row-reverse;align-items:center;justify-content:flex-start} -+[data-chat-flow-kind=user] [class*="_actions"]{display:flex!important;height:24px!important;gap:2px!important;opacity:0!important;visibility:hidden;transition:opacity .12s ease,visibility .12s!important;flex:none} -+[data-chat-flow-kind=user] [class*="_userRow"]:hover [class*="_actions"],[data-chat-flow-kind=user] [class*="_userRow"]:focus-within [class*="_actions"]{opacity:1!important;visibility:visible} -+[data-chat-flow-kind=user] [class*="_actions"] [class*="_timeStart"]{display:none} -+[data-chat-flow-kind=user] [class*="_actions"] button{width:24px;height:24px;border-radius:6px;color:var(--bt-ink-2)} -+[data-chat-flow-kind=user] [class*="_actions"] button:hover{background:var(--bt-hover);color:var(--bt-ink)} -+[data-chat-flow-kind=user] [class*="_actions"] svg{width:15px;height:15px} -+.bt-q-inline{display:flex;margin:4px 0 6px} -+[data-chat-flow-kind=assistant-step]+[data-step-process]:has(.bt-q-inline){margin-top:0!important} -+[data-chat-flow-kind=question-reply]:has(.bt-q-inline),[data-chat-group-key]:has([data-chat-flow-kind=question-reply] .bt-q-inline),[data-chat-group-key]:has(.bt-q-gone){margin-top:0!important} -+.bt-q-inline .bt-q{margin:0;max-width:min(550px,86%);order:0} -+.bt-q-dock{width:100%;display:flex;justify-content:center} -+.bt-q-dock .bt-q{margin:0 auto 8px} -+[class*="_toBottomSlot"]{justify-content:center!important;padding-right:0!important} -+[data-conversation-scroll] [class*="_toBottomSlot"]{bottom:calc(var(--dsh-composer-height,152px) + 8px)!important} -+[class*="_toBottomSlot"]>button{width:36px!important;height:36px!important;margin-top:-36px!important;background:var(--bt-main)!important;color:var(--bt-ink)!important;box-shadow:0 0 0 .5px var(--bt-line-2),0 2px 8px -2px rgba(0,0,0,.12)!important;animation:bt-tobottom-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out;transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}} -+[class*="_toBottomSlot"]>button:active{scale:.9} -+[class*="_toBottomSlot"]>button:hover{background:color-mix(in srgb,var(--bt-ink) 4%,var(--bt-main))!important} -+[class*="_toBottomSlot"]>button svg{display:none} -+[class*="_toBottomSlot"]>button::before{content:"";width:20px;height:20px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat} -+@keyframes bt-tobottom-in{from{translate:0 8px;scale:.8}} -+[data-bt-news] [class*="_toBottomSlot"]>button{visibility:hidden!important;pointer-events:none!important} -+.bt-news{position:fixed;z-index:30;transform:translateX(-50%);display:inline-flex;align-items:center;overflow:hidden;border-radius:999px;background:#0c64c1;color:#fcfcfc;box-shadow:inset 0 0 0 1px rgba(20,20,20,.05),0 1px 3px 0 rgba(0,0,0,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-news-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} -+@keyframes bt-news-in{from{translate:0 8px;scale:.9}} -+.bt-news[data-direction=up]{animation-name:bt-news-in-up,bt-veil-in} -+@keyframes bt-news-in-up{from{translate:0 -8px;scale:.9}} -+.bt-news-jump{display:inline-flex;align-items:center;gap:2px;margin:0;padding:4px 26px 4px 4px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer;outline:none} -+.bt-news-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px} -+.bt-news-ico svg{width:16px;height:16px} -+.bt-news-x{position:absolute;right:4px;top:50%;z-index:1;transform:translateY(-50%);width:20px;height:20px;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;opacity:.75;display:inline-flex;align-items:center;justify-content:center;cursor:pointer} -+.bt-news-x svg{width:12px;height:12px} -+.bt-news-x:hover{opacity:1;background:rgba(20,20,20,.5)} -+@media (prefers-reduced-motion:reduce){.bt-news,[class*="_toBottomSlot"]>button{animation:none}} -+.bt-new-sep{display:flex;align-items:center;gap:8px;margin:14px 0 8px;padding:2px 0;font-size:11px;line-height:14px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:#0c64c1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} -+.bt-new-sep::before,.bt-new-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,#0c64c1 20%,transparent)} -+.bt-open-new{display:flex;flex-direction:column} -+.bt-open-new .bt-new-sep{margin:18px 0 0} -+.bt-open-new .bt-time-sep{height:16px;margin:32px 0 2px} -+.bt-earlier{display:flex;flex-direction:column;gap:6px} -+.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none} -+.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px} -+.bt-urow{display:flex;justify-content:flex-end} + .bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out} + [data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2} + .bt-q{order:1} +@@ -2776,6 +2776,7 @@ + .bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none} + .bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px} + .bt-urow{display:flex;justify-content:flex-end} +.bt-urow>.bt-lead{align-self:flex-end;margin-left:6px} -+.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} -+.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} -+.bt-ububble p{margin:0;line-height:22px} -+.bt-uimages{display:block;font-size:12px;opacity:.7} -+.bt-earlier .bt-event{margin:4px 0} -+.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} -+.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} -+`; -+function installStyles(text, tag) { -+ const style = document.createElement("style"); -+ style.dataset.dshBot = tag; -+ style.textContent = text; -+ document.head.appendChild(style); -+ return style; -+} -+var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g; -+var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g; -+function sheetClassNames(sheet) { -+ const names = []; -+ const visit = (rules) => { -+ for (const rule of rules) { -+ if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]); -+ if (rule.cssRules) visit(rule.cssRules); -+ } -+ }; -+ try { -+ visit(sheet.cssRules); -+ } catch { -+ } -+ return names; -+} -+function compileFragmentSelectors(text, names) { -+ const compiled = /* @__PURE__ */ new Map(); -+ return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { -+ if (!compiled.has(fragment)) { -+ const exact = []; -+ for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); -+ compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole); -+ } -+ return compiled.get(fragment); -+ }); -+} -+function installCompiledStyles(text, tag) { -+ const style = installStyles("", tag); -+ const cache = /* @__PURE__ */ new WeakMap(); -+ const render = () => { -+ const names = /* @__PURE__ */ new Set(); -+ for (const sheet of document.styleSheets) { -+ if (sheet.ownerNode?.dataset?.dshBot) continue; -+ let list = cache.get(sheet); -+ if (!list) { -+ list = sheetClassNames(sheet); -+ cache.set(sheet, list); -+ } -+ for (const name of list) names.add(name); -+ } -+ const next = compileFragmentSelectors(text, names); -+ if (style.textContent !== next) style.textContent = next; -+ }; -+ render(); -+ let timer; -+ const schedule = () => { -+ clearTimeout(timer); -+ timer = setTimeout(render, 120); -+ }; -+ const onLoad = (event) => { -+ if (event.target instanceof HTMLLinkElement) schedule(); -+ }; -+ const observer = new MutationObserver(schedule); -+ observer.observe(document.head, { childList: true }); -+ document.addEventListener("load", onLoad, true); -+ return () => { -+ observer.disconnect(); -+ document.removeEventListener("load", onLoad, true); -+ clearTimeout(timer); -+ style.remove(); -+ }; -+} -+ -+// 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(); + .bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} + .bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} + .bt-ububble p{margin:0;line-height:22px} +@@ -2784,10 +2784,10 @@ + .bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} + .bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} + `; +-function installStyles(text, tag) { ++function installStyles(text2, tag) { + const style = document.createElement("style"); + style.dataset.dshBot = tag; +- style.textContent = text; ++ style.textContent = text2; + document.head.appendChild(style); + return style; + } +@@ -2807,9 +2807,9 @@ + } + return names; + } +-function compileFragmentSelectors(text, names) { ++function compileFragmentSelectors(text2, names) { + const compiled = /* @__PURE__ */ new Map(); +- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { ++ return text2.replace(FRAGMENT_SELECTOR, (whole, fragment) => { + if (!compiled.has(fragment)) { + const exact = []; + for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); +@@ -2818,7 +2818,7 @@ + return compiled.get(fragment); + }); + } +-function installCompiledStyles(text, tag) { ++function installCompiledStyles(text2, tag) { + const style = installStyles("", tag); + const cache = /* @__PURE__ */ new WeakMap(); + const render = () => { +@@ -2832,7 +2832,7 @@ + } + for (const name of list) names.add(name); + } +- const next = compileFragmentSelectors(text, names); ++ const next = compileFragmentSelectors(text2, names); + if (style.textContent !== next) style.textContent = next; + }; + render(); +@@ -2855,226 +2855,204 @@ + }; + } + +-// src/client/mark.js +-var import_react2 = require("react"); +-var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; +-var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; +-var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; +-function useGaze(ref, enabled) { +- (0, import_react2.useEffect)(() => { +- if (!enabled) return void 0; +- let frame = 0; +- let point = null; +- const place = () => { +- frame = 0; +- const node = ref.current; +- if (!node || !point) return; +- const box = node.getBoundingClientRect(); +- const clamp = (value) => Math.max(-1, Math.min(1, value)); +- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); +- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); +- }; +- const onMove = (event) => { +- point = { x: event.clientX, y: event.clientY }; +- if (!frame) frame = requestAnimationFrame(place); +- }; +- const onLeave = () => { +- point = null; +- ref.current?.style.setProperty("--bt-gx", "0"); +- ref.current?.style.setProperty("--bt-gy", "0"); +- }; +- window.addEventListener("pointermove", onMove, { passive: true }); +- document.documentElement.addEventListener("pointerleave", onLeave); +- return () => { +- cancelAnimationFrame(frame); +- window.removeEventListener("pointermove", onMove); +- document.documentElement.removeEventListener("pointerleave", onLeave); +- }; +- }, [enabled]); +-} +-var FIDGETS = ["hop", "sway", "stretch", "look"]; +-function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) { +- const look = given ?? lookOf(bot); +- const ref = (0, import_react2.useRef)(null); +- const maskId = (0, import_react2.useRef)(""); +- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`; +- const [poked, setPoked] = (0, import_react2.useState)(false); +- useGaze(ref, gaze); +- const common = { +- ref, +- className: poked ? "bt-mark bt-mark-poke" : "bt-mark", +- "data-shape": look.image ? "image" : shapeId(look.shape), +- "data-state": STATES.includes(state) ? state : "idle", +- "data-live": live ? "" : void 0, +- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0, +- "data-gaze": gaze ? "" : void 0, +- "data-poke": pokeable ? "" : void 0, +- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} }, +- role: label ? "img" : void 0, +- "aria-label": label, +- "aria-hidden": label ? void 0 : true, +- onClick: pokeable ? () => setPoked(true) : void 0, +- onAnimationEnd: poked ? (event) => { +- if (/^bt-poke/.test(event.animationName)) setPoked(false); +- } : void 0 +- }; +- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false })); +- const box = shapeOf(look.shape).size; +- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } })); +-} +-function StarIcon({ size = 8 }) { +- return (0, import_react2.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" }) +- ); +-} +-function AdminStar() { +- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 })); +-} +-function MainStar() { +- return (0, import_react2.createElement)( +- "svg", +- { viewBox: "0 0 20 20", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" }) +- ); +-} +-function useSettled(state) { +- const prev = (0, import_react2.useRef)(state); +- const [settled, setSettled] = (0, import_react2.useState)(false); +- (0, import_react2.useEffect)(() => { +- const was = prev.current; +- prev.current = state; +- if (state !== "idle") { +- setSettled(false); +- return void 0; +- } +- if (was === "idle" || was === "alert") return void 0; +- setSettled(true); +- const timer = setTimeout(() => setSettled(false), 1800); +- return () => clearTimeout(timer); +- }, [state]); +- return settled; ++// 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) { @@ -4460,7 +3683,28 @@ diff --git a/client.js b/client.js +function isAutoFaceTitle(name) { + const n = String(name ?? "").trim(); + return isGenericFaceTitle(n) || n.startsWith("Face-to-face · "); -+} + } +-function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { +- const settled = useSettled(state); +- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size }); +- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null; +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } }, +- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }), +- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null, +- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null, +- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null, +- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null +- ); +-} +-function clusterSeats(count, frame) { +- if (count === 1) return [[0, 0, frame]]; +- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; +- if (count === 3) { +- const size = frame * 5 / 9; +- const rest = frame - size; +- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; +function ownersFromMembers(members) { + const rows = []; + const seen = /* @__PURE__ */ new Set(); @@ -4471,10 +3715,43 @@ diff --git a/client.js b/client.js + seen.add(key); + const handle = String(member?.handle ?? member?.id ?? "").trim().toLowerCase(); + rows.push({ owner, sort: handle || key }); -+ } + } +- const half = frame / 2; +- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; + rows.sort((a, b) => a.sort.localeCompare(b.sort)); + return rows.slice(0, 3).map((row) => row.owner); -+} + } +-function RoomAvatar({ room: room2, roster, size = 36 }) { +- const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); +- const people = members.length === 1 ? [members[0], null] : members; +- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } }); +- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size }); +- if (size <= 20 && people.length > 1) { +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } }, +- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)), +- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null +- ); +- } +- const seats = clusterSeats(Math.min(people.length, 4), size); +- const overflow = people.length > 4 ? people.length - 3 : 0; +- const gap = size / 14; +- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap })); +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-cluster", style: { width: size, height: size } }, +- seats.map(([x, y, s], index) => { +- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`); +- const style = { left: x, top: y, width: s, height: s }; +- if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" }); +- if (overflow > 0 && index === 3) { +- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`); +- } +- const bot = people[index]; +- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); +- }) +- ); +function uniqueOwners(owners) { + const seen = /* @__PURE__ */ new Set(); + const out = []; @@ -4518,11 +3795,11 @@ diff --git a/client.js b/client.js + return ``; +} +function brandTitle(title) { -+ const text = String(title ?? ""); -+ if (text === "DeepSeek Harness" || text === "DS Bot") return "FlyBot"; -+ if (text.endsWith(" — DeepSeek Harness")) return `${text.slice(0, -" — DeepSeek Harness".length)} — FlyBot`; -+ if (/\bDS Bot\b/.test(text)) return text.replace(/\bDS Bot\b/g, "FlyBot"); -+ return text; ++ const text2 = String(title ?? ""); ++ if (text2 === "DeepSeek Harness" || text2 === "DS Bot") return "FlyBot"; ++ if (text2.endsWith(" — DeepSeek Harness")) return `${text2.slice(0, -" — DeepSeek Harness".length)} — FlyBot`; ++ if (/\bDS Bot\b/.test(text2)) return text2.replace(/\bDS Bot\b/g, "FlyBot"); ++ return text2; +} +function sessionTabLabel(roster, sessionId) { + if (!roster || !sessionId) return ""; @@ -4579,116 +3856,135 @@ diff --git a/client.js b/client.js + observer?.disconnect(); + link.remove(); + }; -+} -+ -+// src/client/icons.js + } + + // src/client/icons.js +-var import_react3 = require("react"); +var import_react5 = require("react"); -+function PlusIcon() { + function PlusIcon() { +- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); + return (0, import_react5.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); -+} -+function SearchIcon() { + } + function SearchIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react5.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); -+} -+function PeopleIcon() { + } + function PeopleIcon() { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, + "svg", + { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), +- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }), +- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) + (0, import_react5.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), + (0, import_react5.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }), + (0, import_react5.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) -+ ); -+} -+function ArrowUpIcon() { + ); + } + function ArrowUpIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); -+} -+function MicIcon({ size = 16 }) { + } + function MicIcon({ size = 16 }) { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, + "svg", + { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), +- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) + (0, import_react5.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), + (0, import_react5.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) -+ ); -+} -+function WaveIcon({ size = 16 }) { + ); + } + function WaveIcon({ size = 16 }) { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, + "svg", + { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, +- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) + [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react5.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) -+ ); -+} -+function ShareIcon() { + ); + } + function ShareIcon() { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "svg", + { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) + (0, import_react5.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) -+ ); -+} -+function CloseIcon() { + ); + } + function CloseIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); -+} + } +-var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); +-var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); +var ChevronLeftIcon = () => (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); +var ChevronRightIcon = () => (0, import_react5.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); -+var lineIcon = (paths) => function LineIcon() { + var lineIcon = (paths) => function LineIcon() { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "svg", + { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) + paths.map((d, index) => (0, import_react5.createElement)("path", { key: index, d })) -+ ); -+}; -+var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; -+function CloudDownloadIcon({ done = false }) { + ); + }; + var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; + function CloudDownloadIcon({ done = false }) { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "svg", + { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("path", { d: CLOUD }), +- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) + (0, import_react5.createElement)("path", { d: CLOUD }), + done ? (0, import_react5.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react5.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react5.createElement)("path", { d: "M10 9.75v7" }), (0, import_react5.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) -+ ); -+} -+var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); -+var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]); -+var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]); -+var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]); -+var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]); -+var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]); -+var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); -+var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); -+var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); + ); + } + var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); +@@ -3086,6 +3086,7 @@ + var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); + var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); + var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); +var WorkflowIcon = lineIcon(["M4.5 4h4v3.5h-4z", "M11.5 12.5h4V16h-4z", "M6.5 7.5v3.5a2 2 0 0 0 2 2h3"]); -+var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); -+var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); -+var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); -+var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]); -+var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); -+var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); -+function PinMarkIcon() { + var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); + var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); + var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); +@@ -3093,14 +3093,14 @@ + var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); + var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); + function PinMarkIcon() { +- return (0, import_react3.createElement)( + return (0, import_react5.createElement)( -+ "svg", -+ { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, + "svg", + { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, +- (0, import_react3.createElement)( + (0, import_react5.createElement)( -+ "g", -+ { transform: "rotate(45 10 10)" }, + "g", + { transform: "rotate(45 10 10)" }, +- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), +- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) + (0, import_react5.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), + (0, import_react5.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) -+ ) -+ ); -+} -+var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); -+var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); -+var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); -+var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); -+var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); -+var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); -+var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); -+var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); + ) + ); + } +@@ -3112,21 +3112,48 @@ + var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); + var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); + var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); +var LinkIcon = lineIcon(["M8.5 11.5a3.5 3.5 0 0 0 5 0l2.5-2.5a3.5 3.5 0 0 0-5-5l-1 1", "M11.5 8.5a3.5 3.5 0 0 0-5 0L4 11a3.5 3.5 0 0 0 5 5l1-1"]); -+var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); -+var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); -+var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); + var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); + var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); + var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); +var ChatIcon = lineIcon(["M4 5.5A1.5 1.5 0 0 1 5.5 4h9A1.5 1.5 0 0 1 16 5.5v6a1.5 1.5 0 0 1-1.5 1.5H9l-3.5 3v-3A1.5 1.5 0 0 1 4 11.5z"]); +var BotIcon = lineIcon(["M6 7.5h8a2 2 0 0 1 2 2V14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9.5a2 2 0 0 1 2-2z", "M10 4.5v3", "M10 3.5h.01", "M7.75 11.5h.01M12.25 11.5h.01"]); +var TerminalIcon = lineIcon(["M4.5 4.5h11a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14V6a1.5 1.5 0 0 1 1.5-1.5z", "M6.5 8.25L8.75 10 6.5 11.75", "M10.5 12h3"]); -+var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); -+var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; + var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); + var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; +function FlyBotMark({ size = 24 }) { + return (0, import_react5.createElement)( + "svg", @@ -4711,964 +4007,671 @@ diff --git a/client.js b/client.js + (0, import_react5.createElement)("circle", { className: "bt-flybot-body", cx: 256, cy: 100, r: 22 }), + (0, import_react5.createElement)("rect", { className: "bt-flybot-body", x: 248, y: 104, width: 16, height: 34 }) + ); - } --var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g; --var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g; --function sheetClassNames(sheet) { -- const names = []; -- const visit = (rules) => { -- for (const rule of rules) { -- if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]); -- if (rule.cssRules) visit(rule.cssRules); -+ -+// src/client/questions.js ++} + + // src/client/questions.js +-var import_react4 = require("react"); +var import_react6 = require("react"); -+var keyLetter = (index) => String.fromCharCode(65 + index); -+var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); -+var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; -+function useOptionKeys(count, onPick, ownRef) { + var keyLetter = (index) => String.fromCharCode(65 + index); + var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); + var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; + function useOptionKeys(count, onPick, ownRef) { +- const pick2 = (0, import_react4.useRef)(onPick); + const pick2 = (0, import_react6.useRef)(onPick); -+ pick2.current = onPick; + pick2.current = onPick; +- (0, import_react4.useEffect)(() => { + (0, import_react6.useEffect)(() => { -+ if (count === 0) return void 0; -+ const onKey = (event) => { -+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; -+ if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; -+ if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; -+ const index = event.key.toUpperCase().charCodeAt(0) - 65; -+ if (index >= 0 && index < count) { -+ event.preventDefault(); -+ pick2.current(index); -+ } else if (index === count && ownRef.current) { -+ event.preventDefault(); -+ ownRef.current.focus(); -+ } -+ }; -+ document.addEventListener("keydown", onKey, true); -+ return () => document.removeEventListener("keydown", onKey, true); -+ }, [count, ownRef]); -+} -+function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { + if (count === 0) return void 0; + const onKey = (event) => { + if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; +@@ -3146,10 +3146,10 @@ + }, [count, ownRef]); + } + function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { +- const [own, setOwn] = (0, import_react4.useState)(""); +- const [picked, setPicked] = (0, import_react4.useState)([]); +- const ownRef = (0, import_react4.useRef)(null); +- const titleId = (0, import_react4.useId)(); + const [own, setOwn] = (0, import_react6.useState)(""); + const [picked, setPicked] = (0, import_react6.useState)([]); + const ownRef = (0, import_react6.useRef)(null); + const titleId = (0, import_react6.useId)(); -+ const options = optionsOf(question); -+ const multi = question.multiSelect === true; -+ const custom = own.trim(); -+ const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; -+ const choose = (label) => { -+ if (multi) { -+ setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); -+ return; - } -+ onAnswer({ selected: [label] }); + const options = optionsOf(question); + const multi = question.multiSelect === true; + const custom = own.trim(); +@@ -3167,7 +3167,7 @@ }; -- try { -- visit(sheet.cssRules); -- } catch { -+ const submit = () => { -+ if (!canSubmit) return; -+ onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); -+ }; -+ useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); -+ const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; + useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); + const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; +- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { + const field = ownAnswer ? (0, import_react6.createElement)("textarea", { -+ ref: ownRef, -+ rows: 1, -+ value: own, -+ autoComplete: "off", -+ spellCheck: false, -+ className: "bt-q-input", -+ "aria-label": t("Your own answer"), -+ placeholder: t("Type your own answer"), -+ "aria-keyshortcuts": ownLetter?.toLowerCase(), -+ onChange: (event) => setOwn(event.target.value), -+ onKeyDown: (event) => { -+ if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; -+ event.preventDefault(); -+ submit(); -+ } -+ }) : null; -+ const rows = options.map((option, optionIndex) => { -+ const selected = multi && picked.includes(option.label); + ref: ownRef, + rows: 1, + value: own, +@@ -3186,7 +3186,7 @@ + }) : null; + const rows = options.map((option, optionIndex) => { + const selected = multi && picked.includes(option.label); +- return (0, import_react4.createElement)( + return (0, import_react6.createElement)( -+ "button", -+ { -+ key: option.label, -+ type: "button", -+ className: "bt-q-opt", -+ "aria-pressed": multi ? selected : void 0, -+ "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), -+ onClick: () => choose(option.label) -+ }, + "button", + { + key: option.label, +@@ -3196,36 +3196,36 @@ + "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), + onClick: () => choose(option.label) + }, +- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), +- (0, import_react4.createElement)( + (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), + (0, import_react6.createElement)( -+ "span", -+ { className: "bt-q-body" }, + "span", + { className: "bt-q-body" }, +- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), +- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null + (0, import_react6.createElement)("span", { className: "bt-q-label" }, option.label), + option.description ? (0, import_react6.createElement)("span", { className: "bt-q-desc" }, option.description) : null -+ ), + ), +- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null + selected && !raised ? (0, import_react6.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react6.createElement)(CheckIcon)) : null -+ ); -+ }); -+ if (raised && field) { + ); + }); + if (raised && field) { +- rows.push((0, import_react4.createElement)( + rows.push((0, import_react6.createElement)( -+ "label", -+ { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, + "label", + { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, +- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) + ownLetter ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, + (0, import_react6.createElement)("span", { className: "bt-q-body" }, field) -+ )); -+ } -+ let foot = null; -+ if (raised && multi) { + )); + } + let foot = null; + if (raised && multi) { +- foot = (0, import_react4.createElement)( + foot = (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q-foot" }, + "div", + { className: "bt-q-foot" }, +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), +- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) + (0, import_react6.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), + (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) -+ ); -+ } else if ((raised || multi) && canSubmit) { + ); + } else if ((raised || multi) && canSubmit) { +- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); + foot = (0, import_react6.createElement)("div", { className: "bt-q-foot" }, (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); } -- return names; +- return (0, import_react4.createElement)( + return (0, import_react6.createElement)( -+ "form", -+ { -+ className: raised ? "bt-q bt-q-raised" : "bt-q", -+ "aria-labelledby": titleId, -+ onSubmit: (event) => { -+ event.preventDefault(); -+ submit(); -+ } -+ }, + "form", + { + className: raised ? "bt-q bt-q-raised" : "bt-q", +@@ -3235,24 +3235,24 @@ + submit(); + } + }, +- (0, import_react4.createElement)( + (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q-head" }, + "div", + { className: "bt-q-head" }, +- (0, import_react4.createElement)( + (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q-text" }, + "div", + { className: "bt-q-text" }, +- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), +- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, +- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null + (0, import_react6.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), + question.detail ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, + raised && multi ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null -+ ), + ), +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) + (0, import_react6.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react6.createElement)(CloseIcon)) -+ ), + ), +- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, +- field && !raised ? (0, import_react4.createElement)( + rows.length > 0 ? (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, + field && !raised ? (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q-custom" }, + "div", + { className: "bt-q-custom" }, +- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field), +- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null + (0, import_react6.createElement)("label", { className: "bt-q-field" }, field), + !multi && canSubmit ? (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null -+ ) : null, -+ foot -+ ); - } --function compileFragmentSelectors(text, names) { -- const compiled = /* @__PURE__ */ new Map(); -- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { -- if (!compiled.has(fragment)) { -- const exact = []; -- for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); -- compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole); -- } -- return compiled.get(fragment); -- }); -+function SettledQuestion({ question, answer }) { -+ const options = optionsOf(question); -+ const prompt = question?.question ?? ""; -+ const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; -+ if (chosen.length === 0) { + ) : null, + foot + ); +@@ -3262,29 +3262,29 @@ + const prompt = question?.question ?? ""; + const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; + if (chosen.length === 0) { +- return (0, import_react4.createElement)( + return (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, + "div", + { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, +- (0, import_react4.createElement)( + (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q-head" }, + "div", + { className: "bt-q-head" }, +- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), +- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) + (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt)), + (0, import_react6.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) -+ ) -+ ); -+ } + ) + ); + } +- return (0, import_react4.createElement)( + return (0, import_react6.createElement)( -+ "div", -+ { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, + "div", + { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, +- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, +- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { + prompt ? (0, import_react6.createElement)("div", { className: "bt-q-head" }, (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt))) : null, + (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { -+ const optionIndex = options.findIndex((option) => option.label === label); + const optionIndex = options.findIndex((option) => option.label === label); +- return (0, import_react4.createElement)( + return (0, import_react6.createElement)( -+ "div", -+ { key: `${label}-${index}`, className: "bt-q-opt" }, + "div", + { key: `${label}-${index}`, className: "bt-q-opt" }, +- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), +- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) + optionIndex >= 0 ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, + (0, import_react6.createElement)("span", { className: "bt-q-body" }, (0, import_react6.createElement)("span", { className: "bt-q-label" }, label)), + (0, import_react6.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react6.createElement)(CheckIcon)) -+ ); -+ })) -+ ); - } --function installCompiledStyles(text, tag) { -- const style = installStyles("", tag); -- const cache = /* @__PURE__ */ new WeakMap(); -- const render = () => { -- const names = /* @__PURE__ */ new Set(); -- for (const sheet of document.styleSheets) { -- if (sheet.ownerNode?.dataset?.dshBot) continue; -- let list = cache.get(sheet); -- if (!list) { -- list = sheetClassNames(sheet); -- cache.set(sheet, list); -- } -- for (const name of list) names.add(name); -+function QuestionCard({ matched }) { -+ const pending = matched; -+ const questions = pending.questions ?? []; + ); + })) + ); +@@ -3292,8 +3292,8 @@ + function QuestionCard({ matched }) { + const pending = matched; + const questions = pending.questions ?? []; +- const [index, setIndex] = (0, import_react4.useState)(0); +- const [answers, setAnswers] = (0, import_react4.useState)([]); + const [index, setIndex] = (0, import_react6.useState)(0); + const [answers, setAnswers] = (0, import_react6.useState)([]); -+ const question = questions[index]; -+ if (question === void 0) return null; -+ const onAnswer = (answer) => { -+ const next = [...answers, { id: question.id, ...answer }]; -+ if (index + 1 < questions.length) { -+ setAnswers(next); -+ setIndex(index + 1); -+ return; + const question = questions[index]; + if (question === void 0) return null; + const onAnswer = (answer) => { +@@ -3305,10 +3305,10 @@ } -- const next = compileFragmentSelectors(text, names); -- if (style.textContent !== next) style.textContent = next; -- }; -- render(); -- let timer; -- const schedule = () => { -- clearTimeout(timer); -- timer = setTimeout(render, 120); -- }; -- const onLoad = (event) => { -- if (event.target instanceof HTMLLinkElement) schedule(); -- }; -- const observer = new MutationObserver(schedule); -- observer.observe(document.head, { childList: true }); -- document.addEventListener("load", onLoad, true); -- return () => { -- observer.disconnect(); -- document.removeEventListener("load", onLoad, true); -- clearTimeout(timer); -- style.remove(); -+ pending.answer({ answers: next }); + pending.answer({ answers: next }); }; +- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); + return (0, import_react6.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); } -- --// src/client/mark.js --var import_react2 = require("react"); --var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; --var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; --var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; --function useGaze(ref, enabled) { -- (0, import_react2.useEffect)(() => { -- if (!enabled) return void 0; -- let frame = 0; -- let point = null; -- const place = () => { -- frame = 0; -- const node = ref.current; -- if (!node || !point) return; -- const box = node.getBoundingClientRect(); -- const clamp = (value) => Math.max(-1, Math.min(1, value)); -- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); -- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); -- }; -- const onMove = (event) => { -- point = { x: event.clientX, y: event.clientY }; -- if (!frame) frame = requestAnimationFrame(place); -- }; -- const onLeave = () => { -- point = null; -- ref.current?.style.setProperty("--bt-gx", "0"); -- ref.current?.style.setProperty("--bt-gy", "0"); -- }; -- window.addEventListener("pointermove", onMove, { passive: true }); -- document.documentElement.addEventListener("pointerleave", onLeave); -- return () => { -- cancelAnimationFrame(frame); -- window.removeEventListener("pointermove", onMove); -- document.documentElement.removeEventListener("pointerleave", onLeave); -- }; -- }, [enabled]); --} --var FIDGETS = ["hop", "sway", "stretch", "look"]; --function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) { -- const look = given ?? lookOf(bot); -- const ref = (0, import_react2.useRef)(null); -- const maskId = (0, import_react2.useRef)(""); -- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`; -- const [poked, setPoked] = (0, import_react2.useState)(false); -- useGaze(ref, gaze); -- const common = { -- ref, -- className: poked ? "bt-mark bt-mark-poke" : "bt-mark", -- "data-shape": look.image ? "image" : shapeId(look.shape), -- "data-state": STATES.includes(state) ? state : "idle", -- "data-live": live ? "" : void 0, -- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0, -- "data-gaze": gaze ? "" : void 0, -- "data-poke": pokeable ? "" : void 0, -- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} }, -- role: label ? "img" : void 0, -- "aria-label": label, -- "aria-hidden": label ? void 0 : true, -- onClick: pokeable ? () => setPoked(true) : void 0, -- onAnimationEnd: poked ? (event) => { -- if (/^bt-poke/.test(event.animationName)) setPoked(false); -- } : void 0 -+function PendingQuestion({ sessionId, question, actions, inline }) { + function PendingQuestion({ sessionId, question, actions, inline }) { +- const [answered, setAnswered] = (0, import_react4.useState)(null); + const [answered, setAnswered] = (0, import_react6.useState)(null); -+ if (answered === question.id) return null; -+ const onAnswer = (answer) => { -+ setAnswered(question.id); -+ void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); + if (answered === question.id) return null; + const onAnswer = (answer) => { + setAnswered(question.id); +@@ -3318,10 +3318,10 @@ + setAnswered(question.id); + void actions.dismissQuestion(sessionId); }; -- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false })); -- const box = shapeOf(look.shape).size; -- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } })); --} --function StarIcon({ size = 8 }) { -- return (0, import_react2.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true }, -- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" }) -+ const onDismiss = () => { -+ setAnswered(question.id); -+ void actions.dismissQuestion(sessionId); -+ }; +- return (0, import_react4.createElement)( + return (0, import_react6.createElement)( -+ "div", -+ { className: inline ? "bt-q-inline" : "bt-q-dock" }, + "div", + { className: inline ? "bt-q-inline" : "bt-q-dock" }, +- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) + (0, import_react6.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) ); } --function 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 QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { -+ const question = useRoster((value) => value.questions?.[sessionId]); -+ const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true; -+ const loading = useSession?.(historyLoading) === true; -+ if (question === void 0 || question.callId && (drawn || loading)) return null; + function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { +@@ -3329,16 +3329,16 @@ + const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true; + const loading = useSession?.(historyLoading) === true; + if (question === void 0 || question.callId && (drawn || loading)) return null; +- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); + return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); } --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 AnsweredCell({ node }) { -+ const data = node.data ?? {}; -+ const questions = Array.isArray(data.questions) ? data.questions : []; -+ if (questions.length === 0) { + function AnsweredCell({ node }) { + const data = node.data ?? {}; + const questions = Array.isArray(data.questions) ? data.questions : []; + if (questions.length === 0) { +- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; + return data.text ? (0, import_react6.createElement)("div", { className: "bt-urow" }, (0, import_react6.createElement)("div", { className: "bt-ububble" }, data.text)) : null; -+ } -+ const answers = Array.isArray(data.answers) ? data.answers : []; + } + const answers = Array.isArray(data.answers) ? data.answers : []; +- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { + return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react6.createElement)(SettledQuestion, { -+ key: question.id, -+ question, -+ answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -+ }))); + key: question.id, + question, + answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } +@@ -3346,37 +3346,37 @@ } --function useSettled(state) { -- const prev = (0, import_react2.useRef)(state); -- const [settled, setSettled] = (0, import_react2.useState)(false); -- (0, import_react2.useEffect)(() => { -- const was = prev.current; -- prev.current = state; -- if (state !== "idle") { -- setSettled(false); -- return void 0; -- } -- if (was === "idle" || was === "alert") return void 0; -- setSettled(true); -- const timer = setTimeout(() => setSettled(false), 1800); -- return () => clearTimeout(timer); -- }, [state]); -- return settled; -+ -+// src/client/secret-card.js + + // src/client/secret-card.js +-var import_react5 = require("react"); +var import_react7 = require("react"); -+var MIN_VALUE = 4; -+var NONE = {}; + var MIN_VALUE = 4; + var NONE = {}; +-var KeyIcon = () => (0, import_react5.createElement)( +var KeyIcon = () => (0, import_react7.createElement)( -+ "svg", -+ { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "svg", + { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), +- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) + (0, import_react7.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), + (0, import_react7.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) -+); + ); +-var LockIcon = () => (0, import_react5.createElement)( +var LockIcon = () => (0, import_react7.createElement)( -+ "svg", -+ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "svg", + { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), +- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) + (0, import_react7.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), + (0, import_react7.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) -+); + ); +-var WarnIcon = () => (0, import_react5.createElement)( +var WarnIcon = () => (0, import_react7.createElement)( -+ "svg", -+ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + "svg", + { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), +- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) + (0, import_react7.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), + (0, import_react7.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) -+); -+var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); -+var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); -+var scopeLabel = (scope, roster) => { + ); + var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); + var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); + var scopeLabel = (scope, roster) => { +- if (scope === "all") return t("All Bots"); + if (scope === "all") return t("All my Bots"); -+ const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); -+ return names.length === 0 ? t("No Bot") : names.join(t(" and ")); -+}; -+var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; -+function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { + const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); + return names.length === 0 ? t("No Bot") : names.join(t(" and ")); + }; + var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; + function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { +- return (0, import_react5.createElement)("input", { + return (0, import_react7.createElement)("input", { -+ className: "bt-secret-input", -+ type: MASK_BY_CSS ? "text" : "password", -+ "data-masked": MASK_BY_CSS ? "" : void 0, -+ value, -+ autoFocus, -+ autoComplete: MASK_BY_CSS ? "off" : "new-password", -+ autoCorrect: "off", -+ autoCapitalize: "off", -+ spellCheck: false, -+ "data-1p-ignore": "", -+ "data-lpignore": "true", -+ "data-bwignore": "", -+ "data-form-type": "other", -+ "aria-label": label, -+ placeholder: t("Paste or type the key"), -+ onChange: (event) => onChange(event.target.value), -+ onKeyDown: (event) => { -+ if (event.key !== "Enter" || event.nativeEvent.isComposing) return; -+ event.preventDefault(); -+ onEnter?.(); -+ } -+ }); + className: "bt-secret-input", + type: MASK_BY_CSS ? "text" : "password", + "data-masked": MASK_BY_CSS ? "" : void 0, +@@ -3401,42 +3401,42 @@ + }); } --function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { -- const settled = useSettled(state); -- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size }); -- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null; -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } }, -- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }), -- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null, -- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null, -- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null, -- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null -+function ScopeSwitch({ value, onChange, bot }) { + function ScopeSwitch({ value, onChange, bot }) { +- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; +- return (0, import_react5.createElement)( + const options = [["all", t("All my Bots")], ["bot", t("Only {name}", { name: bot.name })]]; + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, + "div", + { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, +- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), +- options.map(([id, text]) => (0, import_react5.createElement)("button", { + (0, import_react7.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), -+ options.map(([id, text]) => (0, import_react7.createElement)("button", { -+ key: id, -+ type: "button", -+ role: "radio", -+ "aria-checked": value === id, -+ className: "bt-secret-seg", -+ onClick: () => onChange(id) -+ }, text)) ++ options.map(([id, text2]) => (0, import_react7.createElement)("button", { + key: id, + type: "button", + role: "radio", + "aria-checked": value === id, + className: "bt-secret-seg", + onClick: () => onChange(id) +- }, text)) ++ }, text2)) ); } --function clusterSeats(count, frame) { -- if (count === 1) return [[0, 0, frame]]; -- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; -- if (count === 3) { -- const size = frame * 5 / 9; -- const rest = frame - size; -- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; -- } -- const half = frame / 2; -- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; --} --function 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 SecretHead({ bot, title, detail, name, onClose }) { + function SecretHead({ bot, title, detail, name, onClose }) { +- return (0, import_react5.createElement)( + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-q-head bt-secret-head" }, + "div", + { className: "bt-q-head bt-secret-head" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))), +- (0, import_react5.createElement)( + (0, import_react7.createElement)("span", { className: "bt-secret-who" }, (0, import_react7.createElement)(BotMark, { bot, size: 28 }), (0, import_react7.createElement)("span", { className: "bt-secret-lock" }, (0, import_react7.createElement)(LockIcon))), + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-q-text" }, + "div", + { className: "bt-q-text" }, +- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), +- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") + (0, import_react7.createElement)("p", { className: "bt-q-title" }, title), + (0, import_react7.createElement)("div", { className: "bt-q-detail" }, (0, import_react7.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") -+ ), + ), +- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null + onClose ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react7.createElement)(CloseIcon)) : null ); } -- --// src/client/icons.js --var import_react3 = require("react"); --function PlusIcon() { -- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); --} --function SearchIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); -+function SecretCard({ bot, request, roster, actions }) { + function SecretCard({ bot, request, roster, actions }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [scope, setScope] = (0, import_react5.useState)("all"); +- const [replacing, setReplacing] = (0, import_react5.useState)(false); +- const [busy, setBusy] = (0, import_react5.useState)(false); +- const [error, setError] = (0, import_react5.useState)(""); +- const noteId = (0, import_react5.useId)(); + const [value, setValue] = (0, import_react7.useState)(""); + const [scope, setScope] = (0, import_react7.useState)("all"); + const [replacing, setReplacing] = (0, import_react7.useState)(false); + const [busy, setBusy] = (0, import_react7.useState)(false); + const [error, setError] = (0, import_react7.useState)(""); + const noteId = (0, import_react7.useId)(); -+ const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); -+ const run = (promise) => { -+ setBusy(true); -+ setError(""); -+ return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); -+ }; -+ const cancel = () => run(actions.secretCancel(request.id)); -+ const long = value.trim().length >= MIN_VALUE; -+ const save = () => { -+ if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); -+ }; -+ const allow = () => { -+ if (!busy) void run(actions.secretAllow(request.id)); -+ }; -+ const filling = request.kind === "fill" || replacing; + const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); + const run = (promise) => { + setBusy(true); +@@ -3452,73 +3452,73 @@ + if (!busy) void run(actions.secretAllow(request.id)); + }; + const filling = request.kind === "fill" || replacing; +- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; +- return (0, import_react5.createElement)( + const warn = insecure() ? (0, import_react7.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react7.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, + "div", + { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, +- (0, import_react5.createElement)(SecretHead, { + (0, import_react7.createElement)(SecretHead, { -+ bot, -+ title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), -+ detail: request.purpose, -+ name: request.name, -+ onClose: busy ? void 0 : cancel -+ }), + bot, + title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), + detail: request.purpose, + name: request.name, + onClose: busy ? void 0 : cancel + }), +- request.kind === "allow" ? (0, import_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, +- filling ? (0, import_react5.createElement)( + request.kind === "allow" ? (0, import_react7.createElement)("div", { className: "bt-secret-saved" }, (0, import_react7.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, + filling ? (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-body" }, + "div", + { className: "bt-secret-body" }, +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), +- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), + request.kind === "fill" ? (0, import_react7.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null -+ ) : null, + ) : null, +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-note", id: noteId }, -+ t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), + "div", + { className: "bt-secret-note", id: noteId }, + t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), +- (0, import_react5.createElement)("code", null, `$DSH_SECRET_${request.name}`), + (0, import_react7.createElement)("code", null, `$DSH_SECRET_${request.name}`), -+ ".", -+ filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" -+ ), -+ warn, + ".", + filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" + ), + warn, +- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, +- (0, import_react5.createElement)( + error ? (0, import_react7.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-q-foot" }, + "div", + { className: "bt-q-foot" }, +- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, +- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), +- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) + request.kind === "allow" && !replacing ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, + (0, import_react7.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), + filling ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) -+ ) -+ ); + ) + ); } --function PeopleIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), -- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }), -- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) -+function SettledSecret({ bot, request, roster }) { -+ const canceled = request.status === "canceled"; -+ const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); + function SettledSecret({ bot, request, roster }) { + const canceled = request.status === "canceled"; +- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); +- return (0, import_react5.createElement)( ++ const text2 = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, + "div", +- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, +- (0, import_react5.createElement)( ++ { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text2, "data-status": request.status }, + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-q-head" }, + "div", + { className: "bt-q-head" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })), +- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)( + (0, import_react7.createElement)("span", { className: "bt-secret-who" }, (0, import_react7.createElement)(BotMark, { bot, size: 22 })), + (0, import_react7.createElement)("div", { className: "bt-q-text" }, (0, import_react7.createElement)( -+ "p", -+ { className: "bt-q-title" }, -+ text, + "p", + { className: "bt-q-title" }, +- text, +- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) ++ text2, + canceled ? null : (0, import_react7.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) -+ )), + )), +- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) + canceled ? (0, import_react7.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react7.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react7.createElement)(CheckIcon)) -+ ) + ) ); } --function ArrowUpIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); -+function RequestView({ bot, request, roster, actions }) { + function RequestView({ bot, request, roster, actions }) { +- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster }); + return request.status === "pending" ? (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react7.createElement)(SettledSecret, { bot, request, roster }); } --function MicIcon({ size = 16 }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), -- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) -- ); -+function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { -+ const bot = roster.byId[sessionId]; -+ if (!bot) return null; -+ const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); + function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { + const bot = roster.byId[sessionId]; + if (!bot) return null; + const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); +- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions })); + if (request) return (0, import_react7.createElement)("div", { className: "bt-q-inline" }, (0, import_react7.createElement)(RequestView, { bot, request, roster, actions })); -+ if (done && /already saved/.test(contentText(content))) { + if (done && /already saved/.test(contentText(content))) { +- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); + return (0, import_react7.createElement)("div", { className: "bt-event" }, (0, import_react7.createElement)("span", { className: "bt-secret-event" }, (0, import_react7.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); -+ } -+ return null; + } + return null; } --function WaveIcon({ size = 16 }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) -- ); -+function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { -+ const roster = useRoster((value) => value); -+ const shown = useCards?.((state) => state[sessionId]) ?? NONE; -+ const loading = useSession?.(historyLoading) === true; -+ const bot = roster.byId[sessionId]; -+ if (!bot || currentPart(bot) !== sessionId) return null; -+ const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); -+ const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); -+ if (waiting.length === 0) return null; +@@ -3531,44 +3531,46 @@ + const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); + const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); + if (waiting.length === 0) return null; +- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); + return (0, import_react7.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); } --function ShareIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) +var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some"; -+function ScopeEditor({ secret, roster, onSave }) { + function ScopeEditor({ secret, roster, onSave }) { +- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some"); +- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope); + const [mode, setMode] = (0, import_react7.useState)(scopeMode(secret.scope)); + const [picked, setPicked] = (0, import_react7.useState)(secret.scope === "all" ? [] : secret.scope); -+ const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); + const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join(); +- return (0, import_react5.createElement)( + const next = mode === "all" ? "all" : mode === "none" ? [] : picked; + const same2 = next === "all" ? secret.scope === "all" : secret.scope !== "all" && [...next].sort().join() === [...secret.scope].sort().join(); + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-edit-block" }, + "div", + { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), +- (0, import_react5.createElement)( + (0, import_react7.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, -+ [["all", t("All my Bots")], ["some", t("Chosen Bots")], ["none", t("No Bot")]].map(([id, text]) => (0, import_react7.createElement)( -+ "label", -+ { key: id, className: "bt-secret-radio" }, + "div", + { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, +- [["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)( ++ [["all", t("All my Bots")], ["some", t("Chosen Bots")], ["none", t("No Bot")]].map(([id, text2]) => (0, import_react7.createElement)( + "label", + { key: id, className: "bt-secret-radio" }, +- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), +- text + (0, import_react7.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), -+ text -+ )) -+ ), ++ text2 + )) + ), +- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { + mode === "some" ? (0, import_react7.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react7.createElement)("button", { -+ key: bot.id, -+ type: "button", -+ className: "bt-secret-bot", -+ "aria-pressed": picked.includes(bot.id), -+ onClick: () => toggle(bot.id) + key: bot.id, + type: "button", + className: "bt-secret-bot", + "aria-pressed": picked.includes(bot.id), + onClick: () => toggle(bot.id) +- }, (0, import_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null, +- (0, import_react5.createElement)( + }, (0, import_react7.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react7.createElement)(CheckIcon) : null))) : null, + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-row-actions" }, + "div", + { className: "bt-secret-row-actions" }, +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access")) + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(next) }, t("Save access")) -+ ) + ) ); } --function CloseIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); --} --var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); --var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); --var lineIcon = (paths) => function LineIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) -+function ValueEditor({ secret, onSave }) { + function ValueEditor({ secret, onSave }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [replaced, setReplaced] = (0, import_react5.useState)(false); + const [value, setValue] = (0, import_react7.useState)(""); + const [replaced, setReplaced] = (0, import_react7.useState)(false); -+ const long = value.trim().length >= MIN_VALUE; -+ const save = () => { -+ if (!long) return; -+ void Promise.resolve(onSave(value)).then((ok) => { -+ if (ok) { -+ setValue(""); -+ setReplaced(true); -+ } -+ }); -+ }; -+ const edit = (next) => { -+ setValue(next); -+ setReplaced(false); -+ }; + const long = value.trim().length >= MIN_VALUE; + const save = () => { + if (!long) return; +@@ -3583,66 +3583,66 @@ + setValue(next); + setReplaced(false); + }; +- return (0, import_react5.createElement)( + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-edit-block" }, + "div", + { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), +- (0, import_react5.createElement)( + (0, import_react7.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-inline" }, + "div", + { className: "bt-secret-inline" }, +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) -+ ), + ), +- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null + value !== "" && !long ? (0, import_react7.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react7.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react7.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null ); --}; --var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; --function CloudDownloadIcon({ done = false }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("path", { d: CLOUD }), -- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) -+} -+function SecretRow({ secret, roster, actions, run, open, onToggle }) { + } + function SecretRow({ secret, roster, actions, run, open, onToggle }) { +- const [confirm, setConfirm] = (0, import_react5.useState)(false); +- (0, import_react5.useEffect)(() => { + const [confirm, setConfirm] = (0, import_react7.useState)(false); + (0, import_react7.useEffect)(() => { -+ if (!open) setConfirm(false); -+ }, [open]); -+ const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); -+ const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); -+ const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); + if (!open) setConfirm(false); + }, [open]); + const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); + const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); + const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); +- return (0, import_react5.createElement)( + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-item", "data-open": open ? "" : void 0 }, + "div", + { className: "bt-secret-item", "data-open": open ? "" : void 0 }, +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, + "button", + { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ "span", -+ { className: "bt-set-copy" }, + "span", + { className: "bt-set-copy" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), +- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta) + (0, import_react7.createElement)("span", { className: "bt-secret-title" }, (0, import_react7.createElement)(KeyIcon), (0, import_react7.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react7.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), + (0, import_react7.createElement)("span", { className: "bt-set-hint" }, meta) -+ ), + ), +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ "span", -+ { className: "bt-set-control" }, + "span", + { className: "bt-set-control" }, +- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) + secret.scope === "all" ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("All my Bots")) : allowed.length === 0 ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react7.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react7.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) -+ ) -+ ), + ) + ), +- open ? (0, import_react5.createElement)( + open ? (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-edit" }, + "div", + { className: "bt-secret-edit" }, +- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), +- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), +- (0, import_react5.createElement)( + (0, import_react7.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), + (0, import_react7.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-row-actions" }, -+ confirm ? [ + "div", + { className: "bt-secret-row-actions" }, + confirm ? [ +- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), +- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), +- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) +- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) + (0, import_react7.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), + (0, import_react7.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), + (0, import_react7.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) + ] : (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react7.createElement)(TrashIcon), t("Delete")) -+ ) -+ ) : null + ) + ) : null ); } --var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); --var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]); --var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]); --var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]); --var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]); --var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]); --var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); --var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); --var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); --var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); --var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); --var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); --var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]); --var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); --var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); --function PinMarkIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- (0, import_react3.createElement)( -- "g", -- { transform: "rotate(45 10 10)" }, -- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), -- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) -+function AddSecret({ actions, run, onDone }) { + function AddSecret({ actions, run, onDone }) { +- const [name, setName] = (0, import_react5.useState)(""); +- const [purpose, setPurpose] = (0, import_react5.useState)(""); +- const [value, setValue] = (0, import_react5.useState)(""); + const [name, setName] = (0, import_react7.useState)(""); + const [purpose, setPurpose] = (0, import_react7.useState)(""); + const [value, setValue] = (0, import_react7.useState)(""); -+ const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; -+ const save = () => { -+ if (!ready) return; -+ void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { -+ if (ok) { -+ setValue(""); -+ onDone(); -+ } -+ }); -+ }; + const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; + const save = () => { + if (!ready) return; +@@ -3653,43 +3653,43 @@ + } + }); + }; +- return (0, import_react5.createElement)( + return (0, import_react7.createElement)( -+ "div", -+ { className: "bt-set-row bt-set-paste bt-secret-add" }, + "div", + { className: "bt-set-row bt-set-paste bt-secret-add" }, +- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), +- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), +- (0, import_react5.createElement)( + (0, import_react7.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), + (0, import_react7.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), + (0, import_react7.createElement)( -+ "div", -+ { className: "bt-secret-row-actions" }, + "div", + { className: "bt-secret-row-actions" }, +- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) + (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all your Bots; change who may use it afterwards.")), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) ) ); } --var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); --var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); --var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); --var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); --var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); --var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); --var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); --var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); --var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); --var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); --var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); --var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); --var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; -+function SecretsPage({ roster, actions, run, Section }) { + function SecretsPage({ roster, actions, run, Section }) { +- const [open, setOpen] = (0, import_react5.useState)(null); +- const [adding, setAdding] = (0, import_react5.useState)(false); + const [open, setOpen] = (0, import_react7.useState)(null); + const [adding, setAdding] = (0, import_react7.useState)(false); -+ const secrets = roster.secrets ?? []; -+ return [ + const secrets = roster.secrets ?? []; + return [ +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ "p", -+ { key: "about", className: "bt-secret-about" }, -+ t("Keys your Bots asked for. Bots never see the values: they use them as "), + "p", + { key: "about", className: "bt-secret-about" }, + t("Keys your Bots asked for. Bots never see the values: they use them as "), +- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), + (0, import_react7.createElement)("code", null, "$DSH_SECRET_NAME"), -+ t(" in their shell, and every value is replaced by "), + t(" in their shell, and every value is replaced by "), +- (0, import_react5.createElement)("code", null, "[secret:NAME]"), + (0, import_react7.createElement)("code", null, "[secret:NAME]"), -+ t(" before anything goes to a model.") -+ ), + t(" before anything goes to a model.") + ), +- (0, import_react5.createElement)( + (0, import_react7.createElement)( -+ Section, -+ { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, + Section, + { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, +- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, +- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), +- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null + secrets.length === 0 && !adding ? (0, import_react7.createElement)("div", { className: "bt-set-row" }, (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, + secrets.map((secret) => (0, import_react7.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), + adding ? (0, import_react7.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null -+ ), + ), +- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) + adding ? null : (0, import_react7.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) -+ ]; -+} -+var SECRET_CSS = ` -+.bt-secret-head{align-items:center;gap:10px} -+.bt-secret-who{position:relative;flex:none;display:inline-flex} -+.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} -+.bt-secret-lock svg{width:11px;height:11px} -+.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} -+.bt-secret-body{display:flex;flex-direction:column;gap:8px} -+.bt-secret-field{align-items:center;min-height:36px} -+.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} -+.bt-secret-input[data-masked]{-webkit-text-security:disc} -+.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} -+.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} -+.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} -+.bt-secret-glide[data-at="1"]{transform:translateX(100%)} -+.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} -+.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} -+.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} -+/* A key card asks the user to act, so the process group carrying it stays open in every -+ work-details mode (same override as .bt-group-replies in styles.js). */ -+[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} -+[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} -+.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} -+.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} -+@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} -+.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} -+.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} -+.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} -+.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} -+.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} -+.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} -+.bt-secret-done .bt-q-head{align-items:center} -+.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} -+.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} -+.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} -+.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} -+@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} -+@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} -+.bt-secret-event{display:inline-flex;align-items:center;gap:5px} -+.bt-secret-dock{flex-direction:column;align-items:center} -+.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} -+.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} -+.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} -+.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} -+.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} -+.bt-secret-faces{display:inline-flex} -+.bt-secret-faces>*+*{margin-left:-6px} -+.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} -+.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} -+.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} -+.bt-secret-radios{display:flex;gap:16px;font-size:13px} -+.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} -+.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} -+.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} -+.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} -+.bt-secret-bot svg{width:13px;height:13px} -+.bt-secret-inline{display:flex;gap:8px;align-items:center} -+.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} -+.bt-secret-row-actions .bt-set-hint{flex:1} -+.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} -+.bt-secret-danger svg{width:13px;height:13px} -+.bt-secret-add .bt-input{height:34px} -+@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} -+`; + ]; + } + var SECRET_CSS = ` +@@ -3756,6 +3756,144 @@ + @media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} + `; --// src/client/questions.js --var import_react4 = require("react"); --var keyLetter = (index) => String.fromCharCode(65 + index); --var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); --var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; --function useOptionKeys(count, onPick, ownRef) { -- const pick2 = (0, import_react4.useRef)(onPick); -- pick2.current = onPick; -- (0, import_react4.useEffect)(() => { -- if (count === 0) return void 0; -- const onKey = (event) => { -- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; -- if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; -- if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; -- const index = event.key.toUpperCase().charCodeAt(0) - 65; -- if (index >= 0 && index < count) { -- event.preventDefault(); -- pick2.current(index); -- } else if (index === count && ownRef.current) { -- event.preventDefault(); -- ownRef.current.focus(); -- } +// src/client/face-seats.js +function faceSeats(room2, roster, { left = false } = {}) { + const records = (room2.presentedParticipants ?? room2.participants ?? []).filter((p) => p.kind !== "human"); @@ -5681,48 +4684,8 @@ diff --git a/client.js b/client.js + person: local ? seatOwnerName(p.ownerName, p.handle) || cleanPeerName(roster.self?.displayName, "") : ownerNameOf(p), + bot: local ? local.name : remoteBotName(p, "Bot"), + mine: Boolean(local) - }; -- document.addEventListener("keydown", onKey, true); -- return () => document.removeEventListener("keydown", onKey, true); -- }, [count, ownRef]); --} --function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { -- const [own, setOwn] = (0, import_react4.useState)(""); -- const [picked, setPicked] = (0, import_react4.useState)([]); -- const ownRef = (0, import_react4.useRef)(null); -- const titleId = (0, import_react4.useId)(); -- const options = optionsOf(question); -- const multi = question.multiSelect === true; -- const custom = own.trim(); -- const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; -- const choose = (label) => { -- if (multi) { -- setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); -- return; -- } -- onAnswer({ selected: [label] }); - }; -- const submit = () => { -- if (!canSubmit) return; -- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); -- }; -- useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); -- const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; -- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { -- ref: ownRef, -- rows: 1, -- value: own, -- autoComplete: "off", -- spellCheck: false, -- className: "bt-q-input", -- "aria-label": t("Your own answer"), -- placeholder: t("Type your own answer"), -- "aria-keyshortcuts": ownLetter?.toLowerCase(), -- onChange: (event) => setOwn(event.target.value), -- onKeyDown: (event) => { -- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; -- event.preventDefault(); -- submit(); ++ }; ++ }; + const seats = records.filter((p) => p.leftSeq === void 0).map(seat); + for (const id of room2.members ?? []) { + const bot = roster.byId[id]; @@ -5759,42 +4722,9 @@ diff --git a/client.js b/client.js + if (known) { + known.seats.push(seat); + continue; - } -- }) : null; -- const rows = options.map((option, optionIndex) => { -- const selected = multi && picked.includes(option.label); -- return (0, import_react4.createElement)( -- "button", -- { -- key: option.label, -- type: "button", -- className: "bt-q-opt", -- "aria-pressed": multi ? selected : void 0, -- "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), -- onClick: () => choose(option.label) -- }, -- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), -- (0, import_react4.createElement)( -- "span", -- { className: "bt-q-body" }, -- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), -- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null -- ), -- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null -- ); -- }); -- if (raised && field) { -- rows.push((0, import_react4.createElement)( -- "label", -- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, -- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) -- )); ++ } + people2.push({ key, name: seat.person || (seat.mine ? cleanPeerName(roster.self?.displayName, "") : "") || t("Someone"), mine: seat.mine, seats: [seat] }); - } -- let foot = null; -- if (raised && multi) { -- foot = (0, import_react4.createElement)( ++ } + return [...people2.filter((person) => person.mine), ...people2.filter((person) => !person.mine)]; +} +function facePersonLabel(person) { @@ -5851,169 +4781,94 @@ diff --git a/client.js b/client.js + if (!waiting) { + const failed = held.status === "failed"; + return (0, import_react8.createElement)( - "div", -- { className: "bt-q-foot" }, -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), -- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) ++ "div", + { className: "bt-held-note", "data-state": held.status, title: failed && held.error ? held.error : void 0 }, + (0, import_react8.createElement)("span", null, held.status === "approved" ? t("Sent to {to}", { to: held.toName }) : held.status === "auto" ? t("Auto-sent to {to}", { to: held.toName }) : failed ? t("Not sent: delivery failed") : t("Not sent")), + // #968: the terminal answer comes from one predicate, not from `status === 'failed'` here and a + // different field elsewhere. A failure that can never succeed draws no button on any surface. + failed && canRetry(held) ? (0, import_react8.createElement)("button", { type: "button", className: "bt-held-retry", disabled: busy, onClick: () => decide("retry") }, t("Retry")) : null, + error ? (0, import_react8.createElement)("span", { className: "bt-flybot-approval-error", role: "alert" }, error) : null - ); -- } else if ((raised || multi) && canSubmit) { -- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); - } -- return (0, import_react4.createElement)( -- "form", -- { -- className: raised ? "bt-q bt-q-raised" : "bt-q", -- "aria-labelledby": titleId, -- onSubmit: (event) => { -- event.preventDefault(); -- submit(); -- } -- }, -- (0, import_react4.createElement)( ++ ); ++ } + const plain = (0, import_react8.createElement)( + "div", + { className: "bt-held-card", role: "region", "aria-label": t("Waiting for your approval") }, + (0, import_react8.createElement)("div", { className: "bt-held-head" }, t("Waiting for your approval")), + (0, import_react8.createElement)("div", { className: "bt-flybot-approval-origin" }, t("{from} → {to}", { from: held.from ?? t("A Bot"), to: held.toName })), + (0, import_react8.createElement)( - "div", -- { className: "bt-q-head" }, -- (0, import_react4.createElement)( -- "div", -- { className: "bt-q-text" }, -- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), -- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, -- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null -- ), -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) ++ "div", + { className: "bt-flybot-approval-actions" }, + (0, import_react8.createElement)("button", { type: "button", disabled: busy, onClick: () => decide("reject") }, t("Reject")), + (0, import_react8.createElement)("button", { type: "button", className: "bt-flybot-approve", disabled: busy, onClick: () => decide("approve") }, t("Approve")) - ), -- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, -- field && !raised ? (0, import_react4.createElement)( -- "div", -- { className: "bt-q-custom" }, -- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field), -- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null -- ) : null, -- foot ++ ), + error ? (0, import_react8.createElement)("div", { className: "bt-flybot-approval-error", role: "alert" }, error) : null + ); + return (0, import_react8.createElement)( + "div", + { className: "bt-held-actions", "data-flybot": "held-reply-actions" }, + custom ? (0, import_react8.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react8.createElement)(custom.component, { held, decide, call: call3, actions })) : plain - ); - } --function SettledQuestion({ question, answer }) { -- const options = optionsOf(question); -- const prompt = question?.question ?? ""; -- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; -- if (chosen.length === 0) { -- return (0, import_react4.createElement)( -- "div", -- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, -- (0, import_react4.createElement)( -- "div", -- { className: "bt-q-head" }, -- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), -- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) -- ) -- ); -- } -- return (0, import_react4.createElement)( -+ -+// src/client/turn-reasoning.js -+function turnReasoning(steps, current) { -+ const members = steps.length ? steps : [current]; -+ if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; -+ return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); -+} -+var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); -+var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); -+var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); ++ ); ++} + -+// src/client/cells.js + // src/client/turn-reasoning.js + function turnReasoning(steps, current) { + const members = steps.length ? steps : [current]; +@@ -3767,7 +3767,7 @@ + var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); + + // src/client/cells.js +-var import_react6 = require("react"); +var import_react9 = require("react"); -+var import_react_dom = require("react-dom"); -+var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); -+var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); -+function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) { -+ const onClick = (event) => { -+ const target = mentionTarget(event); -+ if (target === null) return; -+ event.preventDefault(); -+ event.stopPropagation(); -+ actions.openSession(target); -+ }; -+ const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; + var import_react_dom = require("react-dom"); + var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); + var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); +@@ -3780,29 +3780,29 @@ + actions.openSession(target); + }; + const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( "div", -- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, -- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, -- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { -- const optionIndex = options.findIndex((option) => option.label === label); -- return (0, import_react4.createElement)( -+ { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, -+ texts2.map((text, index) => { -+ const key = msgKey ? `${msgKey}:${index}` : void 0; -+ const picked = key ? reactions?.[index] ?? [] : []; -+ const last = index === texts2.length - 1; -+ const live = streaming && last; + { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, +- texts2.map((text, index) => { ++ texts2.map((text2, index) => { + const key = msgKey ? `${msgKey}:${index}` : void 0; + const picked = key ? reactions?.[index] ?? [] : []; + const last = index === texts2.length - 1; + const live = streaming && last; +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( "div", -- { key: `${label}-${index}`, className: "bt-q-opt" }, -- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), -- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) -+ { key: index, className: "bt-msg" }, + { key: index, className: "bt-msg" }, +- (0, import_react6.createElement)( + (0, import_react9.createElement)( -+ "div", -+ { className: "bt-msg-line" }, -+ lead && last ? lead : null, + "div", + { className: "bt-msg-line" }, + lead && last ? lead : null, +- (0, import_react6.createElement)( + (0, import_react9.createElement)( -+ "div", -+ { className: "bt-bubble" }, -+ (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) -+ ), -+ live ? null : (0, import_react9.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) -+ ), + "div", + { className: "bt-bubble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) ++ (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text2, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) + ), +- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) ++ live ? null : (0, import_react9.createElement)(MessageTools, { text: text2, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) + ), +- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { + picked.length ? (0, import_react9.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react9.createElement)("button", { -+ key: emoji, -+ type: "button", -+ className: "bt-react", -+ "aria-label": t("Remove reaction {emoji}", { emoji }), -+ onClick: () => actions.toggleReaction(msgKey, index, emoji) -+ }, emoji))) : null - ); -- })) -+ }) + key: emoji, + type: "button", + className: "bt-react", +@@ -3813,9 +3813,22 @@ + }) ); } --function QuestionCard({ matched }) { -- const pending = matched; -- const questions = pending.questions ?? []; -- const [index, setIndex] = (0, import_react4.useState)(0); -- const [answers, setAnswers] = (0, import_react4.useState)([]); -- const question = questions[index]; -- if (question === void 0) return null; -- const onAnswer = (answer) => { -- const next = [...answers, { id: question.id, ...answer }]; -- if (index + 1 < questions.length) { -- setAnswers(next); -- setIndex(index + 1); +var WAITING_OTHERS_NOTICE = "Waiting for the owner's approval"; -+var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; -+function leaveNoticeCoveredByClose(room2, text) { + var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; ++function leaveNoticeCoveredByClose(room2, text2) { + if (room2?.origin !== "face-to-face" || room2.face?.status !== "closed") return false; -+ const match = String(text ?? "").trim().match(/^(.+) left(?: the room)?$/); ++ const match = String(text2 ?? "").trim().match(/^(.+) left(?: the room)?$/); + if (!match) return false; + const who = match[1]; + for (const participant of room2.participants ?? []) { @@ -6023,110 +4878,85 @@ diff --git a/client.js b/client.js + } + return false; +} -+function useDismiss(open, close, refs) { + function useDismiss(open, close, refs) { +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ if (!open) return void 0; -+ const onDown = (event) => { -+ if (refs.some((ref) => ref.current?.contains(event.target))) return; -+ close(); -+ }; -+ const onKey = (event) => { -+ if (event.key === "Escape") { -+ event.stopPropagation(); -+ close(); -+ } -+ }; -+ window.addEventListener("mousedown", onDown); -+ window.addEventListener("keydown", onKey, true); -+ return () => { -+ window.removeEventListener("mousedown", onDown); -+ window.removeEventListener("keydown", onKey, true); -+ }; -+ }, [open]); -+} -+function MessageTools({ text, name, sessionId, react, actions }) { + if (!open) return void 0; + const onDown = (event) => { + if (refs.some((ref) => ref.current?.contains(event.target))) return; +@@ -3835,12 +3835,12 @@ + }; + }, [open]); + } +-function MessageTools({ text, name, sessionId, react, actions }) { +- const [panel, setPanel] = (0, import_react6.useState)(null); +- const [place, setPlace] = (0, import_react6.useState)(null); +- const [copied, setCopied] = (0, import_react6.useState)(false); +- const tools = (0, import_react6.useRef)(null); +- const pop = (0, import_react6.useRef)(null); ++function MessageTools({ text: text2, name, sessionId, react, actions }) { + const [panel, setPanel] = (0, import_react9.useState)(null); + const [place, setPlace] = (0, import_react9.useState)(null); + const [copied, setCopied] = (0, import_react9.useState)(false); + const tools = (0, import_react9.useRef)(null); + const pop = (0, import_react9.useRef)(null); -+ const close = () => setPanel(null); -+ useDismiss(panel !== null, close, [tools, pop]); -+ const open = (kind, event) => { -+ if (panel === kind) { -+ close(); - return; - } -- pending.answer({ answers: next }); -+ const rect = event.currentTarget.getBoundingClientRect(); -+ const below = rect.bottom + 220 < window.innerHeight; -+ setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); -+ setPanel(kind); + const close = () => setPanel(null); + useDismiss(panel !== null, close, [tools, pop]); + const open = (kind, event) => { +@@ -3854,36 +3854,36 @@ + setPanel(kind); }; -- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); --} --function PendingQuestion({ sessionId, question, actions, inline }) { -- const [answered, setAnswered] = (0, import_react4.useState)(null); -- if (answered === question.id) return null; -- const onAnswer = (answer) => { -- setAnswered(question.id); -- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); -+ const copy2 = () => { -+ void navigator.clipboard?.writeText(text).then(() => { -+ setCopied(true); -+ setTimeout(() => setCopied(false), 1e3); -+ }); + const copy2 = () => { +- void navigator.clipboard?.writeText(text).then(() => { ++ void navigator.clipboard?.writeText(text2).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 1e3); + }); }; -- const onDismiss = () => { -- setAnswered(question.id); -- void actions.dismissQuestion(sessionId); -+ const download2 = () => { -+ const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); -+ const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); -+ link.click(); -+ setTimeout(() => URL.revokeObjectURL(url), 1e3); + const download2 = () => { +- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); ++ const url = URL.createObjectURL(new Blob([text2], { type: "text/markdown" })); + const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); + link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1e3); }; -- return (0, import_react4.createElement)( +- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { + const item = (label, Icon, run) => (0, import_react9.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { -+ close(); -+ run(); + close(); + run(); +- } }, (0, import_react6.createElement)(Icon), label); +- return (0, import_react6.createElement)( + } }, (0, import_react9.createElement)(Icon), label); + return (0, import_react9.createElement)( "div", -- { className: inline ? "bt-q-inline" : "bt-q-dock" }, -- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) -+ { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, + { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, +- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, +- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, +- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), +- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( + react ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react9.createElement)(SmileIcon)) : null, -+ sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react9.createElement)(ReplyIcon)) : null, ++ sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text2) }, (0, import_react9.createElement)(ReplyIcon)) : null, + (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react9.createElement)(CheckIcon) : (0, import_react9.createElement)(DotsIcon)), + panel === "react" ? (0, import_react_dom.createPortal)((0, import_react9.createElement)( -+ "div", -+ { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, + "div", + { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, +- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { + REACTIONS.map((emoji) => (0, import_react9.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { -+ close(); -+ react(emoji); -+ } }, emoji)) -+ ), document.body) : null, + close(); + react(emoji); + } }, emoji)) + ), document.body) : null, +- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( + panel === "more" ? (0, import_react_dom.createPortal)((0, import_react9.createElement)( -+ "div", -+ { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, -+ item(t("Copy"), CopyIcon, copy2), -+ item(t("Download"), DownloadIcon, download2) -+ ), document.body) : null + "div", + { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, + item(t("Copy"), CopyIcon, copy2), +@@ -3891,12 +3891,40 @@ + ), document.body) : null ); } --function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { -- const question = useRoster((value) => value.questions?.[sessionId]); -- const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true; -- const loading = useSession?.(historyLoading) === true; -- if (question === void 0 || question.callId && (drawn || loading)) return null; -- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); --} --function AnsweredCell({ node }) { -- const data = node.data ?? {}; -- const questions = Array.isArray(data.questions) ? data.questions : []; -- if (questions.length === 0) { -- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; +-function ThinkingRow({ text }) { +- return (0, import_react6.createElement)( +var PEEK_MAX = 160; +var CUT = "…"; +var MARKUP = [/^\s{0,3}(?:`{3,}|~{3,})/, /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/]; @@ -6138,81 +4968,23 @@ diff --git a/client.js b/client.js + for (const part of graphemes(line2)) { + if (kept.length + part.length > PEEK_MAX - CUT.length) break; + kept += part; - } -- const answers = Array.isArray(data.answers) ? data.answers : []; -- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { -- key: question.id, -- question, -- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -- }))); --} -- --// src/client/secret-card.js --var import_react5 = require("react"); --var MIN_VALUE = 4; --var NONE = {}; --var KeyIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), -- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) --); --var LockIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), -- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) --); --var WarnIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), -- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) --); --var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); --var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); --var scopeLabel = (scope, roster) => { -- if (scope === "all") return t("All Bots"); -- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); -- return names.length === 0 ? t("No Bot") : names.join(t(" and ")); ++ } + return kept + CUT; - }; --var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; --function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { -- return (0, import_react5.createElement)("input", { -- className: "bt-secret-input", -- type: MASK_BY_CSS ? "text" : "password", -- "data-masked": MASK_BY_CSS ? "" : void 0, -- value, -- autoFocus, -- autoComplete: MASK_BY_CSS ? "off" : "new-password", -- autoCorrect: "off", -- autoCapitalize: "off", -- spellCheck: false, -- "data-1p-ignore": "", -- "data-lpignore": "true", -- "data-bwignore": "", -- "data-form-type": "other", -- "aria-label": label, -- placeholder: t("Paste or type the key"), -- onChange: (event) => onChange(event.target.value), -- onKeyDown: (event) => { -- if (event.key !== "Enter" || event.nativeEvent.isComposing) return; -- event.preventDefault(); -- onEnter?.(); -- } ++}; +var onlyMarkup = (lines) => lines.length === 1 ? t("{n} line of reasoning", { n: lines.length }) : t("{n} lines of reasoning", { n: lines.length }); -+var peekLine = (text) => { -+ const lines = text.split("\n").map((part) => part.replace(/\s+/g, " ").trim()).filter((part) => part !== ""); ++var peekLine = (text2) => { ++ const lines = text2.split("\n").map((part) => part.replace(/\s+/g, " ").trim()).filter((part) => part !== ""); + const prose = lines.find((line2) => !isMarkup(line2)); + return prose === void 0 ? onlyMarkup(lines) : clip(prose); +}; -+function ThinkingRow({ text }) { -+ if (typeof text !== "string" || text.trim() === "") return null; -+ const peek = peekLine(text); ++function ThinkingRow({ text: text2 }) { ++ if (typeof text2 !== "string" || text2.trim() === "") return null; ++ const peek = peekLine(text2); + return (0, import_react9.createElement)( -+ "details", -+ { className: "bt-think" }, + "details", + { className: "bt-think" }, +- (0, import_react6.createElement)("summary", null, t("Thought it through")), +- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) + (0, import_react9.createElement)( + "summary", + null, @@ -6220,19 +4992,20 @@ diff --git a/client.js b/client.js + t("Thought it through"), + peek ? (0, import_react9.createElement)("span", { className: "bt-think-peek" }, peek) : null + ), -+ (0, import_react9.createElement)("div", { className: "bt-think-text" }, text) -+ ); -+} -+function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -+ const tail = useTurnData?.("turn-tail"); -+ const location2 = node.location; -+ const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; -+ const seq = node.data.finalNode?.seq; ++ (0, import_react9.createElement)("div", { className: "bt-think-text" }, text2) + ); + } + function useFileMentions({ node, openFile, fileMentions, useTurnData }) { +@@ -3904,19 +3904,33 @@ + const location2 = node.location; + const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; + const seq = node.data.finalNode?.seq; +- return (0, import_react6.useMemo)(() => { + return (0, import_react9.useMemo)(() => { -+ if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; -+ return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; -+ }, [fileMentions, openFile, seq, tail, turn]); -+} + if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; + return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; + }, [fileMentions, openFile, seq, tail, turn]); + } +function useClosingText(node, useTurnData) { + const tail = useTurnData?.("turn-tail"); + const seq = node.data.finalNode?.seq; @@ -6241,44 +5014,48 @@ diff --git a/client.js b/client.js +} +function friendLead(bot, texts2) { + if (bot?.remote === void 0 || texts2.length === 0) return void 0; -+ if (texts2.every((text) => /^Not sent:/.test(text.trim()))) return void 0; ++ if (texts2.every((text2) => /^Not sent:/.test(text2.trim()))) return void 0; + return (0, import_react9.createElement)("span", { className: "bt-lead", title: bot.name, "data-flybot": "friend-lead" }, (0, import_react9.createElement)(BotMark, { bot, size: 22 })); +} -+function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { -+ const roster = useRoster((value) => value); -+ const data = node.data; -+ const steps = useTurnSteps(String(data.turn)); -+ const contexts = useTurnContext(String(data.turn)); -+ const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); + function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { + const roster = useRoster((value) => value); + const data = node.data; + const steps = useTurnSteps(String(data.turn)); + const contexts = useTurnContext(String(data.turn)); + const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); + const held = groupPart === "reasoning" ? void 0 : heldReplyOf(roster.byId[sessionId], sessionId, data.turn); + const closing = useClosingText(node, useTurnData); -+ const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); + const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); +- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); + const person = typedBy(roster, sessionId, data.turn); -+ const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block, index) => index === 0 ? stripTrace(block.text) : block.text).filter((text) => text.trim() !== ""); -+ const running = data.status === "running"; -+ useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); -+ if (hidden) return null; -+ const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); -+ const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; -+ if (texts2.length === 0 && !reasoning && relays.length === 0) return null; -+ const selfId = roster.byId[sessionId]?.id ?? sessionId; ++ const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block, index) => index === 0 ? stripTrace(block.text) : block.text).filter((text2) => text2.trim() !== ""); + const running = data.status === "running"; + useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); + if (hidden) return null; +@@ -3924,13 +3924,14 @@ + const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; + if (texts2.length === 0 && !reasoning && relays.length === 0) return null; + const selfId = roster.byId[sessionId]?.id ?? sessionId; +- return (0, import_react6.createElement)( +- import_react6.Fragment, + const bubbles = texts2.length ? person ? (0, import_react9.createElement)(PersonMessage, { name: person, texts: texts2, roster, selfId, actions, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions, lead: friendLead(roster.byId[sessionId], texts2) }) : null; + return (0, import_react9.createElement)( + import_react9.Fragment, -+ null, -+ relays.map((relay) => { -+ const source = relay.source; -+ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; + null, + relays.map((relay) => { + const source = relay.source; + const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(EventLine, { + return (0, import_react9.createElement)(EventLine, { -+ key: relay.seq, -+ prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), -+ bots: [sender], -+ onOpen: () => { -+ if (sender.id) actions.openExchange(selfId, sender.id); -+ }, -+ whole: true -+ }); -+ }), + key: relay.seq, + prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), + bots: [sender], +@@ -3940,61 +3940,230 @@ + whole: true + }); + }), +- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, +- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null + reasoning ? (0, import_react9.createElement)(ThinkingRow, { text: reasoning }) : null, + held && bubbles ? (0, import_react9.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles + ); @@ -6301,55 +5078,64 @@ 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) { + ); + } + function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { + if (whole && bots.length === 1) { +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( -+ "div", -+ { className: "bt-event" }, + "div", + { className: "bt-event" }, +- (0, import_react6.createElement)( + (0, import_react9.createElement)( -+ "button", -+ { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, + "button", + { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, +- (0, import_react6.createElement)("span", null, prefix), +- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }), +- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name) + (0, import_react9.createElement)("span", null, prefix), + (0, import_react9.createElement)(BotMark, { bot: bots[0], size: 16 }), + (0, import_react9.createElement)("span", { className: "bt-event-name" }, bots[0].name) -+ ) -+ ); -+ } -+ const parts = []; + ) + ); + } + const parts = []; +- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); + parts.push((0, import_react9.createElement)("span", { key: "p" }, prefix)); -+ bots.forEach((bot, index) => { + bots.forEach((bot, index) => { +- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); +- parts.push((0, import_react6.createElement)( + if (index > 0) parts.push((0, import_react9.createElement)("span", { key: `j${index}` }, joiner)); + parts.push((0, import_react9.createElement)( -+ "button", -+ { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, + "button", + { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), + (0, import_react9.createElement)(BotMark, { bot, size: 16 }), -+ bot.name -+ )); -+ }); + bot.name + )); + }); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); + return (0, import_react9.createElement)("div", { className: "bt-event" }, parts); -+} -+var heldForm = (text) => String(text ?? "").replace(/\s+/g, " ").trim().replace(/^(to (the )?face[ -]?room|to the room)\s*:\s*/i, "").trim(); + } +-function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { +- const lead = (0, import_react6.createElement)( ++var heldForm = (text2) => String(text2 ?? "").replace(/\s+/g, " ").trim().replace(/^(to (the )?face[ -]?room|to the room)\s*:\s*/i, "").trim(); +var sameName = (a, b) => String(a ?? "").replace(/\s+/g, " ").trim().toLowerCase() === String(b ?? "").replace(/\s+/g, " ").trim().toLowerCase(); -+function isHeldLine(room2, text, bot) { ++function isHeldLine(room2, text2, bot) { + if (room2?.origin !== "face-to-face") return false; -+ const line2 = heldForm(text); ++ const line2 = heldForm(text2); + if (!line2) return false; + return (room2.pendingApprovals ?? []).some((item) => { + if (item?.kind !== "face_room_message") return false; + if (bot !== void 0 && String(item.from ?? "").trim() !== "" && !sameName(item.from, bot.name)) return false; + const held = heldForm(item.text); + return held !== "" && (held === line2 || held.length >= 799 && line2.startsWith(held)); - }); - } --function ScopeSwitch({ value, onChange, bot }) { -- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; -- return (0, import_react5.createElement)( ++ }); ++} +var HELD_SENDING_MS = 6e4; -+function heldLineState(room2, { seq, botId, text } = {}, now = Date.now()) { ++function heldLineState(room2, { seq, botId, text: text2 } = {}, now = Date.now()) { + if (room2?.origin !== "face-to-face" || !Array.isArray(room2.heldLines)) return void 0; -+ const said = String(text ?? "").trim(); ++ const said = String(text2 ?? "").trim(); + const entry = room2.heldLines.filter((item) => seq !== void 0 && item?.seq !== void 0 ? item.seq === seq : item?.botId === botId && String(item?.text ?? "").trim() === said).at(-1); + if (entry === void 0) return void 0; + if (entry.status === "rejected") return "rejected"; @@ -6357,8 +5143,8 @@ diff --git a/client.js b/client.js + if (entry.status !== "pending" && entry.status !== "sending") return void 0; + return room2.face?.status === "left" || room2.face?.status === "closed" ? "rejected" : "held"; +} -+function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi, seq }) { -+ const held = heldLineState(room2, { seq, botId: bot.id, text }); ++function GroupMessage({ bot, text: text2, room: room2, roster, actions, msgKey, useUi, seq }) { ++ const held = heldLineState(room2, { seq, botId: bot.id, text: text2 }); + if (held === "rejected") { + return (0, import_react9.createElement)( + "div", @@ -6367,12 +5153,14 @@ diff --git a/client.js b/client.js + (0, import_react9.createElement)("span", null, t("{name} · Not sent", { name: bot.name })) + ); + } -+ if (held === "held" || isHeldLine(room2, text, bot)) return null; ++ if (held === "held" || isHeldLine(room2, text2, bot)) return null; + const lead = (0, import_react9.createElement)( -+ "button", -+ { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, + "button", + { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22 }) + (0, import_react9.createElement)(BotMark, { bot, size: 22 }) -+ ); + ); +- return (0, import_react6.createElement)( + const name = (0, import_react9.createElement)( + "span", + { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, @@ -6381,27 +5169,12 @@ diff --git a/client.js b/client.js + ); + return (0, import_react9.createElement)( "div", -- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, -- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), -- options.map(([id, text]) => (0, import_react5.createElement)("button", { -- key: id, -- type: "button", -- role: "radio", -- "aria-checked": value === id, -- className: "bt-secret-seg", -- onClick: () => onChange(id) -- }, text)) -+ { className: "bt-group-msg" }, + { className: "bt-group-msg" }, +- (0, import_react6.createElement)( + name, -+ (0, import_react9.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) - ); - } --function SecretHead({ bot, title, detail, name, onClose }) { -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-q-head bt-secret-head" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))), -- (0, import_react5.createElement)( ++ (0, import_react9.createElement)(Bubbles, { texts: [text2], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) ++ ); ++} +function remoteSpeaker(source, roster, sessionId) { + const handle = source.remote.handle; + const member = (roster.remoteBots ?? []).find((bot2) => bot2.handle === handle); @@ -6434,7 +5207,7 @@ diff --git a/client.js b/client.js + return Object.keys(look).length ? look : void 0; +} +var cleanCarrier = (value) => typeof value === "string" && value.trim() && !/[@()]/.test(value) ? value.trim() : ""; -+function RemoteMessage({ source, text, roster, actions, sessionId, msgKey, useUi }) { ++function RemoteMessage({ source, text: text2, roster, actions, sessionId, msgKey, useUi }) { + const room2 = roster.roomsById?.[sessionId]; + const face = room2?.origin === "face-to-face"; + const { bot, member, name, handle, carrier, chatId } = remoteSpeaker(source, roster, sessionId); @@ -6442,10 +5215,7 @@ diff --git a/client.js b/client.js + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + if (person) { + return (0, import_react9.createElement)( - "div", -- { className: "bt-q-text" }, -- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), -- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") ++ "div", + { className: "bt-group-msg bt-person-msg bt-remote-msg", "data-person": person, "data-remote": handle }, + // One short header: the person, and the Bot that carried the line only when it is known (#275). + (0, import_react9.createElement)( @@ -6454,7 +5224,7 @@ diff --git a/client.js b/client.js + person, + carrier ? (0, import_react9.createElement)("span", { className: "bt-remote-handle" }, t("via {name}", { name: carrier })) : null + ), -+ (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react9.createElement)("span", { className: "bt-lead", title: person }, (0, import_react9.createElement)(PersonMark, { name: person })) }), ++ (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text2)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react9.createElement)("span", { className: "bt-lead", title: person }, (0, import_react9.createElement)(PersonMark, { name: person })) }), + files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null + ); + } @@ -6465,26 +5235,18 @@ diff --git a/client.js b/client.js + { className: "bt-group-msg bt-remote-msg", "data-remote": handle }, + // A face room's header is the one name alone; the handle is on hover only (#457). + face ? (0, import_react9.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) }, title: handle }, name) : (0, import_react9.createElement)( -+ "span", -+ { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, + "span", + { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, +- bot.name, +- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null + name, + (0, import_react9.createElement)("span", { className: "bt-remote-handle", title: source.remote.home ?? handle, "aria-label": t("Friend's Bot") }, (0, import_react9.createElement)(GlobeMark), handle) ), -- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null -+ (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }), +- (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 SecretCard({ bot, request, roster, actions }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [scope, setScope] = (0, import_react5.useState)("all"); -- const [replacing, setReplacing] = (0, import_react5.useState)(false); -- const [busy, setBusy] = (0, import_react5.useState)(false); -- const [error, setError] = (0, import_react5.useState)(""); -- const noteId = (0, import_react5.useId)(); -- const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); -- const run = (promise) => { -- setBusy(true); ++ ); ++} +function FirstContact({ source, roster, sessionId }) { + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + const who = person || remoteSpeaker(source, roster).name; @@ -6504,13 +5266,7 @@ diff --git a/client.js b/client.js + const [error, setError] = (0, import_react9.useState)(""); + const download2 = async (file) => { + setBusy(file.id); - setError(""); -- return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); -- }; -- const cancel = () => run(actions.secretCancel(request.id)); -- const long = value.trim().length >= MIN_VALUE; -- const save = () => { -- if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); ++ setError(""); + try { + const got = await call(`flybot/${plugin}/attachment`, { id: file.id, botId }); + const value = got?.value ?? got; @@ -6524,9 +5280,7 @@ diff --git a/client.js b/client.js + } finally { + setBusy(""); + } - }; -- const allow = () => { -- if (!busy) void run(actions.secretAllow(request.id)); ++ }; + return (0, import_react9.createElement)( + "div", + { className: "bt-file-chips" }, @@ -6536,285 +5290,231 @@ 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 }) { + ); + } + function GroupEvent({ prefix, room: room2, name, roster, actions }) { +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( -+ "div", -+ { className: "bt-event" }, + "div", + { className: "bt-event" }, +- (0, import_react6.createElement)("span", null, prefix), +- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null + (0, import_react9.createElement)("span", null, prefix), + room2 ? (0, import_react9.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react9.createElement)("span", null, name) : null -+ ); -+} -+function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { -+ if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; -+ const self = roster.byId[sessionId]; -+ const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); -+ const other = team || !scope ? void 0 : findBot(scope); -+ const target = other && other.id !== self?.id ? other : self; -+ if (!target) return null; -+ const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); + ); + } + function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { +@@ -4005,14 +4005,14 @@ + const target = other && other.id !== self?.id ? other : self; + if (!target) return null; + const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( -+ "div", -+ { className: "bt-event" }, + "div", + { className: "bt-event" }, +- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) + (0, import_react9.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react9.createElement)(MemoryIcon), label) -+ ); -+} -+function PartLine({ time }) { + ); + } + function PartLine({ time }) { +- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null); + return (0, import_react9.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react9.createElement)("span", null, dayLabel(time)) : null); -+} -+var scrollParent = (node) => { -+ for (let at = node.parentElement; at; at = at.parentElement) { -+ if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; -+ } -+ return null; -+}; -+function EarlierParts({ sessionId, roster, actions }) { + } + var scrollParent = (node) => { + for (let at = node.parentElement; at; at = at.parentElement) { +@@ -4021,11 +4021,11 @@ + return null; + }; + function EarlierParts({ sessionId, roster, actions }) { +- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); +- const top = (0, import_react6.useRef)(null); +- const busy = (0, import_react6.useRef)(false); +- const alive = (0, import_react6.useRef)(true); +- const restore = (0, import_react6.useRef)(null); + const [view, setView] = (0, import_react9.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); + const top = (0, import_react9.useRef)(null); + const busy = (0, import_react9.useRef)(false); + const alive = (0, import_react9.useRef)(true); + const restore = (0, import_react9.useRef)(null); -+ const load = (cursor) => { -+ if (busy.current) return; -+ busy.current = true; -+ setView((current) => ({ ...current, loading: true, failed: false })); -+ actions.earlier(sessionId, cursor).then((page) => { -+ busy.current = false; -+ if (!alive.current) return; -+ const scroller = top.current && scrollParent(top.current); -+ if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; -+ setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); -+ }, () => { -+ busy.current = false; -+ if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); -+ }); + const load = (cursor) => { + if (busy.current) return; + busy.current = true; +@@ -4041,19 +4041,19 @@ + if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); + }); }; -- const filling = request.kind === "fill" || replacing; -- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; -- return (0, import_react5.createElement)( +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ alive.current = true; -+ load(void 0); -+ return () => { -+ alive.current = false; -+ }; -+ }, [sessionId]); + alive.current = true; + load(void 0); + return () => { + alive.current = false; + }; + }, [sessionId]); +- (0, import_react6.useLayoutEffect)(() => { + (0, import_react9.useLayoutEffect)(() => { -+ const pending = restore.current; -+ restore.current = null; -+ if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; -+ }, [view.items]); + const pending = restore.current; + restore.current = null; + if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; + }, [view.items]); +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ const node = top.current; -+ if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; -+ const observer = new IntersectionObserver((entries2) => { -+ if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); -+ }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); -+ observer.observe(node); -+ return () => observer.disconnect(); -+ }, [view.cursor, view.loading, view.failed]); -+ const selfId = roster.byId[sessionId]?.id; -+ const rows = []; -+ for (const item of view.items) { -+ const last = rows.at(-1); -+ if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); -+ else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); -+ } + const node = top.current; + if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; + const observer = new IntersectionObserver((entries2) => { +@@ -4069,35 +4069,35 @@ + if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); + else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); + } +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( "div", -- { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, -- (0, import_react5.createElement)(SecretHead, { -- bot, -- title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), -- detail: request.purpose, -- name: request.name, -- onClose: busy ? void 0 : cancel -- }), -- request.kind === "allow" ? (0, import_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, -- filling ? (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-body" }, -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), -- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null -- ) : null, -- (0, import_react5.createElement)( -+ { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, + { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, +- (0, import_react6.createElement)( + (0, import_react9.createElement)( "div", -- { className: "bt-secret-note", id: noteId }, -- t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), -- (0, import_react5.createElement)("code", null, `$DSH_SECRET_${request.name}`), -- ".", -- filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" -+ { ref: top, className: "bt-earlier-top" }, + { ref: top, className: "bt-earlier-top" }, +- view.loading ? (0, import_react6.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null + view.loading ? (0, import_react9.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react9.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react9.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null ), -- warn, -- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, -- (0, import_react5.createElement)( -- "div", -- { className: "bt-q-foot" }, -- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, -- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), -- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) -- ) -+ rows.map((row, index) => { -+ const key = rows.length - index; -+ switch (row.kind) { -+ case "divider": + rows.map((row, index) => { + const key = rows.length - index; + switch (row.kind) { + case "divider": +- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); + return (0, import_react9.createElement)(PartLine, { key, time: row.time }); -+ case "user": + case "user": +- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( + return (0, import_react9.createElement)("div", { key, className: "bt-urow" }, (0, import_react9.createElement)( -+ "div", -+ { className: "bt-ububble" }, + "div", + { className: "bt-ububble" }, +- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, +- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null + row.text ? (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, + row.images ? (0, import_react9.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null -+ )); -+ case "answer": + )); + case "answer": +- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); + return (0, import_react9.createElement)(AnsweredCell, { key, node: { data: row } }); -+ case "bot": + case "bot": +- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); + return (0, import_react9.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); -+ case "event": { + case "event": { +- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); + if (row.role === "report") return (0, import_react9.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); -+ const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; -+ const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); + const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; + const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); +- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { + return (0, import_react9.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { -+ if (selfId && other.id) actions.openExchange(selfId, other.id); -+ }, whole: true }); -+ } -+ default: -+ return null; -+ } -+ }), + if (selfId && other.id) actions.openExchange(selfId, other.id); + }, whole: true }); + } +@@ -4105,7 +4105,7 @@ + return null; + } + }), +- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) + (0, import_react9.createElement)(PartLine, { time: view.startedAt }) ); } --function SettledSecret({ bot, request, roster }) { -- const canceled = request.status === "canceled"; -- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, -- (0, import_react5.createElement)( -- "div", -- { className: "bt-q-head" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })), -- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)( -- "p", -- { className: "bt-q-title" }, -- text, -- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) -- )), -- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) -- ) -- ); -+function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -+ const roster = useRoster((value) => value); -+ const data = node.data; -+ const source = data.source !== null && typeof data.source === "object" ? data.source : {}; -+ const selfId = roster.byId[sessionId]?.id ?? sessionId; -+ const deferred = source.kind === "bot" && source.deferred === true; -+ const relayed = isRelayContext(data); -+ const turn = node.location?.turn?.turn ?? data.turn; + function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { +@@ -4116,7 +4116,7 @@ + const deferred = source.kind === "bot" && source.deferred === true; + const relayed = isRelayContext(data); + const turn = node.location?.turn?.turn ?? data.turn; +- (0, import_react6.useLayoutEffect)(() => { + (0, import_react9.useLayoutEffect)(() => { -+ if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); -+ }, [relayed, sessionId, turn, source.senderSessionId]); -+ const order = useSessions((list) => { -+ const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -+ return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; -+ }); -+ const peers = useTurnClock((state) => { -+ if (!relayed) return ""; -+ const turns = state[sessionId] ?? {}; -+ return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); -+ }); -+ const visit = useUi((state) => state.visits?.[sessionId]); -+ const hiddenTurns = roster.exchangeTurns[sessionId]; -+ const unreadOpening = useTurnClock((state) => { -+ const first = openingTurn(state[sessionId], order, hiddenTurns); -+ return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; -+ }); -+ const body = triggerBody(); + if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); + }, [relayed, sessionId, turn, source.senderSessionId]); + const order = useSessions((list) => { +@@ -4135,23 +4135,31 @@ + return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; + }); + const body = triggerBody(); +- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; + return unreadOpening ? (0, import_react9.createElement)(import_react9.Fragment, null, (0, import_react9.createElement)("div", { className: "bt-open-new" }, (0, import_react9.createElement)(NewDivider), (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; -+ function triggerBody() { + function triggerBody() { + if (source.form === "remote-chat") return null; + if (source.shown === true) return null; + if (source.remote?.handle) { + const message = (0, import_react9.createElement)(RemoteMessage, { source, text: remoteMessageText(stripHeader(contentText(data.content))), roster, actions, sessionId, msgKey: `${sessionId}:${data.turn}:remote`, useUi }); + return source.remote.firstContact === true ? (0, import_react9.createElement)(import_react9.Fragment, null, (0, import_react9.createElement)(FirstContact, { source, roster, sessionId }), message) : message; + } -+ if (source.kind === "bot") { + if (source.kind === "bot") { +- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); +- if (source.role === "kickoff" || source.role === "room") return null; + if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react9.createElement)(EarlierParts, { sessionId, roster, actions }); + if (source.role === "room" || source.role === "kickoff") return null; + if (source.role === "mention") return (0, import_react9.createElement)("div", { className: "bt-event" }, (0, import_react9.createElement)("span", null, contentText(data.content))); -+ if (source.role === "secret" || deferred) return null; -+ if (source.role === "post") { -+ if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; + if (source.role === "secret" || deferred) return null; + if (source.role === "post") { + if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; + if (source.notice === true || source.senderSessionId === "face-notice") return null; -+ const room2 = roster.roomsById[sessionId]; -+ const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; + const room2 = roster.roomsById[sessionId]; + const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); + return (0, import_react9.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); -+ } -+ if (source.role === "report") { + } + if (source.role === "report") { +- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); + return (0, import_react9.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); -+ } + } +- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); +- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); + if (source.role === "resume" || source.resumed === true) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); + if (source.role === "retry" || source.retried === true) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); -+ const run = relayed ? commRunOf( -+ order.split(",").filter(Boolean).map(Number), -+ Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), -+ turn, -+ hiddenTurns -+ ) : null; -+ if (run === "folded") return null; -+ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -+ const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); -+ const bots = [sender]; -+ for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { -+ if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); -+ } + const run = relayed ? commRunOf( + order.split(",").filter(Boolean).map(Number), + Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), +@@ -4165,7 +4165,7 @@ + for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { + if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); + } +- return (0, import_react6.createElement)(EventLine, { + return (0, import_react9.createElement)(EventLine, { -+ prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), -+ bots, -+ onOpen: (bot) => actions.openExchange(selfId, bot.id), -+ whole: true -+ }); -+ } -+ if (source.kind === "schedule") { -+ const prompts = reminderPrompts(contentText(data.content)); + prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), + bots, + onOpen: (bot) => actions.openExchange(selfId, bot.id), +@@ -4174,24 +4174,24 @@ + } + if (source.kind === "schedule") { + const prompts = reminderPrompts(contentText(data.content)); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); + return (0, import_react9.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); -+ } -+ if (source.kind === "user-question-reply") return null; + } + if (source.kind === "user-question-reply") return null; +- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); + return (0, import_react9.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); -+ } -+} -+function NewDivider() { + } + } + function NewDivider() { +- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New"))); + return (0, import_react9.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react9.createElement)("span", null, t("New"))); -+} -+function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { -+ const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; + } + function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { + const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ if (!open) turnProcess.setOpen(true); -+ }, [open, turnProcess]); -+ const turn = node?.location?.turn?.turn ?? node?.data?.turn; -+ const start = node?.location?.turn?.start?.time; -+ const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); + if (!open) turnProcess.setOpen(true); + }, [open, turnProcess]); + const turn = node?.location?.turn?.turn ?? node?.data?.turn; + const start = node?.location?.turn?.start?.time; + const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ actions.noteTurnTime(sessionId, turn, "first", start); -+ }, [sessionId, turn, start]); -+ const visit = useUi((state) => state.visits?.[sessionId]); -+ const fresh = useTurnClock((state) => { -+ const own = state[sessionId]?.[turn]; -+ return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; -+ }); + actions.noteTurnTime(sessionId, turn, "first", start); + }, [sessionId, turn, start]); + const visit = useUi((state) => state.visits?.[sessionId]); +@@ -4199,11 +4199,65 @@ + const own = state[sessionId]?.[turn]; + return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; + }); +- return fresh ? (0, import_react6.createElement)(NewDivider) : null; + return fresh ? (0, import_react9.createElement)(NewDivider) : null; -+} -+function Nothing() { -+ return null; } --function RequestView({ bot, request, roster, actions }) { -- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster }); + function Nothing() { + return null; + } +var HIDDEN_REMOVED_TOOLS = /* @__PURE__ */ new Set(["ask_user_question"]); +var isConnectorTool = (name) => typeof name === "string" && /^mcp__(github|composio)__/.test(name); +var SILENT_REGISTRATION_TOOLS = /* @__PURE__ */ new Set([ @@ -6868,86 +5568,14 @@ diff --git a/client.js b/client.js + } + if (parts.length === 0) return null; + return (0, import_react9.createElement)("div", { className: "bt-event" }, parts.join(" · ")); ++} + var SEPARATOR_GAP_MS = 30 * 60 * 1e3; + function clockLabel(time, now = Date.now()) { + const date = new Date(time); +@@ -4233,10 +4233,19 @@ + return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; } --function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { -- const bot = roster.byId[sessionId]; -- if (!bot) return null; -- const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); -- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions })); -- if (done && /already saved/.test(contentText(content))) { -- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); -+var SEPARATOR_GAP_MS = 30 * 60 * 1e3; -+function clockLabel(time, now = Date.now()) { -+ const date = new Date(time); -+ const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); -+ const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); -+ const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); -+ if (days === 0) return t("Today {time}", { time: clock4 }); -+ if (days === 1) return t("Yesterday {time}", { time: clock4 }); -+ const sameYear = date.getFullYear() === new Date(now).getFullYear(); -+ return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; -+} -+function nextTurnStart(turns, turn) { -+ let next; -+ for (const key in turns) { -+ const candidate = Number(key); -+ if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; - } -- return null; -+ return next === void 0 ? void 0 : turns[next].first; - } --function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { -- const roster = useRoster((value) => value); -- const shown = useCards?.((state) => state[sessionId]) ?? NONE; -- const loading = useSession?.(historyLoading) === true; -- const bot = roster.byId[sessionId]; -- if (!bot || currentPart(bot) !== sessionId) return null; -- const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); -- const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); -- if (waiting.length === 0) return null; -- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); -+function openingTurn(turns, firstTurns, hiddenTurns) { -+ if (!turns || !firstTurns) return null; -+ const start = nextTurnStart(turns, -Infinity); -+ if (start === void 0) return null; -+ const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]); -+ const listed = firstTurns.split(",").map(Number); -+ const earlier = listed.filter((entry) => entry < turn); -+ return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; - } --function ScopeEditor({ secret, roster, onSave }) { -- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some"); -- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope); -- const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); -- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join(); -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit-block" }, -- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, -- [["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)( -- "label", -- { key: id, className: "bt-secret-radio" }, -- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), -- text -- )) -- ), -- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { -- key: bot.id, -- type: "button", -- className: "bt-secret-bot", -- "aria-pressed": picked.includes(bot.id), -- onClick: () => toggle(bot.id) -- }, (0, import_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null, -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-row-actions" }, -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access")) -- ) -- ); -+var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; + var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; +function movedOnSince(visit, turns, hiddenTurns) { + if (typeof visit?.entered !== "number") return false; + for (const key in turns ?? {}) { @@ -6955,438 +5583,177 @@ diff --git a/client.js b/client.js + if (turns[key].first !== void 0 && turns[key].first > visit.entered) return true; + } + return false; - } --function ValueEditor({ secret, onSave }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [replaced, setReplaced] = (0, import_react5.useState)(false); -- const long = value.trim().length >= MIN_VALUE; -- const save = () => { -- if (!long) return; -- void Promise.resolve(onSave(value)).then((ok) => { -- if (ok) { -- setValue(""); -- setReplaced(true); -- } -- }); -- }; -- const edit = (next) => { -- setValue(next); -- setReplaced(false); -- }; -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit-block" }, -- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-inline" }, -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) -- ), -- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null ++} +var settledVisit = (visit) => typeof visit?.seen === "number" && visit.seen < visit.entered ? { ...visit, seen: visit.entered } : visit; -+function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { -+ const turn = location2.turn; -+ const time = location2.end.time; + function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { + const turn = location2.turn; + const time = location2.end.time; +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ actions.noteTurnTime(sessionId, turn, "end", time); -+ }, [sessionId, turn, time]); -+ const label = useTurnClock((state) => { -+ const start = nextTurnStart(state[sessionId] ?? {}, turn); -+ return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; -+ }); -+ const visit = useUi((state) => state.visits?.[sessionId]); -+ const fresh = useTurnClock((state) => { -+ if (typeof visit?.seen !== "number" || time > visit.seen) return false; -+ const start = nextTurnStart(state[sessionId] ?? {}, turn); -+ return start !== void 0 && start > visit.seen && start <= visit.entered; -+ }); -+ if (!label && !fresh) return null; + actions.noteTurnTime(sessionId, turn, "end", time); + }, [sessionId, turn, time]); + const label = useTurnClock((state) => { +@@ -4250,15 +4250,15 @@ + return start !== void 0 && start > visit.seen && start <= visit.entered; + }); + if (!label && !fresh) return null; +- return (0, import_react6.createElement)( +- import_react6.Fragment, + return (0, import_react9.createElement)( + import_react9.Fragment, -+ null, + null, +- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, +- fresh ? (0, import_react6.createElement)(NewDivider) : null + label ? (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, + fresh ? (0, import_react9.createElement)(NewDivider) : null ); } --function SecretRow({ secret, roster, actions, run, open, onToggle }) { -- const [confirm, setConfirm] = (0, import_react5.useState)(false); -- (0, import_react5.useEffect)(() => { -- if (!open) setConfirm(false); -- }, [open]); -- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); -- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); -- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-item", "data-open": open ? "" : void 0 }, -- (0, import_react5.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, -- (0, import_react5.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), -- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta) -- ), -- (0, import_react5.createElement)( -- "span", -- { className: "bt-set-control" }, -- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) -- ) -- ), -- open ? (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit" }, -- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), -- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-row-actions" }, -- confirm ? [ -- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), -- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), -- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) -- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) -- ) -- ) : null -- ); -+function Shimmer({ children }) { + function Shimmer({ children }) { +- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); + return (0, import_react9.createElement)("span", { className: "bt-shimmer" }, children); } --function AddSecret({ actions, run, onDone }) { -- const [name, setName] = (0, import_react5.useState)(""); -- const [purpose, setPurpose] = (0, import_react5.useState)(""); -- const [value, setValue] = (0, import_react5.useState)(""); -- const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; -- const save = () => { -- if (!ready) return; -- void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { -- if (ok) { -- setValue(""); -- onDone(); -- } -- }); -- }; -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-set-row bt-set-paste bt-secret-add" }, -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-row-actions" }, -- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) -- ) -- ); -+var PHRASES = { -+ thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], -+ working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] -+}; -+var TOOL_PHRASES = [ -+ [/web_?search|search_?web/i, "Searching online"], -+ [/fetch|browse|browser|web/i, "Reading a page"], -+ [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"], -+ [/^(read|view|cat)\b|read_/i, "Opening a file"], -+ [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"], -+ [/write|edit|patch|apply|replace/i, "Writing changes"], -+ [/image|photo|draw/i, "Working on an image"] -+]; -+var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" }; -+function toolActivity(name, arg, findBot, self) { -+ switch (name) { -+ case "ask_user": -+ return null; -+ case "request_secret": -+ return null; -+ case "list_secrets": -+ return { label: t("Checking the keys"), state: "searching" }; -+ case "ask_user_question": -+ return { label: t("Waiting for your answer"), state: "alert" }; -+ case "group_relay": -+ return { relay: true, state: "orbit" }; -+ case "message_bot": { -+ const target = findBot(arg("to")); -+ return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; -+ } -+ case "stop_bot": { -+ const target = findBot(arg("bot")); -+ return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; -+ } -+ case "create_bot": -+ return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" }; -+ case "update_bot": { -+ const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -+ return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" }; -+ } -+ case "list_bots": -+ return { label: t("Checking the team"), state: "searching" }; -+ case "read_group_chat": -+ return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" }; -+ case "read_own_chat": -+ return { label: t("Looking back through the chat"), state: "searching" }; -+ case "remember": -+ return { label: t("Saving to memory"), state: "working" }; -+ case "forget": -+ return { label: t("Updating memory"), state: "working" }; -+ case "recall": -+ return { label: t("Checking memory"), state: "searching" }; -+ default: -+ if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" }; -+ const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1]; -+ return { label: phrase ? t(phrase) : null, state: toolState(name) }; -+ } + var PHRASES = { + thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], +@@ -4320,19 +4320,22 @@ } --function SecretsPage({ roster, actions, run, Section }) { -- const [open, setOpen] = (0, import_react5.useState)(null); -- const [adding, setAdding] = (0, import_react5.useState)(false); -- const secrets = roster.secrets ?? []; -- return [ -- (0, import_react5.createElement)( -- "p", -- { key: "about", className: "bt-secret-about" }, -- t("Keys your Bots asked for. Bots never see the values: they use them as "), -- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), -- t(" in their shell, and every value is replaced by "), -- (0, import_react5.createElement)("code", null, "[secret:NAME]"), -- t(" before anything goes to a model.") -- ), -- (0, import_react5.createElement)( -- Section, -- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, -- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, -- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), -- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null -- ), -- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) -- ]; -+function workPhrase(tool) { -+ if (!tool) return t("Thinking about it"); + function workPhrase(tool) { + if (!tool) return t("Thinking about it"); +- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); + return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool }); - } --var SECRET_CSS = ` --.bt-secret-head{align-items:center;gap:10px} --.bt-secret-who{position:relative;flex:none;display:inline-flex} --.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} --.bt-secret-lock svg{width:11px;height:11px} --.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} --.bt-secret-body{display:flex;flex-direction:column;gap:8px} --.bt-secret-field{align-items:center;min-height:36px} --.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} --.bt-secret-input[data-masked]{-webkit-text-security:disc} --.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} --.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} --.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} --.bt-secret-glide[data-at="1"]{transform:translateX(100%)} --.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} --.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} --.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} --/* A key card asks the user to act, so the process group carrying it stays open in every -- work-details mode (same override as .bt-group-replies in styles.js). */ --[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} --[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} --.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} --.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} --@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} --.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} --.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} --.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} --.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} --.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} --.bt-secret-done .bt-q-head{align-items:center} --.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} --.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} --.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} --.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} --@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} --@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} --.bt-secret-event{display:inline-flex;align-items:center;gap:5px} --.bt-secret-dock{flex-direction:column;align-items:center} --.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} --.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} --.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} --.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} --.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} --.bt-secret-faces{display:inline-flex} --.bt-secret-faces>*+*{margin-left:-6px} --.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} --.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} --.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} --.bt-secret-radios{display:flex;gap:16px;font-size:13px} --.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} --.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} --.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} --.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} --.bt-secret-bot svg{width:13px;height:13px} --.bt-secret-inline{display:flex;gap:8px;align-items:center} --.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} --.bt-secret-row-actions .bt-set-hint{flex:1} --.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} --.bt-secret-danger svg{width:13px;height:13px} --.bt-secret-add .bt-input{height:34px} --@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} --`; -- --// src/client/turn-reasoning.js --function turnReasoning(steps, current) { -- const members = steps.length ? steps : [current]; -- if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; -- return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); ++} +function workText(tool, step) { + return step ? t("{action}: {detail}", { action: workPhrase(tool), detail: step }) : `${workPhrase(tool)}…`; } --var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); --var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); --var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); -- --// src/client/cells.js --var import_react6 = require("react"); --var import_react_dom = require("react-dom"); --var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); --var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); --function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) { -- const onClick = (event) => { -- const target = mentionTarget(event); -- if (target === null) return; -- event.preventDefault(); -- event.stopPropagation(); -- actions.openSession(target); -- }; -- const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; -- return (0, import_react6.createElement)( -+function useLiveNote(actions, sessionId, id, entry) { -+ const key = entry ? JSON.stringify(entry) : ""; + function useLiveNote(actions, sessionId, id, entry) { + const key = entry ? JSON.stringify(entry) : ""; +- (0, import_react6.useEffect)(() => { + (0, import_react9.useEffect)(() => { -+ if (!key) return void 0; -+ actions.noteLive(sessionId, id, JSON.parse(key)); -+ return () => actions.noteLive(sessionId, id, null); -+ }, [sessionId, id, key]); -+} + if (!key) return void 0; + actions.noteLive(sessionId, id, JSON.parse(key)); + return () => actions.noteLive(sessionId, id, null); + }, [sessionId, id, key]); + } +-function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { +- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); +- (0, import_react6.useEffect)(() => { +function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi }) { + const [live, setLive] = (0, import_react9.useState)({ replies: [] }); + (0, import_react9.useEffect)(() => { -+ if (done) return void 0; -+ let stop = false; -+ let timer; -+ let seen = ""; -+ const tick = async () => { -+ const result = await actions.roomProgress(sessionId); -+ if (stop) return; -+ const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; + if (done) return void 0; + let stop = false; + let timer; +@@ -4341,10 +4341,10 @@ + const result = await actions.roomProgress(sessionId); + if (stop) return; + const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; +- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; + const key = entry ? JSON.stringify(entry.replies) : ""; -+ if (entry && key !== seen) { -+ seen = key; + if (entry && key !== seen) { + seen = key; +- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); + setLive({ replies: entry.replies ?? [] }); -+ } -+ timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); -+ }; -+ void tick(); -+ return () => { -+ stop = true; -+ clearTimeout(timer); -+ }; -+ }, [done, sessionId, callId]); -+ let replies = live.replies; -+ if (done) { -+ try { -+ replies = JSON.parse(contentText(content)).replies ?? []; -+ } catch { -+ replies = []; -+ } -+ } + } + timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); + }; +@@ -4362,71 +4362,89 @@ + replies = []; + } + } +- const pending = done || !live.speaking ? [] : [live.speaking]; +- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; +- if (replies.length === 0 && pending.length === 0) return null; + if (replies.length === 0) return null; -+ const room2 = roster.roomsById[sessionId]; + const room2 = roster.roomsById[sessionId]; +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( "div", -- { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, -- texts2.map((text, index) => { -- const key = msgKey ? `${msgKey}:${index}` : void 0; -- const picked = key ? reactions?.[index] ?? [] : []; -- const last = index === texts2.length - 1; -- const live = streaming && last; -- return (0, import_react6.createElement)( -- "div", -- { key: index, className: "bt-msg" }, -- (0, import_react6.createElement)( -+ { className: "bt-group-replies" }, -+ replies.map((reply, index) => { + { className: "bt-group-replies" }, + replies.map((reply, index) => { +- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; + const stored = roster.byId[reply.botId]; + const resolved = reply.remote ? stored : rosterBot(roster, reply.botId) ?? stored; + const bot = resolved ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; -+ if (reply.kind === "user") { + if (reply.kind === "user") { +- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( + const who = reply.name && reply.name !== "User" ? reply.name : reply.authorId; + if (reply.other) { + return (0, import_react9.createElement)(PersonMessage, { key: `user-${index}`, name: who, texts: [remoteMessageText(reply.text)], roster, selfId: sessionId, actions, sessionId, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); + } + return (0, import_react9.createElement)( "div", -- { className: "bt-msg-line" }, -- lead && last ? lead : null, -- (0, import_react6.createElement)( +- { className: "bt-ububble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) +- )); + { key: `user-${index}`, className: "bt-urow", "data-author": reply.authorId, "data-author-name": who }, + who ? (0, import_react9.createElement)("span", { className: "bt-uauthor" }, who) : null, + (0, import_react9.createElement)( - "div", -- { className: "bt-bubble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) ++ "div", + { className: "bt-ububble" }, + (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: remoteMessageText(reply.text), labels: mdLabels(), variant: "compact" }) - ), -- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) -- ), -- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { -- key: emoji, -- type: "button", -- className: "bt-react", -- "aria-label": t("Remove reaction {emoji}", { emoji }), -- onClick: () => actions.toggleReaction(msgKey, index, emoji) -- }, emoji))) : null -- ); ++ ), + room2?.origin === "face-to-face" ? (0, import_react9.createElement)("span", { className: "bt-lead", title: who || void 0 }, (0, import_react9.createElement)(PersonMark, { name: who || void 0 })) : null + ); -+ } -+ if (reply.kind === "failed") { + } + if (reply.kind === "failed") { +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( -+ "div", + "div", +- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, + { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status" }, -+ t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) -+ ); -+ } -+ if (reply.kind === "lookup") { + t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) + ); + } + if (reply.kind === "lookup") { +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( -+ "div", -+ { key: `${reply.botId}-${index}`, className: "bt-event" }, -+ reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) -+ ); -+ } -+ if (reply.kind === "missed") { + "div", + { key: `${reply.botId}-${index}`, className: "bt-event" }, + reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) + ); + } + if (reply.kind === "missed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); -+ } -+ if (reply.kind === "resumed") { + } + if (reply.kind === "resumed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); -+ } -+ if (reply.kind === "cut") { + } + if (reply.kind === "cut") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); -+ } -+ if (reply.kind === "approval" || reply.kind === "working") { + } + if (reply.kind === "approval" || reply.kind === "working") { +- return (0, import_react6.createElement)( + return (0, import_react9.createElement)( -+ "div", -+ { key: `${reply.botId}-${index}`, className: "bt-event" }, + "div", + { key: `${reply.botId}-${index}`, className: "bt-event" }, +- (0, import_react6.createElement)( + (0, import_react9.createElement)( -+ "button", -+ { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, + "button", + { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), +- (0, import_react6.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) + (0, import_react9.createElement)(BotMark, { bot, size: 16 }), + (0, import_react9.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) -+ ) -+ ); -+ } -+ if (reply.kind === "stopped") { + ) + ); + } + if (reply.kind === "stopped") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); + return (0, import_react9.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); -+ } + } +- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); +- }), +- pending.map((id) => { +- const bot = roster.byId[id]; +- if (!bot) return null; +- return (0, import_react6.createElement)( +- "div", +- { key: `pending-${id}`, className: "bt-activity" }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), +- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) +- ); + if (reply.kind === "notice" && room2?.origin === "face-to-face" && String(reply.text ?? "").trim() === WAITING_OTHERS_NOTICE) return null; + if ((reply.kind === "notice" || reply.kind === "left") && leaveNoticeCoveredByClose(room2, reply.text)) return null; + if (reply.kind === "notice" || reply.kind === "joined" || reply.kind === "left" || reply.kind === "started") { @@ -7409,132 +5776,148 @@ diff --git a/client.js b/client.js }) ); } --var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; --function useDismiss(open, close, refs) { -- (0, import_react6.useEffect)(() => { -- if (!open) return void 0; -- const onDown = (event) => { -- if (refs.some((ref) => ref.current?.contains(event.target))) return; -- close(); -- }; -- const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- close(); -+var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); -+var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); -+function callTree(root) { -+ const calls = []; -+ const seen = /* @__PURE__ */ new Set(); +@@ -4435,52 +4435,52 @@ + function callTree(root) { + const calls = []; + const seen = /* @__PURE__ */ new Set(); +- const visit = (call2) => { +- const id = call2.callId ?? call2; + const visit = (call4) => { + const id = call4.callId ?? call4; -+ if (seen.has(id)) return; -+ seen.add(id); + if (seen.has(id)) return; + seen.add(id); +- calls.push(call2); +- for (const child of call2.subCalls ?? []) visit(child); + calls.push(call4); + for (const child of call4.subCalls ?? []) visit(child); -+ }; -+ visit(root); -+ return calls; -+} -+function useShownCards(actions, sessionId, callIds) { -+ const key = JSON.stringify(callIds); + }; + visit(root); + return calls; + } + function useShownCards(actions, sessionId, callIds) { + const key = JSON.stringify(callIds); +- (0, import_react6.useLayoutEffect)(() => { + (0, import_react9.useLayoutEffect)(() => { -+ const ids = JSON.parse(key); -+ if (ids.length === 0 || !actions.noteCards) return void 0; -+ actions.noteCards(sessionId, ids, 1); -+ return () => actions.noteCards(sessionId, ids, -1); -+ }, [sessionId, key]); -+} + const ids = JSON.parse(key); + if (ids.length === 0 || !actions.noteCards) return void 0; + actions.noteCards(sessionId, ids, 1); + return () => actions.noteCards(sessionId, ids, -1); + }, [sessionId, key]); + } +-function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { +- const tool = toolName(call2); +- const done = call2.kind === "tool-result"; +- const { text: arg, value: argValue } = toolArgs(call2); +function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) { + const tool = toolName(call4); + const done = call4.kind === "tool-result"; + const { text: arg, value: argValue } = toolArgs(call4); -+ const self = roster.byId[sessionId]; -+ switch (tool) { -+ case "message_bot": { -+ const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; + const self = roster.byId[sessionId]; + switch (tool) { + case "message_bot": { + const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); + if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); + return (0, import_react9.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -+ } -+ case "stop_bot": { -+ const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; + } + case "stop_bot": { + const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); + if (done && call4.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); + return (0, import_react9.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -+ } -+ case "create_bot": { -+ const created = findBot(arg("name")); -+ if (!done || created === void 0) return null; + } + case "create_bot": { + const created = findBot(arg("name")); + if (!done || created === void 0) return null; +- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); + return (0, import_react9.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); -+ } -+ case "update_bot": { -+ const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); + } + case "update_bot": { + const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; + return done && target ? (0, import_react9.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; -+ } -+ case "group_relay": + } + case "group_relay": +- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); + return (0, import_react9.createElement)(GroupReplies, { sessionId, callId: call4.callId, done, content: call4.content, roster, actions, useUi }); -+ case "create_group": -+ case "update_group": -+ case "delete_group": -+ case "post_to_group": { -+ const verbs = { -+ create_group: ["Created group", "Couldn't create the group"], -+ update_group: ["Updated group", "Couldn't update the group"], -+ delete_group: ["Deleted group", "Couldn't delete the group"], -+ post_to_group: ["Posted in", "Couldn't post in the group"] -+ }[tool]; -+ if (!done) return null; + case "create_group": + case "update_group": + case "delete_group": +@@ -4492,64 +4492,90 @@ + post_to_group: ["Posted in", "Couldn't post in the group"] + }[tool]; + if (!done) return null; +- const result = contentText(call2.content); + const result = contentText(call4.content); -+ const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); + const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); +- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); + if (call4.isError || !named) return (0, import_react9.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); -+ const name = named[2]; -+ const room2 = roster.rooms.find((entry) => entry.name === name); + const name = named[2]; + const room2 = roster.rooms.find((entry) => entry.name === name); +- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); + return (0, import_react9.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); -+ } -+ case "ask_user": { -+ const question = roster.questions?.[sessionId]; + } + case "ask_user": { + const question = roster.questions?.[sessionId]; +- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); +- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); + if (!question || question.callId !== call4.callId) return (0, import_react9.createElement)("span", { className: "bt-q-gone", hidden: true }); + return (0, import_react9.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); -+ } -+ case "request_secret": + } + case "request_secret": +- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); + return (0, import_react9.createElement)(SecretToolCell, { sessionId, callId: call4.callId, name: arg("name"), done, content: call4.content, roster, actions }); -+ case "ask_user_question": { -+ if (!done) return null; -+ let parsed = null; -+ try { + case "ask_user_question": { + if (!done) return null; + let parsed = null; + try { +- parsed = JSON.parse(contentText(call2.content)); + parsed = JSON.parse(contentText(call4.content)); -+ } catch { -+ parsed = null; + } catch { + parsed = null; } -+ const answer = parsed?.answers?.[0]; -+ const question = argValue("questions")?.[0]; -+ if (!answer) return null; + const answer = parsed?.answers?.[0]; + const question = argValue("questions")?.[0]; + if (!answer) return null; +- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); + return (0, import_react9.createElement)("div", { className: "bt-q-inline" }, (0, import_react9.createElement)(SettledQuestion, { question, answer })); -+ } -+ case "read_group_chat": { -+ if (!done) return null; -+ const room2 = roster.rooms.find((entry) => entry.name === arg("group")); + } + case "read_group_chat": { + if (!done) return null; + const room2 = roster.rooms.find((entry) => entry.name === arg("group")); +- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; + return room2 ? (0, import_react9.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; -+ } -+ case "remember": -+ case "forget": + } + case "remember": + case "forget": +- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; + return done && !call4.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call4.content), roster, actions, findBot }) : null; -+ default: -+ return null; -+ } -+} + default: + return null; + } + } +-function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { +function ToolCell({ node, sessionId, useRoster, useUi, actions }) { -+ const roster = useRoster((value) => value); -+ const root = node.data.root; -+ const turn = node.location?.turn?.turn ?? root.turn; -+ const hidden = isHiddenTurn(roster, sessionId, turn); + const roster = useRoster((value) => value); + const root = node.data.root; + const turn = node.location?.turn?.turn ?? root.turn; + const hidden = isHiddenTurn(roster, sessionId, turn); +- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); + const findBot = (nameOrId) => roster.byId[nameOrId] ?? botByName(roster.bots, nameOrId); -+ const calls = hidden ? [] : callTree(root); + const calls = hidden ? [] : callTree(root); +- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); +- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); + const drawn = calls.filter((call4) => DRAWN.has(toolName(call4))); + const running = root.kind === "tool-result" ? void 0 : calls.findLast((call4) => call4.kind !== "tool-result"); -+ const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; -+ useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); + const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; + useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); +- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); + useShownCards(actions, sessionId, drawn.filter((call4) => CARDS.has(toolName(call4)) && call4.callId).map((call4) => call4.callId)); -+ if (drawn.length === 0) return null; + if (drawn.length === 0) return null; +- const env = { sessionId, roster, actions, useUi, useWork, findBot }; +- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); + const env = { sessionId, roster, actions, useUi, findBot }; + return (0, import_react9.createElement)(import_react9.Fragment, null, drawn.map((call4, index) => (0, import_react9.createElement)(import_react9.Fragment, { key: call4.callId ?? index }, renderCall(call4, env)))); +} @@ -7563,19 +5946,21 @@ 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 + } + + // src/client/activity-row.js +-var import_react7 = require("react"); +var import_react10 = require("react"); -+var import_react_dom2 = require("react-dom"); -+var PHRASE_HOLD_MS = 800; -+var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); -+function elapsedLabel(ms) { -+ const minutes = Math.floor(ms / 6e4); -+ if (minutes < 60) return `${Math.max(1, minutes)}m`; -+ const hours = Math.floor(minutes / 60); -+ return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; -+} + var import_react_dom2 = require("react-dom"); + var PHRASE_HOLD_MS = 800; + var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); +@@ -4559,9 +4559,69 @@ + const hours = Math.floor(minutes / 60); + return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; + } +-function useRunningHost(active) { +- const [host, setHost] = (0, import_react7.useState)(null); +- (0, import_react7.useEffect)(() => { +var REMOTE_GRACE_MS = 1500; +function useAwaitingReply(roster, sessionId) { + const at = roster?.remoteAwaiting?.[sessionId]; @@ -7623,245 +6008,61 @@ diff --git a/client.js b/client.js + const check = () => { + frame = 0; + if (lastTurnOnScreen("assistant-step") > sendTurn.current) setReplied(true); - }; -- window.addEventListener("mousedown", onDown); -- window.addEventListener("keydown", onKey, true); ++ }; + const observer = new MutationObserver(() => { + if (!frame) frame = requestAnimationFrame(check); + }); + observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); + check(); - return () => { -- window.removeEventListener("mousedown", onDown); -- window.removeEventListener("keydown", onKey, true); ++ return () => { + cancelAnimationFrame(frame); + observer.disconnect(); - }; -- }, [open]); ++ }; + }, [remote, sending]); + return replied; - } --function MessageTools({ text, name, sessionId, react, actions }) { -- const [panel, setPanel] = (0, import_react6.useState)(null); -- const [place, setPlace] = (0, import_react6.useState)(null); -- const [copied, setCopied] = (0, import_react6.useState)(false); -- const tools = (0, import_react6.useRef)(null); -- const pop = (0, import_react6.useRef)(null); -- const close = () => setPanel(null); -- useDismiss(panel !== null, close, [tools, pop]); -- const open = (kind, event) => { -- if (panel === kind) { -- close(); -- return; ++} +function useRunningHost(active, where = RUNNING_HOST) { + const [host, setHost] = (0, import_react10.useState)(null); + (0, import_react10.useEffect)(() => { -+ if (!active) { -+ setHost(null); -+ return void 0; - } -- const rect = event.currentTarget.getBoundingClientRect(); -- const below = rect.bottom + 220 < window.innerHeight; -- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); -- setPanel(kind); -- }; -- const copy2 = () => { -- void navigator.clipboard?.writeText(text).then(() => { -- setCopied(true); -- setTimeout(() => setCopied(false), 1e3); -+ let frame = 0; -+ const find = () => { -+ frame = 0; + if (!active) { + setHost(null); + return void 0; +@@ -4569,7 +4569,7 @@ + let frame = 0; + const find = () => { + frame = 0; +- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); + const node = findHost(where); -+ setHost((prev) => prev === node ? prev : node); -+ }; -+ const observer = new MutationObserver(() => { -+ if (!frame) frame = requestAnimationFrame(find); - }); -- }; -- const download2 = () => { -- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); -- const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); -- link.click(); -- setTimeout(() => URL.revokeObjectURL(url), 1e3); -- }; -- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { -- close(); -- run(); -- } }, (0, import_react6.createElement)(Icon), label); -- return (0, import_react6.createElement)( -- "div", -- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, -- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, -- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, -- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), -- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( -- "div", -- { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, -- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { -- close(); -- react(emoji); -- } }, emoji)) -- ), document.body) : null, -- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( -- "div", -- { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, -- item(t("Copy"), CopyIcon, copy2), -- item(t("Download"), DownloadIcon, download2) -- ), document.body) : null -- ); --} --function ThinkingRow({ text }) { -- return (0, import_react6.createElement)( -- "details", -- { className: "bt-think" }, -- (0, import_react6.createElement)("summary", null, t("Thought it through")), -- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) -- ); --} --function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -- const tail = useTurnData?.("turn-tail"); -- const location2 = node.location; -- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; -- const seq = node.data.finalNode?.seq; -- return (0, import_react6.useMemo)(() => { -- if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; -- return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; -- }, [fileMentions, openFile, seq, tail, turn]); -+ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); -+ find(); -+ return () => { -+ cancelAnimationFrame(frame); -+ observer.disconnect(); -+ }; -+ }, [active, where]); -+ return host; - } --function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { -- const roster = useRoster((value) => value); -- const data = node.data; -- const steps = useTurnSteps(String(data.turn)); -- const contexts = useTurnContext(String(data.turn)); -- const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); -- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); -- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); -- const running = data.status === "running"; -- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); -- if (hidden) return null; -- const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); -- const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; -- if (texts2.length === 0 && !reasoning && relays.length === 0) return null; -- const selfId = roster.byId[sessionId]?.id ?? sessionId; -- return (0, import_react6.createElement)( -- import_react6.Fragment, -- null, -- relays.map((relay) => { -- const source = relay.source; -- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(EventLine, { -- key: relay.seq, -- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), -- bots: [sender], -- onOpen: () => { -- if (sender.id) actions.openExchange(selfId, sender.id); -- }, -- whole: true -+function useRowEntrances(replacing) { -+ const check = (0, import_react10.useRef)(replacing); -+ check.current = replacing; -+ (0, import_react10.useEffect)(() => { -+ const seen = /* @__PURE__ */ new WeakSet(); -+ const started = Date.now(); -+ let column = null; -+ let frame = 0; -+ const settle = (event) => { -+ if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; -+ delete event.currentTarget.dataset.btEnter; -+ event.currentTarget.removeEventListener("animationend", settle); -+ }; -+ const scan = () => { -+ frame = 0; -+ const next = document.querySelector("[data-chat-flow]"); -+ if (next !== column) { -+ observer.disconnect(); -+ column = next; -+ if (column) observer.observe(column, { childList: true, subtree: true }); -+ } -+ if (!column) return; -+ const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; -+ let last = -1; -+ items.forEach((item, index) => { -+ if (seen.has(item)) last = index; - }); -- }), -- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, -- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null -- ); --} --function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { -- if (whole && bots.length === 1) { -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)( -- "button", -- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, -- (0, import_react6.createElement)("span", null, prefix), -- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }), -- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name) -- ) -- ); -- } -- const parts = []; -- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); -- bots.forEach((bot, index) => { -- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); -- parts.push((0, import_react6.createElement)( -- "button", -- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), -- bot.name -- )); -- }); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); -+ const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); -+ for (const item of fresh) seen.add(item); -+ if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; -+ for (const item of fresh) { -+ if (items.indexOf(item) < last) continue; -+ const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); -+ item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; -+ item.addEventListener("animationend", settle); -+ } -+ }; -+ const observer = new MutationObserver(() => { -+ if (!frame) frame = requestAnimationFrame(scan); -+ }); -+ scan(); -+ return () => { -+ cancelAnimationFrame(frame); -+ observer.disconnect(); -+ }; -+ }, []); - } --function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { -- const lead = (0, import_react6.createElement)( -- "button", -- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 22 }) -- ); -- return (0, import_react6.createElement)( -+function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { -+ const roster = useRoster((value) => value); -+ const running = useSessionStatus((map) => map.get(sessionId)?.running === true); -+ const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); -+ const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); -+ const bot = roster.byId[sessionId]; -+ const room2 = roster.roomsById[sessionId]; + setHost((prev) => prev === node ? prev : node); + }; + const observer = new MutationObserver(() => { +@@ -4581,13 +4581,13 @@ + cancelAnimationFrame(frame); + observer.disconnect(); + }; +- }, [active]); ++ }, [active, where]); + return host; + } + function useRowEntrances(replacing) { +- const check = (0, import_react7.useRef)(replacing); ++ const check = (0, import_react10.useRef)(replacing); + check.current = replacing; +- (0, import_react7.useEffect)(() => { ++ (0, import_react10.useEffect)(() => { + const seen = /* @__PURE__ */ new WeakSet(); + const started = Date.now(); + let column = null; +@@ -4638,21 +4638,31 @@ + const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); + const bot = roster.byId[sessionId]; + const room2 = roster.roomsById[sessionId]; +- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); + const entries2 = (0, import_react10.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); -+ const streaming = entries2.some((entry) => entry.text); -+ const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); -+ const relaying = tools.some((entry) => entry.relay); + const streaming = entries2.some((entry) => entry.text); + const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); + const relaying = tools.some((entry) => entry.relay); +- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0); + const awaiting = useAwaitingReply(roster, sessionId); + const sendRun = (0, import_react10.useRef)(false); + if (!running) sendRun.current = false; @@ -7872,37 +6073,49 @@ diff --git a/client.js b/client.js + const bridging = bot?.remote !== void 0 && !streaming && lastRun.current.running && lastRun.current.send && !running; + lastRun.current = { running, send: sendRun.current }; + const steady = bridging; -+ const tool = tools.at(-1); + const tool = tools.at(-1); +- const inputRef = (0, import_react7.useRef)(0); + const inputRef = (0, import_react10.useRef)(0); -+ const inputAt = Math.max(inputRef.current, pendingInput); -+ inputRef.current = inputAt; + const inputAt = Math.max(inputRef.current, pendingInput); + inputRef.current = inputAt; +- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; +- const [shown, setShown] = (0, import_react7.useState)(null); + const phrase = visible ? remoteApprovalPhrase(bot?.remote !== void 0, { tool, inputAt }) : null; + const want = !visible ? null : phrase ? { ...phrase, label: t(phrase.label) } : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; + const [shown, setShown] = (0, import_react10.useState)(null); -+ const wantKey = want?.key ?? null; + const wantKey = want?.key ?? null; +- const markState = (0, import_react7.useRef)("thinking"); + const markState = (0, import_react10.useRef)("thinking"); -+ if (want) markState.current = want.state; + if (want) markState.current = want.state; +- (0, import_react7.useEffect)(() => { + (0, import_react10.useEffect)(() => { -+ if (wantKey === null || shown?.key === wantKey) return void 0; -+ const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); -+ const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; -+ if (wait <= 0) { -+ make(); -+ return void 0; -+ } -+ const timer = setTimeout(make, wait); -+ return () => clearTimeout(timer); -+ }, [wantKey, shown]); + if (wantKey === null || shown?.key === wantKey) return void 0; + const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); + const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; +@@ -4663,26 +4663,31 @@ + const timer = setTimeout(make, wait); + return () => clearTimeout(timer); + }, [wantKey, shown]); +- const [phase, setPhase] = (0, import_react7.useState)(null); +- const endedAt = (0, import_react7.useRef)(0); +- const shift = (0, import_react7.useRef)(null); +- const leaving = (0, import_react7.useRef)(false); + const [phase, setPhase] = (0, import_react10.useState)(null); + const endedAt2 = (0, import_react10.useRef)(0); + const shift = (0, import_react10.useRef)(null); + const leaving = (0, import_react10.useRef)(false); -+ leaving.current = phase !== null && !visible; + leaving.current = phase !== null && !visible; +- (0, import_react7.useEffect)(() => { + (0, import_react10.useEffect)(() => { -+ if (visible) { -+ setPhase("in"); -+ return void 0; -+ } + if (visible) { + setPhase("in"); + return void 0; + } +- setPhase((current) => current === "in" ? "out" : current); +- const timer = setTimeout(() => { +- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; +- setPhase((current) => current === "out" ? null : current); +- }, 140); + let timer; + const leave = () => { + setPhase((current) => current === "in" ? "out" : current); @@ -7913,29 +6126,35 @@ diff --git a/client.js b/client.js + }; + if (steady) timer = setTimeout(leave, REMOTE_GRACE_MS); + else leave(); -+ return () => clearTimeout(timer); -+ }, [visible]); + return () => clearTimeout(timer); + }, [visible]); +- (0, import_react7.useLayoutEffect)(() => { + (0, import_react10.useLayoutEffect)(() => { -+ if (phase !== null) return; + if (phase !== null) return; +- endedAt.current = Date.now(); + endedAt2.current = Date.now(); -+ setShown(null); -+ const before = shift.current; -+ shift.current = null; -+ const scroller = document.querySelector("[data-conversation-scroll]"); -+ if (before === null || !scroller || reducedMotion()) return; -+ const delta = before - scroller.scrollTop; -+ if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); -+ }, [phase]); + setShown(null); + const before = shift.current; + shift.current = null; +@@ -4691,31 +4691,37 @@ + const delta = before - scroller.scrollTop; + if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); + }, [phase]); +- (0, import_react7.useEffect)(() => { + (0, import_react10.useEffect)(() => { -+ if (!running) actions.clearLive(sessionId); -+ }, [running, sessionId]); + if (!running) actions.clearLive(sessionId); + }, [running, sessionId]); +- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); +- const [now, setNow] = (0, import_react7.useState)(Date.now); +- (0, import_react7.useEffect)(() => { + useRowEntrances(() => leaving.current || Date.now() - endedAt2.current < 300); + const [now, setNow] = (0, import_react10.useState)(Date.now); + (0, import_react10.useEffect)(() => { -+ if (phase !== "in") return void 0; -+ const timer = setInterval(() => setNow(Date.now()), 1e4); -+ return () => clearInterval(timer); -+ }, [phase]); + if (phase !== "in") return void 0; + const timer = setInterval(() => setNow(Date.now()), 1e4); + return () => clearInterval(timer); + }, [phase]); +- const host = useRunningHost(running); + const remote = bot?.remote !== void 0; + const host = useRunningHost(running || remote && phase !== null, running || !remote ? RUNNING_HOST : TAIL_HOST); + const drawn = (0, import_react10.useRef)(false); @@ -7943,200 +6162,52 @@ diff --git a/client.js b/client.js + (0, import_react10.useEffect)(() => { + drawn.current = Boolean(host && phase !== null && shown); + }, [host, phase, shown]); -+ if (!host || phase === null || !shown) return null; -+ const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; + if (!host || phase === null || !shown) return null; + const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; +- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)( + return (0, import_react_dom2.createPortal)((0, import_react10.createElement)( "div", -- { className: "bt-group-msg" }, -- (0, import_react6.createElement)( +- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true }, +- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), +- (0, import_react7.createElement)( + { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : void 0, "data-flybot": shown.key === "remote-approval" ? "remote-approval" : void 0, "aria-hidden": true }, + bot ? (0, import_react10.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react10.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), + (0, import_react10.createElement)( "span", -- { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, -- bot.name, -- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null -- ), -- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) -- ); --} --function GroupEvent({ prefix, room: room2, name, roster, actions }) { -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)("span", null, prefix), -- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null -- ); --} --function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { -- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; -- const self = roster.byId[sessionId]; -- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); -- const other = team || !scope ? void 0 : findBot(scope); -- const target = other && other.id !== self?.id ? other : self; -- if (!target) return null; -- const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) -- ); -+ { className: "bt-typing-text" }, + { className: "bt-typing-text" }, +- (0, import_react7.createElement)( + (0, import_react10.createElement)( -+ "span", -+ { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, + "span", + { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, +- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`), +- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null + (0, import_react10.createElement)(Shimmer, null, `${shown.text}…`), + elapsed ? (0, import_react10.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null -+ ) -+ ) -+ ), host); - } --function PartLine({ time }) { -- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null); -+ -+// src/client/dsh-offer.js -+var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); -+var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; -+var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); -+function showsDshRow(dsh, app) { -+ if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; -+ if (dsh.kind === "npm") return true; -+ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); + ) + ) + ), host); +@@ -4731,49 +4731,1666 @@ + return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); } --var scrollParent = (node) => { -- for (let at = node.parentElement; at; at = at.parentElement) { -- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; -- } -- return null; -+ + +// src/client/overlay-hooks.js +var import_react11 = require("react"); +var escapeLayers = []; +var onEscapeKey = (event) => { + if (event.key === "Escape") escapeLayers.at(-1)?.current(); - }; --function EarlierParts({ sessionId, roster, actions }) { -- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); -- const top = (0, import_react6.useRef)(null); -- const busy = (0, import_react6.useRef)(false); -- const alive = (0, import_react6.useRef)(true); -- const restore = (0, import_react6.useRef)(null); -- const load = (cursor) => { -- if (busy.current) return; -- busy.current = true; -- setView((current) => ({ ...current, loading: true, failed: false })); -- actions.earlier(sessionId, cursor).then((page) => { -- busy.current = false; -- if (!alive.current) return; -- const scroller = top.current && scrollParent(top.current); -- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; -- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); -- }, () => { -- busy.current = false; -- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); -- }); -- }; -- (0, import_react6.useEffect)(() => { -- alive.current = true; -- load(void 0); ++}; +function useEscape(onClose) { + const ref = (0, import_react11.useRef)(onClose); + ref.current = onClose; + (0, import_react11.useEffect)(() => { + if (escapeLayers.length === 0) window.addEventListener("keydown", onEscapeKey); + escapeLayers.push(ref); - return () => { -- alive.current = false; ++ return () => { + escapeLayers.splice(escapeLayers.indexOf(ref), 1); + if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); - }; -- }, [sessionId]); -- (0, import_react6.useLayoutEffect)(() => { -- const pending = restore.current; -- restore.current = null; -- if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; -- }, [view.items]); -- (0, import_react6.useEffect)(() => { -- const node = top.current; -- if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; -- const observer = new IntersectionObserver((entries2) => { -- if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); -- }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); -- observer.observe(node); -- return () => observer.disconnect(); -- }, [view.cursor, view.loading, view.failed]); -- const selfId = roster.byId[sessionId]?.id; -- const rows = []; -- for (const item of view.items) { -- const last = rows.at(-1); -- if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); -- else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); -- } -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, -- (0, import_react6.createElement)( -- "div", -- { ref: top, className: "bt-earlier-top" }, -- view.loading ? (0, import_react6.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null -- ), -- rows.map((row, index) => { -- const key = rows.length - index; -- switch (row.kind) { -- case "divider": -- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); -- case "user": -- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( -- "div", -- { className: "bt-ububble" }, -- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, -- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null -- )); -- case "answer": -- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); -- case "bot": -- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); -- case "event": { -- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); -- const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; -- const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); -- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { -- if (selfId && other.id) actions.openExchange(selfId, other.id); -- }, whole: true }); -- } -- default: -- return null; -- } -- }), -- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) -- ); ++ }; + }, []); - } --function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -- const roster = useRoster((value) => value); -- const data = node.data; -- const source = data.source !== null && typeof data.source === "object" ? data.source : {}; -- const selfId = roster.byId[sessionId]?.id ?? sessionId; -- const deferred = source.kind === "bot" && source.deferred === true; -- const relayed = isRelayContext(data); -- const turn = node.location?.turn?.turn ?? data.turn; -- (0, import_react6.useLayoutEffect)(() => { -- if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); -- }, [relayed, sessionId, turn, source.senderSessionId]); -- const order = useSessions((list) => { -- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -- return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; -- }); -- const peers = useTurnClock((state) => { -- if (!relayed) return ""; -- const turns = state[sessionId] ?? {}; -- return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); -- }); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const hiddenTurns = roster.exchangeTurns[sessionId]; -- const unreadOpening = useTurnClock((state) => { -- const first = openingTurn(state[sessionId], order, hiddenTurns); -- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; ++} +function usePaneLeft() { + const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); + (0, import_react11.useLayoutEffect)(() => { @@ -8166,100 +6237,24 @@ diff --git a/client.js b/client.js + const doing = workIn(room2, work, id); + const approvals = askedIn(room2, work, id, doing); + return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" }; - }); -- const body = triggerBody(); -- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; -- function triggerBody() { -- if (source.kind === "bot") { -- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); -- if (source.role === "kickoff" || source.role === "room") return null; -- if (source.role === "secret" || deferred) return null; -- if (source.role === "post") { -- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; -- const room2 = roster.roomsById[sessionId]; -- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); -- } -- if (source.role === "report") { -- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); -- } -- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); -- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); -- const run = relayed ? commRunOf( -- order.split(",").filter(Boolean).map(Number), -- Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), -- turn, -- hiddenTurns -- ) : null; -- if (run === "folded") return null; -- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); -- const bots = [sender]; -- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { -- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); -- } -- return (0, import_react6.createElement)(EventLine, { -- prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), -- bots, -- onOpen: (bot) => actions.openExchange(selfId, bot.id), -- whole: true -- }); -- } -- if (source.kind === "schedule") { -- const prompts = reminderPrompts(contentText(data.content)); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); -- } -- if (source.kind === "user-question-reply") return null; -- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); -- } - } --function NewDivider() { -- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New"))); ++ }); ++} +function busyMembers(room2, work, roster) { + const busy = memberWork(room2, work).filter((entry) => entry.state !== "idle" && roster.byId[entry.id]); + return [...busy.filter((entry) => entry.state === "ask"), ...busy.filter((entry) => entry.state === "work")].map((entry) => ({ ...entry, bot: roster.byId[entry.id] })); - } --function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { -- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; -- (0, import_react6.useEffect)(() => { -- if (!open) turnProcess.setOpen(true); -- }, [open, turnProcess]); -- const turn = node?.location?.turn?.turn ?? node?.data?.turn; -- const start = node?.location?.turn?.start?.time; -- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); -- (0, import_react6.useEffect)(() => { -- actions.noteTurnTime(sessionId, turn, "first", start); -- }, [sessionId, turn, start]); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const fresh = useTurnClock((state) => { -- const own = state[sessionId]?.[turn]; -- return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; -- }); -- return fresh ? (0, import_react6.createElement)(NewDivider) : null; ++} +function splitBusy(busy) { + const here = busy.filter((entry) => entry.state === "work" && entry.here); + const rows = [...here.filter((entry) => entry.doing.speaking === true), ...here.filter((entry) => entry.doing.speaking !== true)].slice(0, ROWS_MAX); + return { rows, rest: busy.filter((entry) => !rows.includes(entry)) }; - } --function Nothing() { -- return null; ++} +function botGroupWork(botId, work, roster) { + return Object.entries(work.rooms ?? {}).flatMap(([roomId, entries2]) => { + const entry = entries2.find((item) => item.botId === botId); + const room2 = roster.roomsById[roomId]; + return entry && room2 ? [{ room: room2, tool: entry.tool, step: stepIn(work, botId, roomId, entry.tool), since: entry.since }] : []; + }); - } --var SEPARATOR_GAP_MS = 30 * 60 * 1e3; --function clockLabel(time, now = Date.now()) { -- const date = new Date(time); -- const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); -- const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); -- const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); -- if (days === 0) return t("Today {time}", { time: clock4 }); -- if (days === 1) return t("Yesterday {time}", { time: clock4 }); -- const sameYear = date.getFullYear() === new Date(now).getFullYear(); -- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; ++} +function summaryText(busy) { + const asks = busy.filter((entry) => entry.state === "ask"); + const workers = busy.filter((entry) => entry.state === "work"); @@ -8268,64 +6263,26 @@ diff --git a/client.js b/client.js + const first = workers[0]; + const doing = first ? `${first.bot.name} · ${workText(first.doing.tool, first.doing.step)}${workers.length > 1 ? ` +${workers.length - 1}` : ""}` : ""; + return { waiting, doing }; - } --function nextTurnStart(turns, turn) { -- let next; -- for (const key in turns) { -- const candidate = Number(key); -- if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; -- } -- return next === void 0 ? void 0 : turns[next].first; ++} +function Face({ entry, size }) { + return (0, import_react12.createElement)( + "span", + { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 }, + (0, import_react12.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") }) + ); - } --function openingTurn(turns, firstTurns, hiddenTurns) { -- if (!turns || !firstTurns) return null; -- const start = nextTurnStart(turns, -Infinity); -- if (start === void 0) return null; -- const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]); -- const listed = firstTurns.split(",").map(Number); -- const earlier = listed.filter((entry) => entry < turn); -- return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; ++} +var elapsedSince = (now, at) => typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : ""; +function WorkRow({ bot, tool, step, label, at, now, onOpen }) { -+ const text = `${label} · ${workText(tool, step)}`; ++ const text2 = `${label} · ${workText(tool, step)}`; + const elapsed = elapsedSince(now, at); + return (0, import_react12.createElement)( + "button", -+ { type: "button", className: "bt-mw-row", "aria-label": [text, elapsed].filter(Boolean).join(" · "), onClick: onOpen }, ++ { type: "button", className: "bt-mw-row", "aria-label": [text2, elapsed].filter(Boolean).join(" · "), onClick: onOpen }, + (0, import_react12.createElement)(BotMark, { bot, size: 22, state: toolState(tool ?? ""), live: true }), -+ (0, import_react12.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react12.createElement)(Shimmer, null, text)), ++ (0, import_react12.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react12.createElement)(Shimmer, null, text2)), + elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null + ); - } --var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; --function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { -- const turn = location2.turn; -- const time = location2.end.time; -- (0, import_react6.useEffect)(() => { -- actions.noteTurnTime(sessionId, turn, "end", time); -- }, [sessionId, turn, time]); -- const label = useTurnClock((state) => { -- const start = nextTurnStart(state[sessionId] ?? {}, turn); -- return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; -- }); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const fresh = useTurnClock((state) => { -- if (typeof visit?.seen !== "number" || time > visit.seen) return false; -- const start = nextTurnStart(state[sessionId] ?? {}, turn); -- return start !== void 0 && start > visit.seen && start <= visit.entered; -- }); -- if (!label && !fresh) return null; -- return (0, import_react6.createElement)( -- import_react6.Fragment, -- null, -- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, -- fresh ? (0, import_react6.createElement)(NewDivider) : null ++} +function SummaryLine({ busy, now, onOpen }) { + const { waiting, doing } = summaryText(busy); + const elapsed = doing ? elapsedSince(now, busy.find((entry) => entry.state === "work")?.doing.since) : ""; @@ -8341,10 +6298,8 @@ diff --git a/client.js b/client.js + doing ? (0, import_react12.createElement)(Shimmer, null, doing) : null + ), + elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null - ); - } --function Shimmer({ children }) { -- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); ++ ); ++} +function MemberDock({ sessionId, useRoster, useWork, actions }) { + const roster = useRoster((value) => value); + const work = useWork((value) => value); @@ -8378,19 +6333,7 @@ diff --git a/client.js b/client.js + now, + onOpen: () => actions.openSession(item.room.id) + }))); - } --var PHRASES = { -- thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], -- working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] --}; --var TOOL_PHRASES = [ -- [/web_?search|search_?web/i, "Searching online"], -- [/fetch|browse|browser|web/i, "Reading a page"], -- [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"], -- [/^(read|view|cat)\b|read_/i, "Opening a file"], -- [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"], -- [/write|edit|patch|apply|replace/i, "Writing changes"], -- [/image|photo|draw/i, "Working on an image"] ++} +function useFlowColumn(ref, active, key) { + (0, import_react12.useLayoutEffect)(() => { + const node = ref.current; @@ -8421,51 +6364,7 @@ diff --git a/client.js b/client.js +var alwaysChoices = (approval, bot) => bot?.remote ? [] : [ + ["always-bot", "Always allow for this Bot"], + ...approval.roomId ? [["always-room", "Always allow for this group"]] : [] - ]; --var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" }; --function toolActivity(name, arg, findBot, self) { -- switch (name) { -- case "ask_user": -- return null; -- case "request_secret": -- return null; -- case "list_secrets": -- return { label: t("Checking the keys"), state: "searching" }; -- case "ask_user_question": -- return { label: t("Waiting for your answer"), state: "alert" }; -- case "group_relay": -- return { relay: true, state: "orbit" }; -- case "message_bot": { -- const target = findBot(arg("to")); -- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; -- } -- case "stop_bot": { -- const target = findBot(arg("bot")); -- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; -- } -- case "create_bot": -- return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" }; -- case "update_bot": { -- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -- return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" }; -- } -- case "list_bots": -- return { label: t("Checking the team"), state: "searching" }; -- case "read_group_chat": -- return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" }; -- case "read_own_chat": -- return { label: t("Looking back through the chat"), state: "searching" }; -- case "remember": -- return { label: t("Saving to memory"), state: "working" }; -- case "forget": -- return { label: t("Updating memory"), state: "working" }; -- case "recall": -- return { label: t("Checking memory"), state: "searching" }; -- default: -- if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" }; -- const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1]; -- return { label: phrase ? t(phrase) : null, state: toolState(name) }; -- } ++]; +function ApprovalCard({ approval, bot, actions }) { + const [busy, setBusy] = (0, import_react12.useState)(false); + const answer = (choice) => { @@ -8491,10 +6390,7 @@ diff --git a/client.js b/client.js + (0, import_react12.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name })) + ) + ); - } --function workPhrase(tool) { -- if (!tool) return t("Thinking about it"); -- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); ++} +function MemberWork({ entry, bot, room: room2, actions, now }) { + const [stopping, setStopping] = (0, import_react12.useState)(false); + if (entry.state === "ask") { @@ -8520,25 +6416,13 @@ diff --git a/client.js b/client.js + entry.here && entry.doing.handed !== true ? (0, import_react12.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null + ) + ); - } --function useLiveNote(actions, sessionId, id, entry) { -- const key = entry ? JSON.stringify(entry) : ""; -- (0, import_react6.useEffect)(() => { -- if (!key) return void 0; -- actions.noteLive(sessionId, id, JSON.parse(key)); -- return () => actions.noteLive(sessionId, id, null); -- }, [sessionId, id, key]); ++} +function useMemberWork(room2, useWork) { + const work = useWork?.((value) => value) ?? NO_WORK; + const entries2 = room2 ? memberWork(room2, work) : []; + const now = useNow(entries2.some((entry) => entry.state !== "idle")); + return { entries: entries2, now }; - } --function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { -- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); -- (0, import_react6.useEffect)(() => { -- if (done) return void 0; -- let stop = false; ++} +var MEMBER_CSS = ` +.bt-mw-dock-stack{box-sizing:border-box;display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto 6px;pointer-events:none} +.bt-mw-row{pointer-events:auto;display:flex;align-items:center;gap:10px;min-width:0;max-width:calc(100% + 6px);height:32px;margin-inline-start:-6px;padding:0 12px 0 6px;border:0;border-radius:16px;background:none;color:var(--bt-ink-2);font:inherit;font-size:14px;cursor:pointer;animation:bt-mw-in .32s cubic-bezier(.23,1,.32,1) backwards;transition:background-color .16s ease-out} @@ -8619,20 +6503,7 @@ diff --git a/client.js b/client.js + (0, import_react13.useEffect)(() => { + taskListeners.add(setTasks); + let stopped = false; - let timer; -- let seen = ""; -- const tick = async () => { -- const result = await actions.roomProgress(sessionId); -- if (stop) return; -- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; -- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; -- if (entry && key !== seen) { -- seen = key; -- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); -- } -- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); -- }; -- void tick(); ++ let timer; + const load = () => Promise.resolve(actions?.tasks?.({ archive: true })).then((list) => { + if (!stopped && list !== void 0) publishTasks(list); + }).catch(() => { @@ -8640,19 +6511,11 @@ diff --git a/client.js b/client.js + if (!stopped) timer = setTimeout(load, POLL_MS2); + }); + load(); - return () => { -- stop = true; ++ return () => { + stopped = true; - clearTimeout(timer); ++ clearTimeout(timer); + taskListeners.delete(setTasks); - }; -- }, [done, sessionId, callId]); -- let replies = live.replies; -- if (done) { -- try { -- replies = JSON.parse(contentText(content)).replies ?? []; -- } catch { -- replies = []; ++ }; + }, []); + return tasks; +} @@ -8662,99 +6525,28 @@ diff --git a/client.js b/client.js +var workflowPoll = null; +function publishWorkflows(settled) { + sharedWorkflows = workflowsOf(settled); -+ for (const listener of workflowListeners) listener(sharedWorkflows); -+} -+function useWorkflows(actions) { -+ const [workflows, setWorkflows] = (0, import_react13.useState)(sharedWorkflows); -+ (0, import_react13.useEffect)(() => { -+ workflowListeners.add(setWorkflows); -+ if (workflowPoll === null && typeof actions?.workflowState === "function") { -+ const load = () => Promise.resolve(actions.workflowState()).then(publishWorkflows, () => { -+ }); -+ load(); -+ workflowPoll = setInterval(load, POLL_MS2); - } -- } -- const pending = done || !live.speaking ? [] : [live.speaking]; -- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; -- if (replies.length === 0 && pending.length === 0) return null; -- const room2 = roster.roomsById[sessionId]; -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-group-replies" }, -- replies.map((reply, index) => { -- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; -- if (reply.kind === "user") { -- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( -- "div", -- { className: "bt-ububble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) -- )); -- } -- if (reply.kind === "failed") { -- return (0, import_react6.createElement)( -- "div", -- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, -- t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) -- ); -- } -- if (reply.kind === "lookup") { -- return (0, import_react6.createElement)( -- "div", -- { key: `${reply.botId}-${index}`, className: "bt-event" }, -- reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) -- ); -- } -- if (reply.kind === "missed") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); -- } -- if (reply.kind === "resumed") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); -- } -- if (reply.kind === "cut") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); -- } -- if (reply.kind === "approval" || reply.kind === "working") { -- return (0, import_react6.createElement)( -- "div", -- { key: `${reply.botId}-${index}`, className: "bt-event" }, -- (0, import_react6.createElement)( -- "button", -- { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), -- (0, import_react6.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) -- ) -- ); -- } -- if (reply.kind === "stopped") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); ++ for (const listener of workflowListeners) listener(sharedWorkflows); ++} ++function useWorkflows(actions) { ++ const [workflows, setWorkflows] = (0, import_react13.useState)(sharedWorkflows); ++ (0, import_react13.useEffect)(() => { ++ workflowListeners.add(setWorkflows); ++ if (workflowPoll === null && typeof actions?.workflowState === "function") { ++ const load = () => Promise.resolve(actions.workflowState()).then(publishWorkflows, () => { ++ }); ++ load(); ++ workflowPoll = setInterval(load, POLL_MS2); ++ } + return () => { + workflowListeners.delete(setWorkflows); + if (workflowListeners.size === 0 && workflowPoll !== null) { + clearInterval(workflowPoll); + workflowPoll = null; - } -- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); -- }), -- pending.map((id) => { -- const bot = roster.byId[id]; -- if (!bot) return null; -- return (0, import_react6.createElement)( -- "div", -- { key: `pending-${id}`, className: "bt-activity" }, -- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), -- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) -- ); -- }) -- ); ++ } + }; + }, []); + return workflows; - } --var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); --var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); --function callTree(root) { -- const calls = []; ++} +var ownBot = (roster, id) => { + const bot = roster.byId?.[id]; + if (bot === void 0 || bot.remote) return void 0; @@ -8766,11 +6558,11 @@ diff --git a/client.js b/client.js +var placeOf = (roster, roomId) => roomId && roomName(roster, roomId) ? t("in {name}", { name: roomName(roster, roomId) }) : ""; +var lastWhy = (task) => [...task.actors ?? []].reverse().find((entry) => typeof entry.why === "string" && entry.why !== "")?.why ?? ""; +var chatOfTask = (task) => task.origin?.roomId ?? task.origin?.sessionId ?? task.ownerBotId; -+var ownAskChat = (roster, bot, text) => { -+ if (bot === void 0 || typeof text !== "string" || text === "") return void 0; ++var ownAskChat = (roster, bot, text2) => { ++ if (bot === void 0 || typeof text2 !== "string" || text2 === "") return void 0; + for (const [key, question] of Object.entries(roster.questions ?? {})) { + if (question == null || typeof question !== "object" || roster.roomsById?.[key]) continue; -+ if (question.question !== text) continue; ++ if (question.question !== text2) continue; + if (key === bot.id || key === currentPart(bot)) return bot.id; + } + return void 0; @@ -8786,7 +6578,7 @@ diff --git a/client.js b/client.js + if (entry) options.push(`${workPhrase(entry.tool)}…`); + } + if (sessions?.byId && card.chat) options.push(previewOf(sessions, partOf(roster, card.chat), [], [], botNames(roster))); -+ return options.find((text) => typeof text === "string" && text !== "" && text !== card.title && text !== card.reason) ?? ""; ++ return options.find((text2) => typeof text2 === "string" && text2 !== "" && text2 !== card.title && text2 !== card.reason) ?? ""; +} +function toolCalls(card, { roster, work }) { + const botId = card.bot?.id; @@ -8868,13 +6660,7 @@ diff --git a/client.js b/client.js + const count = room2.pendingCount ?? 0; + if (count > 0) held(room2.id, count, { room: room2, place: placeOf(roster, room2.id), ...people(roster, null) }); + } - const seen = /* @__PURE__ */ new Set(); -- const visit = (call2) => { -- const id = call2.callId ?? call2; -- if (seen.has(id)) return; -- seen.add(id); -- calls.push(call2); -- for (const child of call2.subCalls ?? []) visit(child); ++ const seen = /* @__PURE__ */ new Set(); + const running = (botId, entry, roomId) => { + const bot = ownBot(roster, botId); + const key = `${botId}|${roomId ?? ""}`; @@ -8892,63 +6678,7 @@ diff --git a/client.js b/client.js + chat: roomId ?? bot.id, + ...people(roster, bot.id) + }); - }; -- visit(root); -- return calls; --} --function useShownCards(actions, sessionId, callIds) { -- const key = JSON.stringify(callIds); -- (0, import_react6.useLayoutEffect)(() => { -- const ids = JSON.parse(key); -- if (ids.length === 0 || !actions.noteCards) return void 0; -- actions.noteCards(sessionId, ids, 1); -- return () => actions.noteCards(sessionId, ids, -1); -- }, [sessionId, key]); --} --function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { -- const tool = toolName(call2); -- const done = call2.kind === "tool-result"; -- const { text: arg, value: argValue } = toolArgs(call2); -- const self = roster.byId[sessionId]; -- switch (tool) { -- case "message_bot": { -- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "stop_bot": { -- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "create_bot": { -- const created = findBot(arg("name")); -- if (!done || created === void 0) return null; -- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); -- } -- case "update_bot": { -- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; -- } -- case "group_relay": -- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); -- case "create_group": -- case "update_group": -- case "delete_group": -- case "post_to_group": { -- const verbs = { -- create_group: ["Created group", "Couldn't create the group"], -- update_group: ["Updated group", "Couldn't update the group"], -- delete_group: ["Deleted group", "Couldn't delete the group"], -- post_to_group: ["Posted in", "Couldn't post in the group"] -- }[tool]; -- if (!done) return null; -- const result = contentText(call2.content); -- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); -- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); -- const name = named[2]; -- const room2 = roster.rooms.find((entry) => entry.name === name); -- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); ++ }; + for (const [botId, entry] of Object.entries(work.bots ?? {})) running(botId, entry, entry.roomId); + for (const [roomId, entries2] of Object.entries(work.rooms ?? {})) for (const entry of entries2) running(entry.botId, entry, roomId); + const lastEnded = /* @__PURE__ */ new Map(); @@ -9007,11 +6737,7 @@ diff --git a/client.js b/client.js + const at2 = endedAt(run); + if (!fresh(at2)) continue; + if (latest === void 0 || at2 > endedAt(latest)) latest = run; - } -- case "ask_user": { -- const question = roster.questions?.[sessionId]; -- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); -- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); ++ } + if (latest === void 0) continue; + const at = endedAt(latest); + const name = typeof graph.name === "string" && graph.name.trim() !== "" ? graph.name.trim() : String(graph.id ?? t("A workflow")); @@ -9037,19 +6763,10 @@ diff --git a/client.js b/client.js + const part = currentPart(bot); + const at = sessions.byId[part]?.updatedAt; + if (!fresh(at)) continue; -+ const text = previewOf(sessions, part, [], [], botNames(roster)); -+ if (text === "") continue; -+ done.push({ id: `chat:${bot.id}`, kind: "chat", status: "done", bot, at, title: text, place: "", chat: bot.id, ...people(roster, bot.id) }); - } -- case "request_secret": -- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); -- case "ask_user_question": { -- if (!done) return null; -- let parsed = null; -- try { -- parsed = JSON.parse(contentText(call2.content)); -- } catch { -- parsed = null; ++ const text2 = previewOf(sessions, part, [], [], botNames(roster)); ++ if (text2 === "") continue; ++ done.push({ id: `chat:${bot.id}`, kind: "chat", status: "done", bot, at, title: text2, place: "", chat: bot.id, ...people(roster, bot.id) }); ++ } + } + const newest = (a, b) => (b.at ?? Infinity) - (a.at ?? Infinity); + for (const card of [...needs, ...progress, ...done]) { @@ -9071,10 +6788,10 @@ diff --git a/client.js b/client.js + return Promise.resolve(actions.answerQuestion(card.sessionId, card.question.id, { selected, custom })); + } + if (card?.kind === "task" && card.status === "input") { -+ const text = typeof choice === "string" ? choice.trim() : ""; ++ const text2 = typeof choice === "string" ? choice.trim() : ""; + const questionId = card.task?.inputRequest?.id; -+ if (text === "" || typeof questionId !== "string") return void 0; -+ return Promise.resolve(actions.answerTaskInput(card.task.id, questionId, text)); ++ if (text2 === "" || typeof questionId !== "string") return void 0; ++ return Promise.resolve(actions.answerTaskInput(card.task.id, questionId, text2)); + } + if (card?.kind === "task" && card.status === "result") { + if (choice === "accept") return Promise.resolve(actions.acceptTaskResult(card.task.id)); @@ -9132,11 +6849,7 @@ diff --git a/client.js b/client.js + if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; + event.preventDefault(); + send(); - } -- const answer = parsed?.answers?.[0]; -- const question = argValue("questions")?.[0]; -- if (!answer) return null; -- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); ++ } + }) : null, + slot.open ? (0, import_react13.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: send }, t("Send it back")) : (0, import_react13.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + slot.open = true; @@ -9161,11 +6874,7 @@ diff --git a/client.js b/client.js + if (selected.length === 0 && custom === "") return; + onAnswer(card, { selected, custom }); + return; - } -- case "read_group_chat": { -- if (!done) return null; -- const room2 = roster.rooms.find((entry) => entry.name === arg("group")); -- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; ++ } + if (custom === "") return; + onAnswer(card, custom); + }; @@ -9174,12 +6883,7 @@ diff --git a/client.js b/client.js + if (!multi) { + onAnswer(card, { selected: [label], custom: "" }); + return; - } -- case "remember": -- case "forget": -- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; -- default: -- return null; ++ } + slot.picked = slot.picked.includes(label) ? slot.picked.filter((item) => item !== label) : [...slot.picked, label]; + redraw((count) => count + 1); + }; @@ -9321,7 +7025,7 @@ diff --git a/client.js b/client.js + if (card?.kind === "workflow") { + actions.openSettings("workflow"); + return; - } ++ } + actions.openSession(card.chat); +} +function InboxDialog({ roster, useWork, useSessions, actions, leaving = false }) { @@ -9400,22 +7104,7 @@ diff --git a/client.js b/client.js + ) + ) + ); - } --function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { -- const roster = useRoster((value) => value); -- const root = node.data.root; -- const turn = node.location?.turn?.turn ?? root.turn; -- const hidden = isHiddenTurn(roster, sessionId, turn); -- const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); -- const calls = hidden ? [] : callTree(root); -- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); -- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); -- const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; -- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); -- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); -- if (drawn.length === 0) return null; -- const env = { sessionId, roster, actions, useUi, useWork, findBot }; -- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); ++} +var INBOX_CSS = ` +.bt-inbox-row{min-height:40px;padding:6px 8px;gap:10px} +.bt-inbox-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} @@ -9514,22 +7203,12 @@ diff --git a/client.js b/client.js + const value = task?.progress; + if (typeof value !== "number" || !Number.isFinite(value)) return columnOf(task) === "done" && task?.status !== "cancelled" && task?.status !== "failed" ? 100 : 0; + return Math.max(0, Math.min(100, Math.round(value))); - } - --// src/client/activity-row.js --var import_react7 = require("react"); --var import_react_dom2 = require("react-dom"); --var PHRASE_HOLD_MS = 800; --var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); --function elapsedLabel(ms) { -- const minutes = Math.floor(ms / 6e4); -- if (minutes < 60) return `${Math.max(1, minutes)}m`; -- const hours = Math.floor(minutes / 60); -- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; ++} ++ +// src/shared/board-card.js +function taskCardKey(id) { -+ const text = String(id ?? "").trim(); -+ return text === "" ? null : `task:${text}`; ++ const text2 = String(id ?? "").trim(); ++ return text2 === "" ? null : `task:${text2}`; +} +function taskCard(task) { + const key = taskCardKey(task?.id); @@ -9540,13 +7219,7 @@ diff --git a/client.js b/client.js + id: String(task.id), + title: String(task.title ?? "") + }; - } --function useRunningHost(active) { -- const [host, setHost] = (0, import_react7.useState)(null); -- (0, import_react7.useEffect)(() => { -- if (!active) { -- setHost(null); -- return void 0; ++} + +// src/client/board-card.js +var import_react14 = require("react"); @@ -9688,12 +7361,7 @@ diff --git a/client.js b/client.js + for (const bot of children.get(parent) ?? []) { + rows.push({ kind: "bot", id: bot.id, name: bot.name, depth, bot }); + walk(bot.id, depth + 1); - } -- let frame = 0; -- const find = () => { -- frame = 0; -- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); -- setHost((prev) => prev === node ? prev : node); ++ } + }; + walk("", 0); + const groups = (roster?.rooms ?? []).filter((room2) => room2 && room2.hidden !== true && room2.origin !== "face-to-face" && adminOf(roster, room2)).map((room2) => ({ kind: "group", id: room2.id, name: room2.displayName || room2.name || t("Group"), room: room2 })); @@ -9779,22 +7447,13 @@ diff --git a/client.js b/client.js + if (stopped || !Array.isArray(list)) return; + setArchive(list.filter((task) => columnOf(task) === "done")); + }).catch(() => void 0); - }; -- const observer = new MutationObserver(() => { -- if (!frame) frame = requestAnimationFrame(find); -- }); -- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); -- find(); ++ }; + load(); + const timer = setInterval(load, POLL_MS3); - return () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); ++ return () => { + stopped = true; + clearInterval(timer); - }; -- }, [active]); -- return host; ++ }; + }, [key]); + const tasks = scope?.kind === "group" ? groupTasks : own.filter((task) => !scope?.botId || task.ownerBotId === scope.botId); + const replace = (task) => { @@ -9805,42 +7464,7 @@ diff --git a/client.js b/client.js + }; + const done = scope?.kind === "group" ? archive : archive.filter((task) => !scope?.botId || task.ownerBotId === scope.botId); + return { own, tasks, archive: done, replace }; - } --function useRowEntrances(replacing) { -- const check = (0, import_react7.useRef)(replacing); -- check.current = replacing; -- (0, import_react7.useEffect)(() => { -- const seen = /* @__PURE__ */ new WeakSet(); -- const started = Date.now(); -- let column = null; -- let frame = 0; -- const settle = (event) => { -- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; -- delete event.currentTarget.dataset.btEnter; -- event.currentTarget.removeEventListener("animationend", settle); -- }; -- const scan = () => { -- frame = 0; -- const next = document.querySelector("[data-chat-flow]"); -- if (next !== column) { -- observer.disconnect(); -- column = next; -- if (column) observer.observe(column, { childList: true, subtree: true }); -- } -- if (!column) return; -- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; -- let last = -1; -- items.forEach((item, index) => { -- if (seen.has(item)) last = index; -- }); -- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); -- for (const item of fresh) seen.add(item); -- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; -- for (const item of fresh) { -- if (items.indexOf(item) < last) continue; -- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); -- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; -- item.addEventListener("animationend", settle); ++} +function RailButton({ row, selected, meta, onSelect }) { + return (0, import_react15.createElement)( + "button", @@ -9933,17 +7557,7 @@ diff --git a/client.js b/client.js + const id = event.dataTransfer?.getData("text/plain"); + if (id) onMove?.(id, column); + onDrag?.(null); - } -- }; -- const observer = new MutationObserver(() => { -- if (!frame) frame = requestAnimationFrame(scan); -- }); -- scan(); -- return () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); -- }; -- }, []); ++ } + }, + (0, import_react15.createElement)( + "h3", @@ -9971,81 +7585,7 @@ diff --git a/client.js b/client.js + lane.tasks.map((task) => (0, import_react15.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) + )) : plain.map((task) => (0, import_react15.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag) })) + ); - } --function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { -- const roster = useRoster((value) => value); -- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); -- const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); -- const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); -- const bot = roster.byId[sessionId]; -- const room2 = roster.roomsById[sessionId]; -- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); -- const streaming = entries2.some((entry) => entry.text); -- const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); -- const relaying = tools.some((entry) => entry.relay); -- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0); -- const tool = tools.at(-1); -- const inputRef = (0, import_react7.useRef)(0); -- const inputAt = Math.max(inputRef.current, pendingInput); -- inputRef.current = inputAt; -- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; -- const [shown, setShown] = (0, import_react7.useState)(null); -- const wantKey = want?.key ?? null; -- const markState = (0, import_react7.useRef)("thinking"); -- if (want) markState.current = want.state; -- (0, import_react7.useEffect)(() => { -- if (wantKey === null || shown?.key === wantKey) return void 0; -- const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); -- const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; -- if (wait <= 0) { -- make(); -- return void 0; -- } -- const timer = setTimeout(make, wait); -- return () => clearTimeout(timer); -- }, [wantKey, shown]); -- const [phase, setPhase] = (0, import_react7.useState)(null); -- const endedAt = (0, import_react7.useRef)(0); -- const shift = (0, import_react7.useRef)(null); -- const leaving = (0, import_react7.useRef)(false); -- leaving.current = phase !== null && !visible; -- (0, import_react7.useEffect)(() => { -- if (visible) { -- setPhase("in"); -- return void 0; -- } -- setPhase((current) => current === "in" ? "out" : current); -- const timer = setTimeout(() => { -- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; -- setPhase((current) => current === "out" ? null : current); -- }, 140); -- return () => clearTimeout(timer); -- }, [visible]); -- (0, import_react7.useLayoutEffect)(() => { -- if (phase !== null) return; -- endedAt.current = Date.now(); -- setShown(null); -- const before = shift.current; -- shift.current = null; -- const scroller = document.querySelector("[data-conversation-scroll]"); -- if (before === null || !scroller || reducedMotion()) return; -- const delta = before - scroller.scrollTop; -- if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); -- }, [phase]); -- (0, import_react7.useEffect)(() => { -- if (!running) actions.clearLive(sessionId); -- }, [running, sessionId]); -- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); -- const [now, setNow] = (0, import_react7.useState)(Date.now); -- (0, import_react7.useEffect)(() => { -- if (phase !== "in") return void 0; -- const timer = setInterval(() => setNow(Date.now()), 1e4); -- return () => clearInterval(timer); -- }, [phase]); -- const host = useRunningHost(running); -- if (!host || phase === null || !shown) return null; -- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; -- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)( ++} +function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate }) { + const columns = boardColumns(tasks); + const lanes = scope?.kind === "group"; @@ -10154,11 +7694,11 @@ diff --git a/client.js b/client.js + } + }; + const create = async (title) => { -+ const text = String(title ?? "").trim(); -+ if (!text || scope?.kind !== "bot") return; ++ const text2 = String(title ?? "").trim(); ++ if (!text2 || scope?.kind !== "bot") return; + setError(""); + try { -+ replace(await actions.taskCreate({ botId: scope.botId, title: text, column: "not-started" })); ++ replace(await actions.taskCreate({ botId: scope.botId, title: text2, column: "not-started" })); + setDraft(""); + } catch (failure) { + setError(failure?.message ?? String(failure)); @@ -10166,18 +7706,7 @@ diff --git a/client.js b/client.js + }; + const name = scopeName(roster, scope); + return (0, import_react15.createElement)( - "div", -- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true }, -- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), -- (0, import_react7.createElement)( -- "span", -- { className: "bt-typing-text" }, -- (0, import_react7.createElement)( -- "span", -- { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, -- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`), -- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null -- ) ++ "div", + { className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 }, + (0, import_react15.createElement)("div", { className: "bt-scrim", onClick: close }), + (0, import_react15.createElement)( @@ -10206,19 +7735,9 @@ diff --git a/client.js b/client.js + onDrag: setDragging, + onCreate: create + }) - ) -- ), host); ++ ) + ); - } -- --// src/client/dsh-offer.js --var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); --var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; --var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); --function showsDshRow(dsh, app) { -- if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; -- if (dsh.kind === "npm") return true; -- return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); ++} +var KANBAN_CSS = ` +.bt-board-row{min-height:40px;padding:6px 8px;gap:10px} +.bt-board-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} @@ -10280,9 +7799,9 @@ diff --git a/client.js b/client.js + .bt-mem.bt-board{width:100vw;border-radius:0;border:0} + .bt-board-columns{flex-direction:column} + .bt-board-column{min-width:0} - } ++} +`; - ++ // src/client/sidebar.js -var import_react8 = require("react"); +var import_react16 = require("react"); @@ -10298,7 +7817,8 @@ diff --git a/client.js b/client.js +var PinMark = () => (0, import_react16.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react16.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) => { +-var workKeys = (text) => text === "" ? [] : text.split(",").map((key) => { ++var workKeys = (text2) => text2 === "" ? [] : text2.split(",").map((key) => { const [id, tool] = key.split("|"); return { id, tool: tool || void 0 }; }); @@ -10346,7 +7866,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4785,55 +4785,131 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio +@@ -4785,55 +4785,131 @@ onClick: () => actions.openSession(id), onContextMenu }, @@ -10493,7 +8013,7 @@ diff --git a/client.js b/client.js "data-room-id": id, onClick: () => actions.openSession(id), onContextMenu: (event) => { -@@ -4841,20 +4841,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, +@@ -4841,20 +4841,20 @@ actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); } }, @@ -10522,7 +8042,7 @@ diff --git a/client.js b/client.js if (!renaming) return; input.current?.focus(); input.current?.select(); -@@ -4875,10 +4875,10 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { +@@ -4875,10 +4875,10 @@ } })(); }; @@ -10535,7 +8055,7 @@ diff --git a/client.js b/client.js ref: input, defaultValue: session.title, "aria-label": t("Rename"), -@@ -4891,7 +4891,7 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { +@@ -4891,7 +4891,7 @@ }) ); } @@ -10544,7 +8064,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4905,8 +4905,8 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { +@@ -4905,8 +4905,8 @@ actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -10555,7 +8075,7 @@ diff --git a/client.js b/client.js ); } function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { -@@ -4941,10 +4941,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4941,10 +4941,10 @@ const preview = sessions[0]?.title ?? t("New session"); const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); const open = active && wide; @@ -10568,7 +8088,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4959,30 +4959,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4959,30 +4959,30 @@ actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -10610,7 +8130,7 @@ diff --git a/client.js b/client.js key: session.id, session, agent, -@@ -4996,8 +4996,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4996,8 +4996,8 @@ } var GLIDE = springEasing(1e3, 63); function useListGlide(listRef, layout, key) { @@ -10621,7 +8141,7 @@ diff --git a/client.js b/client.js const list = listRef.current; if (!list) return; const next = /* @__PURE__ */ new Map(); -@@ -5014,9 +5014,44 @@ function useListGlide(listRef, layout, key) { +@@ -5014,9 +5014,44 @@ tops.current = { layout, map: next }; }, [layout, key]); } @@ -10667,7 +8187,7 @@ diff --git a/client.js b/client.js const roster = useRoster((value) => value); const markedUnread = useUi((value) => value.unread); props.useRegistry((value) => value); -@@ -5025,17 +5025,26 @@ function TeamSidebar(props) { +@@ -5025,17 +5025,26 @@ for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; return JSON.stringify(stamps); }); @@ -10698,7 +8218,7 @@ diff --git a/client.js b/client.js useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(",")); useLandOnMainBot(roster, useSessions, actions); const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide }; -@@ -5045,39 +5045,42 @@ function TeamSidebar(props) { +@@ -5045,39 +5045,42 @@ event.preventDefault(); actions.openPalette(event.key); }; @@ -10761,7 +8281,7 @@ diff --git a/client.js b/client.js if (!open) return void 0; const onDown = (event) => { if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; -@@ -5116,14 +5116,14 @@ function UpdateButton({ useUpdate, actions }) { +@@ -5116,14 +5116,14 @@ setDshError(""); void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); }; @@ -10781,7 +8301,7 @@ diff --git a/client.js b/client.js ref: button, type: "button", className: "bt-round bt-update", -@@ -5133,21 +5133,21 @@ function UpdateButton({ useUpdate, actions }) { +@@ -5133,21 +5133,21 @@ "aria-haspopup": "dialog", "aria-expanded": open, onClick: toggle @@ -10814,7 +8334,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-update-go", disabled: phase === "installing", -@@ -5155,17 +5155,17 @@ function UpdateButton({ useUpdate, actions }) { +@@ -5155,17 +5155,17 @@ }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) ) ) : null, @@ -10839,7 +8359,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-update-go", disabled: dshPhase === "installing", -@@ -5176,61 +5176,25 @@ function UpdateButton({ useUpdate, actions }) { +@@ -5176,61 +5176,25 @@ ), document.body) : null ); } @@ -10911,7 +8431,7 @@ diff --git a/client.js b/client.js ); } var openShellSettings = null; -@@ -5259,6 +5259,18 @@ function openHarnessOnboarding(id) { +@@ -5259,6 +5259,18 @@ else openHarnessSettings(); }); } @@ -10930,7 +8450,7 @@ diff --git a/client.js b/client.js function openThroughMenu(trigger) { const before = new Set(document.querySelectorAll('[role="menu"]')); trigger.click(); -@@ -5288,32 +5288,32 @@ function pressSettingsShortcut() { +@@ -5288,32 +5288,32 @@ })); } function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { @@ -10971,7 +8491,7 @@ diff --git a/client.js b/client.js if (!open) return void 0; const onDown = (event) => { if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; -@@ -5341,30 +5341,32 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi +@@ -5341,30 +5341,32 @@ const groups = [ [ [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], @@ -11016,7 +8536,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -5372,7 +5372,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi +@@ -5372,7 +5372,7 @@ setOpen(false); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } @@ -11025,7 +8545,7 @@ diff --git a/client.js b/client.js )) ), document.body) : null ); -@@ -5382,8 +5382,8 @@ function useLandOnMainBot(roster, useSessions, actions) { +@@ -5382,8 +5382,8 @@ const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; }); @@ -11036,7 +8556,7 @@ diff --git a/client.js b/client.js if (!roster.ready || !roster.mainBotId) return void 0; const [id, blank3] = mainView.split("|"); const bot = id === "" ? void 0 : roster.byId[id]; -@@ -5397,40 +5397,40 @@ function useLandOnMainBot(roster, useSessions, actions) { +@@ -5397,40 +5397,40 @@ return void 0; } const last = lastBot.current; @@ -11087,7 +8607,7 @@ diff --git a/client.js b/client.js } var SIDEBAR_CSS = ` .bt-side-top .bt-brand{cursor:default;padding-left:6px} -@@ -5456,7 +5456,7 @@ var SIDEBAR_CSS = ` +@@ -5456,7 +5456,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)} @@ -11096,7 +8616,7 @@ diff --git a/client.js b/client.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)} -@@ -5491,18 +5491,140 @@ var SIDEBAR_CSS = ` +@@ -5491,18 +5491,140 @@ .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}} @@ -11182,7 +8702,7 @@ diff --git a/client.js b/client.js + const host = useRunningHost2(visible); + const shown = (0, import_react17.useRef)(null); + if (visible) shown.current = room2 ? { text: memberAck?.text ?? "", at: memberAck?.at ?? Date.now() } : { text: busyAckText(hostAck), at: hostAck?.at ?? Date.now() }; -+ const text = shown.current?.text ?? ""; ++ const text2 = shown.current?.text ?? ""; + if (!visible || !host || !shown.current || !bot) return null; + return (0, import_react_dom4.createPortal)((0, import_react17.createElement)( + "div", @@ -11191,8 +8711,8 @@ diff --git a/client.js b/client.js + "data-flybot": "busy-ack", + role: "status", + "aria-live": "polite", -+ "aria-busy": text ? void 0 : "true", -+ "data-pending": text ? void 0 : "" ++ "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)( @@ -11204,7 +8724,7 @@ diff --git a/client.js b/client.js + (0, import_react17.createElement)( + "div", + { className: "bt-msg-line" }, -+ text ? (0, import_react17.createElement)("div", { className: "bt-bubble" }, text) : (0, import_react17.createElement)( ++ text2 ? (0, import_react17.createElement)("div", { className: "bt-bubble" }, text2) : (0, import_react17.createElement)( + "div", + { className: "bt-bubble bt-busy-dots", "aria-hidden": "true" }, + (0, import_react17.createElement)("span", null), @@ -11242,7 +8762,7 @@ diff --git a/client.js b/client.js const left = seen + MEMORY_NOTICE_MS - Date.now(); setShown(seen > 0 && left > 0); if (seen === 0 || left <= 0) return void 0; -@@ -5512,19 +5512,19 @@ function useMemoryNotice(ids, useMemoryNews) { +@@ -5512,19 +5512,19 @@ return shown; } function MemoryNotice({ shown }) { @@ -11268,7 +8788,7 @@ diff --git a/client.js b/client.js const roster = useRoster((value) => value); const title = useSessions((list) => list.byId[sessionId]?.displayTitle ?? list.byId[sessionId]?.title ?? ""); const firstTurns = useSessions((list) => { -@@ -5537,7 +5537,7 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe +@@ -5537,7 +5537,7 @@ const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; }); @@ -11277,7 +8797,7 @@ diff --git a/client.js b/client.js if (opening) document.body.style.setProperty("--bt-first-sep", JSON.stringify(opening)); else document.body.style.removeProperty("--bt-first-sep"); return () => document.body.style.removeProperty("--bt-first-sep"); -@@ -5548,52 +5548,41 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe +@@ -5548,52 +5548,41 @@ const bot = roster.byId[sessionId]; const room2 = roster.roomsById[sessionId]; const detailsOpen = useUi((state) => state.details === sessionId); @@ -11327,14 +8847,15 @@ 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)) +- ), +- (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 }) + 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 }), @@ -11349,7 +8870,7 @@ diff --git a/client.js b/client.js let frame = 0; let watched = null; const observer = new MutationObserver(() => schedule()); -@@ -5642,7 +5642,7 @@ function useTranscriptView() { +@@ -5642,7 +5642,7 @@ }, []); return view; } @@ -11358,7 +8879,7 @@ diff --git a/client.js b/client.js const replies = useSessions((list) => { const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; return Array.isArray(outline) ? outline.reduce((count, entry) => count + (entry.response ? 1 : 0), 0) : 0; -@@ -5658,18 +5658,22 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { +@@ -5658,18 +5658,22 @@ }); const view = useTranscriptView(); const unpinned = view?.unpinned === true; @@ -11386,7 +8907,7 @@ diff --git a/client.js b/client.js const scroller = document.querySelector("[data-conversation-scroll]"); if (!scroller) return void 0; scroller.toggleAttribute("data-bt-news", showDown); -@@ -5677,16 +5677,16 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { +@@ -5677,16 +5677,16 @@ }, [showDown]); if (!view || !showDown && !showUp) return null; const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages"); @@ -11407,7 +8928,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-news-x", "aria-label": t("Dismiss new messages"), -@@ -5694,7 +5694,7 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { +@@ -5694,7 +5694,7 @@ event.stopPropagation(); onDismiss(); } @@ -11416,32 +8937,23 @@ diff --git a/client.js b/client.js ); const toLatest = () => { const scroller = document.querySelector("[data-conversation-scroll]"); -@@ -5702,31 +5702,147 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { - if (back) back.click(); +@@ -5703,9 +5703,9 @@ else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); }; -- const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); + const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); - return (0, import_react_dom4.createPortal)( - (0, import_react9.createElement)( - import_react9.Fragment, -- 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 -- ), -- document.body -- ); -+ 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, -+ 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 -+ ), -+ document.body -+ ); -+} -+ + null, + showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, + showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null +@@ -5714,19 +5714,135 @@ + ); + } + +// src/client/composer-label.js +var ATTRIBUTES = ["aria-label", "data-placeholder"]; +function holdComposerLabel(doc, label) { @@ -11556,8 +9068,8 @@ diff --git a/client.js b/client.js + for (const held of slots.values()) held.off(); + slots.clear(); + }; - } - ++} ++ // src/client/composer.js -var import_react10 = require("react"); -var import_react_dom5 = require("react-dom"); @@ -11582,7 +9094,21 @@ diff --git a/client.js b/client.js if (!note) return void 0; const timer = setTimeout(() => setNote(""), 2600); return () => clearTimeout(timer); -@@ -5774,19 +5774,38 @@ function useDictation(onText) { +@@ -5745,10 +5745,10 @@ + let interim = ""; + for (let index = event.resultIndex; index < event.results.length; index += 1) { + const result = event.results[index]; +- const text = result[0]?.transcript ?? ""; ++ const text2 = result[0]?.transcript ?? ""; + if (result.isFinal) { +- if (text.trim()) latest.current(text, true); +- } else interim += text; ++ if (text2.trim()) latest.current(text2, true); ++ } else interim += text2; + } + if (interim) latest.current(interim, false); + }; +@@ -5774,19 +5774,38 @@ const stop = () => recognition.current?.stop(); return { listening, note, start, stop }; } @@ -11606,8 +9132,10 @@ diff --git a/client.js b/client.js + ); +} function ComposerVoice({ sessionId, inputActions, actions, useUi }) { - const dictation = useDictation((text, final) => { - if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); +- const dictation = useDictation((text, final) => { +- if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); ++ const dictation = useDictation((text2, final) => { ++ if (final && inputActions) inputActions.insertText(text2, inputActions.captureInsertion()); }); const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); - (0, import_react10.useEffect)(() => { @@ -11624,7 +9152,7 @@ diff --git a/client.js b/client.js const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); if (box instanceof HTMLElement) { box.focus(); -@@ -5797,10 +5797,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { +@@ -5797,10 +5797,10 @@ }, [quote?.token]); if (sessionId === void 0) return null; const pluginMic = actions.speech() !== void 0; @@ -11638,7 +9166,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mic", "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), -@@ -5808,16 +5808,16 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { +@@ -5808,16 +5808,16 @@ title: dictation.listening ? t("Stop dictation") : t("Dictate"), onMouseDown: keepComposerFocus, onClick: () => dictation.listening ? dictation.stop() : dictation.start() @@ -11659,7 +9187,7 @@ diff --git a/client.js b/client.js ); } function menuIcons(roster) { -@@ -5825,17 +5825,17 @@ function menuIcons(roster) { +@@ -5825,17 +5825,17 @@ return (row) => { if (!row.bot) return PeopleIcon; const id = row.bot.id; @@ -11682,7 +9210,7 @@ diff --git a/client.js b/client.js if (!open) return void 0; const onDown = (event) => { if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); -@@ -5876,10 +5876,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { +@@ -5876,10 +5876,10 @@ actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); }; const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); @@ -11696,7 +9224,7 @@ diff --git a/client.js b/client.js ref: button, type: "button", className: "bt-plus", -@@ -5889,8 +5889,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { +@@ -5889,8 +5889,8 @@ "aria-expanded": Boolean(open), onMouseDown: keepComposerFocus, onClick: toggle @@ -11707,7 +9235,7 @@ diff --git a/client.js b/client.js "div", { ref: menu, -@@ -5900,9 +5900,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { +@@ -5900,9 +5900,9 @@ style: { left: shown.left, bottom: shown.bottom }, "data-leaving": leaving || void 0 }, @@ -11720,7 +9248,7 @@ diff --git a/client.js b/client.js ), document.body) : null ); } -@@ -5922,15 +5922,57 @@ var conversationOf = (roster, sessionId) => { +@@ -5922,15 +5922,57 @@ function mentionRows(roster, sessionId, query = "") { const here = conversationOf(roster, sessionId); if (here === null) return null; @@ -11782,7 +9310,7 @@ diff --git a/client.js b/client.js function chatRows(roster, list, sessionId) { const here = conversationOf(roster, sessionId); if (here === null) return []; -@@ -5959,14 +5959,31 @@ function atSources({ roster, sessions, launched, iconFor }) { +@@ -5959,14 +5959,31 @@ order: -20, showGroupTitle: false, async candidates(session, { query }) { @@ -11818,7 +9346,7 @@ diff --git a/client.js b/client.js }, onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 }; -@@ -5995,6 +5995,83 @@ function atSources({ roster, sessions, launched, iconFor }) { +@@ -5995,6 +5995,83 @@ return [bots, chats]; } @@ -11902,7 +9430,31 @@ diff --git a/client.js b/client.js // src/client/speech.js var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle"; var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]); -@@ -6121,233 +6121,9 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { +@@ -6008,8 +6008,8 @@ + function waveBytes(samples, rate = 16e3) { + const bytes = new Uint8Array(44 + samples.length * 2); + const view = new DataView(bytes.buffer); +- const ascii = (at, text) => { +- for (let index = 0; index < text.length; index += 1) bytes[at + index] = text.charCodeAt(index); ++ const ascii = (at, text2) => { ++ for (let index = 0; index < text2.length; index += 1) bytes[at + index] = text2.charCodeAt(index); + }; + ascii(0, "RIFF"); + view.setUint32(4, 36 + samples.length * 2, true); +@@ -6030,9 +6030,9 @@ + return bytes; + } + function base64Of(bytes) { +- let text = ""; +- for (let index = 0; index < bytes.length; index += 32768) text += String.fromCharCode(...bytes.subarray(index, index + 32768)); +- return btoa(text); ++ let text2 = ""; ++ for (let index = 0; index < bytes.length; index += 32768) text2 += String.fromCharCode(...bytes.subarray(index, index + 32768)); ++ return btoa(text2); + } + var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); + async function openCapture() { +@@ -6121,233 +6121,9 @@ }; } @@ -12138,7 +9690,7 @@ diff --git a/client.js b/client.js var MENU_EXIT_MS = 120; function groupModels(models) { const groups = /* @__PURE__ */ new Map(); -@@ -6367,17 +6367,10 @@ function groupModels(models) { +@@ -6367,17 +6367,10 @@ }); } var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); @@ -12158,7 +9710,7 @@ diff --git a/client.js b/client.js setFetched(void 0); if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; let live = true; -@@ -6402,12 +6402,12 @@ function effortRows(reasoning) { +@@ -6402,12 +6402,12 @@ ]; } function EffortRows({ reasoning, current, onPick }) { @@ -12176,7 +9728,7 @@ diff --git a/client.js b/client.js "button", { key: row.effort ?? "default", -@@ -6416,17 +6416,17 @@ function EffortRows({ reasoning, current, onPick }) { +@@ -6416,17 +6416,17 @@ "aria-checked": row.effort === current, onClick: () => onPick(row.effort) }, @@ -12200,7 +9752,7 @@ diff --git a/client.js b/client.js "button", { key: model.ref, -@@ -6436,20 +6436,20 @@ function ModelRows({ roster, current, onPick }) { +@@ -6436,20 +6436,20 @@ title: model.ref, onClick: () => onPick(model.ref) }, @@ -12229,7 +9781,7 @@ diff --git a/client.js b/client.js if (place === null) return void 0; const onDown = (event) => { if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; -@@ -6484,6 +6484,7 @@ function useAnchoredMenu() { +@@ -6484,6 +6484,7 @@ return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; } function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { @@ -12237,7 +9789,7 @@ diff --git a/client.js b/client.js const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); const shown = modelOf(bot, roster); const current = bot.model ?? bot.appliedModel; -@@ -6511,10 +6511,10 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo +@@ -6511,10 +6511,10 @@ else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); }; const models = roster.models ?? []; @@ -12251,7 +9803,7 @@ diff --git a/client.js b/client.js "button", { ref: button, -@@ -6527,37 +6527,38 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo +@@ -6527,37 +6527,38 @@ "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, onClick: toggle }, @@ -12304,12 +9856,14 @@ diff --git a/client.js b/client.js const models = roster.models ?? []; const lost = bot.model ? !models.some((model) => model.ref === bot.model) : false; if (bot.model && (!lost || models.length === 0)) return null; -@@ -6570,12 +6570,12 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { +@@ -6569,13 +6569,13 @@ + void actions.refreshModels?.(); }).finally(() => setBusy("")); }; - const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); +- const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); - const choices = models.length === 0 ? setupChoices(actions).map(([label, run]) => (0, import_react13.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)) : [ - ...groupModels(models).map(([provider, list]) => (0, import_react13.createElement)( ++ const 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)( "div", @@ -12321,7 +9875,7 @@ diff --git a/client.js b/client.js key: model.ref, type: "button", className: "bt-model-chip", -@@ -6584,50 +6584,115 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { +@@ -6584,50 +6584,115 @@ "aria-label": `${model.name} · ${provider}`, title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, onClick: () => pick2(model.ref) @@ -12350,7 +9904,7 @@ diff --git a/client.js b/client.js - (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" }, text) ++ (0, import_react20.createElement)("div", { className: "bt-bubble" }, text2) ) ) ), @@ -12393,9 +9947,9 @@ diff --git a/client.js b/client.js + return (0, import_react20.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key")); +} +function missingModelKey(message) { -+ const text = String(message ?? ""); -+ if (!/\bno api key\b|\bmissing (?:an? )?api key\b|\bapi key (?:is )?(?:missing|not set|not configured)\b/i.test(text)) return void 0; -+ return { route: /provider route "([^"]+)"/i.exec(text)?.[1] ?? "" }; ++ const text2 = String(message ?? ""); ++ if (!/\bno api key\b|\bmissing (?:an? )?api key\b|\bapi key (?:is )?(?:missing|not set|not configured)\b/i.test(text2)) return void 0; ++ return { route: /provider route "([^"]+)"/i.exec(text2)?.[1] ?? "" }; +} +function modelKeyChoices(actions, route) { + if (/^deepseek/i.test(route)) return setupChoices(actions); @@ -12406,8 +9960,8 @@ diff --git a/client.js b/client.js + const code = typeof data.code === "string" && data.code ? data.code : void 0; + const raw = String(data.message ?? ""); + const shell = (key, fallback) => { -+ const text = typeof chatT === "function" ? chatT(key) : void 0; -+ return typeof text === "string" && text !== "" && text !== key ? text : fallback; ++ const text2 = typeof chatT === "function" ? chatT(key) : void 0; ++ return typeof text2 === "string" && text2 !== "" && text2 !== key ? text2 : fallback; + }; + const missing = missingModelKey(raw); + const title = code === "ACCOUNT_SIGNED_OUT" ? shell("message.accountStopped", t("This turn failed")) : shell("message.turnError", t("This turn failed")); @@ -12453,7 +10007,7 @@ diff --git a/client.js b/client.js ...ops, ...hooks, t: actions.accountT(), -@@ -6639,6 +6639,16 @@ function SignInHost({ entry, actions }) { +@@ -6639,6 +6639,16 @@ }); } var MODEL_CSS = ` @@ -12470,7 +10024,7 @@ diff --git a/client.js b/client.js .bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease} .bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)} .bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} -@@ -6675,18 +6675,96 @@ var MODEL_CSS = ` +@@ -6675,18 +6675,96 @@ @media (prefers-reduced-motion:reduce){.bt-model-dock{animation:none}.bt-model-spin{animation-duration:2s}.bt-model-caret{transition:none}.bt-model-tick svg{animation:none}} `; @@ -12575,7 +10129,7 @@ diff --git a/client.js b/client.js const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); const toggle = () => { if (isOpen) { -@@ -6719,35 +6719,35 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } +@@ -6719,35 +6719,35 @@ }; const known = info?.folders ?? []; const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; @@ -12627,7 +10181,7 @@ diff --git a/client.js b/client.js className: "bt-folder-input", autoFocus: true, value: draft, -@@ -6759,33 +6759,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } +@@ -6759,33 +6759,33 @@ if (event.key === "Enter" && !event.nativeEvent.isComposing) typed(); } }), @@ -12677,7 +10231,7 @@ diff --git a/client.js b/client.js "button", { ref: button, -@@ -6798,19 +6798,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } +@@ -6798,19 +6798,19 @@ title: value, onClick: toggle }, @@ -12703,7 +10257,7 @@ diff --git a/client.js b/client.js ), document.body) : null ); } -@@ -6830,8 +6830,8 @@ var FOLDER_CSS = ` +@@ -6830,8 +6830,8 @@ `; // src/client/schedules.js @@ -12714,7 +10268,7 @@ diff --git a/client.js b/client.js var shared = null; var listeners = /* @__PURE__ */ new Set(); function publish(view) { -@@ -6839,8 +6839,8 @@ function publish(view) { +@@ -6839,8 +6839,8 @@ for (const listener of listeners) listener(view); } function useSchedules(actions) { @@ -12725,7 +10279,7 @@ diff --git a/client.js b/client.js listeners.add(setView); let stopped = false; let timer; -@@ -6848,7 +6848,7 @@ function useSchedules(actions) { +@@ -6848,7 +6848,7 @@ if (!stopped) publish(next); }).catch(() => { }).finally(() => { @@ -12734,7 +10288,7 @@ diff --git a/client.js b/client.js }); load(); return () => { -@@ -6873,7 +6873,8 @@ function weekdayNames(days) { +@@ -6873,7 +6873,8 @@ function everyText(seconds) { if (seconds % 86400 === 0) return seconds === 86400 ? t("Every day") : t("Every {count} days", { count: seconds / 86400 }); if (seconds % 3600 === 0) return seconds === 3600 ? t("Every hour") : t("Every {count} hours", { count: seconds / 3600 }); @@ -12744,7 +10298,18 @@ diff --git a/client.js b/client.js } function timingText(task) { const zone = task.timeZone && task.timeZone !== localZone() ? ` (${task.timeZone})` : ""; -@@ -6896,11 +6896,25 @@ function timingText(task) { +@@ -6887,8 +6887,8 @@ + return `${t("Every day at {time}", { time: clock2(task.time) })}${zone}`; + case "weekly": { + const days = (task.weekdays ?? []).join(); +- const text = days === "1,2,3,4,5" ? t("Every weekday at {time}", { time: clock2(task.time) }) : days === "6,7" ? t("Every weekend at {time}", { time: clock2(task.time) }) : days === "1,2,3,4,5,6,7" ? t("Every day at {time}", { time: clock2(task.time) }) : t("Every {days} at {time}", { days: weekdayNames(task.weekdays ?? []), time: clock2(task.time) }); +- return `${text}${zone}`; ++ const text2 = days === "1,2,3,4,5" ? t("Every weekday at {time}", { time: clock2(task.time) }) : days === "6,7" ? t("Every weekend at {time}", { time: clock2(task.time) }) : days === "1,2,3,4,5,6,7" ? t("Every day at {time}", { time: clock2(task.time) }) : t("Every {days} at {time}", { days: weekdayNames(task.weekdays ?? []), time: clock2(task.time) }); ++ return `${text2}${zone}`; + } + case "cron": + return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; +@@ -6896,11 +6896,25 @@ return ""; } } @@ -12775,7 +10340,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); -@@ -6913,20 +6913,49 @@ function TaskRow({ task, actions, onView, showBot, roster }) { +@@ -6913,20 +6913,49 @@ setBusy(true); void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); }; @@ -12834,7 +10399,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mini bt-task-remove", "data-confirm": confirming || void 0, -@@ -6934,7 +6934,7 @@ function TaskRow({ task, actions, onView, showBot, roster }) { +@@ -6934,7 +6934,7 @@ "aria-label": confirming ? t("Click again to delete") : t("Delete task"), title: confirming ? void 0 : t("Delete task"), onClick: remove @@ -12843,7 +10408,7 @@ diff --git a/client.js b/client.js ); } var pad = (number) => String(number).padStart(2, "0"); -@@ -6944,27 +6944,49 @@ function inAnHour() { +@@ -6944,27 +6944,49 @@ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:00`; } var KINDS = [["at", "Once"], ["daily", "Every day"], ["weekly", "Every week"], ["every", "Repeat"]]; @@ -12910,7 +10475,7 @@ diff --git a/client.js b/client.js setBusy(true); setError(""); let when3; -@@ -6975,151 +6975,298 @@ function TaskForm({ roster, actions, start, onDone, onCancel }) { +@@ -6975,151 +6975,298 @@ setBusy(false); return; } @@ -13271,7 +10836,7 @@ diff --git a/client.js b/client.js .bt-task[data-done]{opacity:.6} .bt-task[data-busy]{opacity:.4} .bt-task-title{font-size:13px;line-height:18px;font-weight:500;overflow-wrap:anywhere} -@@ -7127,6 +7127,12 @@ var SCHEDULE_CSS = ` +@@ -7127,6 +7127,12 @@ .bt-task-next{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums;white-space:nowrap} .bt-task-remove{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} .bt-task-remove svg{width:14px;height:14px} @@ -13284,16 +10849,23 @@ diff --git a/client.js b/client.js .bt-task-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135} .bt-task-head{display:flex;align-items:center;gap:6px} .bt-task-form{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} -@@ -7158,7 +7158,7 @@ var SCHEDULE_CSS = ` +@@ -7158,12 +7158,12 @@ `; // src/client/capability-settings.js -var import_react16 = require("react"); +var import_react24 = require("react"); var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] }); - var parseMap = (text, label) => { +-var parseMap = (text, label) => { ++var parseMap = (text2, label) => { let value; -@@ -7172,11 +7172,11 @@ var parseMap = (text, label) => { + try { +- value = JSON.parse(text || "{}"); ++ value = JSON.parse(text2 || "{}"); + } catch { + throw new Error(t("{label} must be valid JSON.", { label })); + } +@@ -7172,11 +7172,11 @@ }; var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…"); function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) { @@ -13308,7 +10880,7 @@ diff --git a/client.js b/client.js className: multiline ? "bt-input bt-input-area" : "bt-input", value, disabled, -@@ -7188,7 +7188,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl +@@ -7188,7 +7188,7 @@ ); } function CapabilityForm({ draft, setDraft, items, actions, onClose }) { @@ -13317,7 +10889,7 @@ diff --git a/client.js b/client.js const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value })); const submit = async (event) => { event.preventDefault(); -@@ -7199,7 +7199,7 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) { +@@ -7199,7 +7199,7 @@ if (draft.kind === "mcp" && draft.transport === "stdio") { payload.transport = "stdio"; payload.command = draft.command; @@ -13326,7 +10898,7 @@ diff --git a/client.js b/client.js payload.env = parseMap(draft.envText, t("Environment")); } else if (draft.kind === "mcp") { payload.transport = "streamable-http"; -@@ -7219,95 +7219,95 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) { +@@ -7219,95 +7219,95 @@ } }; const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind)); @@ -13462,7 +11034,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(""), 4e3); return () => clearTimeout(timer); -@@ -7330,20 +7330,20 @@ function CapabilitiesPage({ roster, actions, run }) { +@@ -7330,20 +7330,20 @@ setConfirming(""); void run(actions.deleteCapability(item.name)); }; @@ -13490,7 +11062,7 @@ diff --git a/client.js b/client.js key: item.name, item, confirming: confirming === item.name, -@@ -7359,10 +7359,10 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow +@@ -7359,10 +7359,10 @@ const attached = new Set(bot.capabilities ?? []); const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; if (items.length === 0) return null; @@ -13503,7 +11075,7 @@ diff --git a/client.js b/client.js key: item.name, label: item.name, hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, -@@ -7372,9 +7372,55 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow +@@ -7372,9 +7372,55 @@ ); } @@ -13561,7 +11133,7 @@ diff --git a/client.js b/client.js var MENU_EXIT_MS2 = 120; var counts = /* @__PURE__ */ new Map(); var countListeners = /* @__PURE__ */ new Set(); -@@ -7384,13 +7384,13 @@ function noteCount(botId, count) { +@@ -7384,13 +7384,13 @@ for (const listener of countListeners) listener(); } function useMemoryCount(botId, actions) { @@ -13578,7 +11150,7 @@ diff --git a/client.js b/client.js if (!stop) noteCount(botId, view.entries.length); }).catch(() => { }); -@@ -7404,16 +7404,16 @@ function useMemoryCount(botId, actions) { +@@ -7404,16 +7404,16 @@ function MemoryRow({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); @@ -13601,7 +11173,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -7428,45 +7428,45 @@ function ago(at, now) { +@@ -7428,45 +7428,45 @@ return format.format(-Math.round(hours / 24), "day"); } function Skeleton({ label }) { @@ -13666,7 +11238,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -7478,9 +7478,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => { +@@ -7478,9 +7478,9 @@ return groups; }, []); function EntryRow({ entry, remove }) { @@ -13679,7 +11251,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); -@@ -7495,16 +7495,16 @@ function EntryRow({ entry, remove }) { +@@ -7495,16 +7495,16 @@ setBusy(true); if (!await remove(entry)) setBusy(false); }; @@ -13701,7 +11273,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mem-remove", "data-confirm": confirming || void 0, -@@ -7512,7 +7512,7 @@ function EntryRow({ entry, remove }) { +@@ -7512,7 +7512,7 @@ "aria-label": confirming ? t("Click again to remove") : t("Remove from memory"), title: confirming ? void 0 : t("Remove from memory"), onClick: click @@ -13710,7 +11282,7 @@ diff --git a/client.js b/client.js ); } function Entries({ bot, view, remove }) { -@@ -7521,25 +7521,25 @@ function Entries({ bot, view, remove }) { +@@ -7521,82 +7521,82 @@ { 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 } ]; @@ -13747,8 +11319,17 @@ diff --git a/client.js b/client.js )) ); } -@@ -7551,52 +7551,52 @@ function changeLine(change) { - return change.scope === "team" ? t("Added to team memory: {text}", { text }) : t("Added: {text}", { text }); + function changeLine(change) { +- const text = change.text.length > 120 ? `${change.text.slice(0, 120)}…` : change.text; +- if (!change.ok) return t("Not changed: {text} ({reason})", { text, reason: hostText(change.error ?? "") }); +- if (change.op === "remove") return change.scope === "team" ? t("Removed from team memory: {text}", { text }) : t("Removed: {text}", { text }); +- if (change.op === "edit") return t("Updated: {text}", { text }); +- return change.scope === "team" ? t("Added to team memory: {text}", { text }) : t("Added: {text}", { text }); ++ const text2 = change.text.length > 120 ? `${change.text.slice(0, 120)}…` : change.text; ++ if (!change.ok) return t("Not changed: {text} ({reason})", { text: text2, reason: hostText(change.error ?? "") }); ++ if (change.op === "remove") return change.scope === "team" ? t("Removed from team memory: {text}", { text: text2 }) : t("Removed: {text}", { text: text2 }); ++ if (change.op === "edit") return t("Updated: {text}", { text: text2 }); ++ return change.scope === "team" ? t("Added to team memory: {text}", { text: text2 }) : t("Added: {text}", { text: text2 }); } function Reply({ answer, dismiss }) { - return (0, import_react17.createElement)( @@ -13819,7 +11400,7 @@ diff --git a/client.js b/client.js alive.current = true; void load({ summarize: true }); return () => { -@@ -7606,9 +7606,9 @@ function useMemoryView(botId, actions) { +@@ -7606,9 +7606,9 @@ }, [botId]); return { view, error, load, show }; } @@ -13832,7 +11413,7 @@ diff --git a/client.js b/client.js const id = setInterval(() => setNow(Date.now()), every); return () => clearInterval(id); }, [every]); -@@ -7616,18 +7616,18 @@ function useNow(every) { +@@ -7616,18 +7616,18 @@ } function MemoryDialog({ bot, actions, leaving = false }) { const { view, error, load, show } = useMemoryView(bot.id, actions); @@ -13848,7 +11429,7 @@ diff --git a/client.js b/client.js - const inputRef = (0, import_react17.useRef)(null); - const now = useNow(3e4); - const close = (0, import_react17.useCallback)(() => actions.closeMemory(), []); -+ const [text, setText] = (0, import_react26.useState)(""); ++ 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); @@ -13861,7 +11442,16 @@ diff --git a/client.js b/client.js if (!menu) return void 0; const onDown = (event) => { if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; -@@ -7689,52 +7689,52 @@ function MemoryDialog({ bot, actions, leaving = false }) { +@@ -7671,7 +7671,7 @@ + }; + const ask = async (event) => { + event.preventDefault(); +- const asked = text.trim(); ++ const asked = text2.trim(); + if (asked === "" || answer?.pending) return; + setText(""); + setAnswer({ asked, pending: true }); +@@ -7689,65 +7689,65 @@ 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; @@ -13936,21 +11526,25 @@ diff --git a/client.js b/client.js + (0, import_react26.createElement)("input", { ref: inputRef, className: "bt-mem-input", - value: text, -@@ -7744,10 +7744,10 @@ function MemoryDialog({ bot, actions, leaving = false }) { +- value: text, ++ value: text2, + maxLength: 2e3, + disabled: view === null, + placeholder: t("Ask or update"), "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }), onChange: (event) => setText(event.target.value) }), - (0, import_react17.createElement)( + (0, import_react26.createElement)( "button", - { type: "submit", className: "bt-mem-send", disabled: busy || text.trim() === "", "aria-label": t("Send") }, +- { 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) ) ) ) -@@ -7755,8 +7755,40 @@ function MemoryDialog({ bot, actions, leaving = false }) { +@@ -7755,8 +7755,40 @@ ); } @@ -13992,7 +11586,7 @@ diff --git a/client.js b/client.js var capitalize = (word) => word[0].toUpperCase() + word.slice(1); async function avatarImage(file) { if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); -@@ -7780,8 +7780,8 @@ async function avatarImage(file) { +@@ -7780,8 +7780,8 @@ throw new Error(t("That image is too large")); } function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { @@ -14003,7 +11597,7 @@ diff --git a/client.js b/client.js const commit = () => { const next = draft?.trim(); setDraft(null); -@@ -7792,7 +7792,7 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu +@@ -7792,7 +7792,7 @@ if (next === void 0 || next === (value ?? "") || required && next === "") return; void onSave(next); }; @@ -14012,7 +11606,7 @@ diff --git a/client.js b/client.js className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", value: draft ?? value ?? "", placeholder, -@@ -7813,29 +7813,29 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu +@@ -7813,29 +7813,29 @@ }); } function Row({ label, hint, stack = false, children }) { @@ -14051,7 +11645,7 @@ diff --git a/client.js b/client.js ); } function modelHint(bot, roster) { -@@ -7846,22 +7846,22 @@ function modelHint(bot, roster) { +@@ -7846,22 +7846,22 @@ return t("Applies from the next message."); } function ShapeRing({ shape, size }) { @@ -14079,7 +11673,7 @@ diff --git a/client.js b/client.js key: id, type: "button", className: "bt-shape", -@@ -7869,8 +7869,8 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { +@@ -7869,8 +7869,8 @@ title: t(shapeOf(id).label), "aria-pressed": !hasPhoto && look.shape === id, onClick: () => onShape(id) @@ -14090,7 +11684,7 @@ diff --git a/client.js b/client.js key: color, type: "button", className: "bt-color", -@@ -7878,19 +7878,19 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { +@@ -7878,19 +7878,19 @@ title: t(capitalize(color)), "aria-pressed": look.color === color, onClick: () => onColor(color) @@ -14116,7 +11710,7 @@ diff --git a/client.js b/client.js const onDown = (event) => { if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; onClose(); -@@ -7908,7 +7908,7 @@ function AvatarEditor({ bot, actions, run, onClose }) { +@@ -7908,7 +7908,7 @@ window.removeEventListener("keydown", onKey, true); }; }, [onClose]); @@ -14125,7 +11719,7 @@ diff --git a/client.js b/client.js if (tab !== "upload") return void 0; const onPaste = (event) => { const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); -@@ -7922,32 +7922,32 @@ function AvatarEditor({ bot, actions, run, onClose }) { +@@ -7922,32 +7922,32 @@ }, [tab, bot]); const hasPhoto = Boolean(look.image); const glide = useGlide(tab); @@ -14166,7 +11760,7 @@ diff --git a/client.js b/client.js "div", { className: "bt-dropzone", -@@ -7963,15 +7963,15 @@ function AvatarEditor({ bot, actions, run, onClose }) { +@@ -7963,15 +7963,15 @@ upload(event.dataTransfer.files?.[0]); } }, @@ -14188,7 +11782,7 @@ diff --git a/client.js b/client.js const file = event.target.files?.[0]; event.target.value = ""; upload(file); -@@ -7982,26 +7982,27 @@ function AvatarEditor({ bot, actions, run, onClose }) { +@@ -7982,26 +7982,27 @@ function MemoryCard({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); @@ -14224,7 +11818,7 @@ diff --git a/client.js b/client.js const isMainBot = isMainOf(roster, bot.id); const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; const isTile = roster.mainBotId === bot.id; -@@ -8011,149 +8011,812 @@ function BotSettingsForm({ bot, roster, actions, run }) { +@@ -8011,112 +8011,116 @@ if (confirming) void run(actions.deleteBot(bot.id)); else setConfirming(true); }; @@ -14252,52 +11846,75 @@ diff --git a/client.js b/client.js - (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)) -+ ), + ), +- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null + editing ? (0, import_react28.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null -+ ), + ), +- (0, import_react18.createElement)( + (0, import_react28.createElement)( -+ "div", -+ { className: "bt-bs-who" }, + "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_react18.createElement)( + (0, import_react28.createElement)( -+ Card, -+ { title: t("Profile") }, + 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_react18.createElement)( + (0, import_react28.createElement)( -+ Card, -+ { title: t("Model") }, + Card, + { title: t("Model") }, +- (0, import_react18.createElement)( + (0, import_react28.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, + "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 }) + ), + bot.model ? (0, import_react28.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null -+ ), + ), +- (0, import_react18.createElement)( + (0, import_react28.createElement)( -+ Card, -+ { title: t("Working folder") }, + Card, + { title: t("Working folder") }, +- (0, import_react18.createElement)( + (0, import_react28.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, + "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_react18.createElement)( + (0, import_react28.createElement)( -+ Card, -+ { title: t("Instructions") }, + Card, + { title: t("Instructions") }, +- (0, import_react18.createElement)( + (0, import_react28.createElement)( -+ Row, -+ { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, + 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_react18.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), +- roster.memory === false ? null : (0, import_react18.createElement)(MemoryCard, { bot, actions }), +- (0, import_react18.createElement)(BotSchedules, { bot, roster, actions }), +- (0, import_react18.createElement)( + // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406). + (0, import_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 }), @@ -14306,60 +11923,73 @@ diff --git a/client.js b/client.js + (0, import_react28.createElement)(BotSchedules, { bot, roster, actions }), + (0, import_react28.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }), + (0, import_react28.createElement)( -+ Card, -+ { title: t("Main Bot") }, + Card, + { title: t("Main Bot") }, +- onlyMain ? (0, import_react18.createElement)( + onlyMain ? (0, import_react28.createElement)( -+ "div", -+ { className: "bt-set-row bt-bs-row" }, + "div", + { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)( + (0, import_react28.createElement)( -+ "span", -+ { className: "bt-set-copy" }, + "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_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)) }) -+ ), -+ // The Main Bot tile is always first and always shown, like in the context menu. + ), + // 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)( -+ Card, -+ { title: t("Sidebar") }, + 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_react18.createElement)( + (0, import_react28.createElement)( -+ Card, + Card, +- { title: t("Manage") }, +- (0, import_react18.createElement)( + { title: t("More") }, + (0, import_react28.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, + "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)) -+ ), + ), +- isMainBot ? null : (0, import_react18.createElement)( + isMainBot ? null : (0, import_react28.createElement)( -+ "button", -+ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, + "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)( -+ "span", -+ { className: "bt-set-copy" }, + "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.")) ), -- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null +- (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)) -+ ) -+ ) -+ ); -+} -+var BOT_SETTINGS_CSS = ` -+.bt-bs{display:flex;flex-direction:column;gap:18px} -+.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} -+.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} -+.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -+.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} -+.bt-bs .bt-avedit{left:0;transform:none} -+.bt-bs-row{min-height:48px} -+label.bt-bs-row{cursor:text} -+.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} + ) + ) + ); +@@ -8130,9 +8130,17 @@ + .bt-bs .bt-avedit{left:0;transform:none} + .bt-bs-row{min-height:48px} + label.bt-bs-row{cursor:text} +-.bt-bs-row>.bt-set-copy{flex:1 1 auto} + .bt-bs-label{font-size:13px;line-height:18px;font-weight:500} +/* #1347: a row keeps its label and its control side by side. The *label* is one line — "Name" used to + break into "Nam" + "e" inside a 36px column — and a control never breaks its own words: "Test + connection" came out as "Test connecti" / "on" and Main Bot's "Change" as "Ch/an/ge". The hint under @@ -14367,29 +11997,16 @@ diff --git a/client.js b/client.js +.bt-bs-row>.bt-set-copy{flex:1 1 8ch;min-width:8ch} +.bt-bs-row>.bt-set-copy>.bt-bs-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bt-bs-row .bt-soft,.bt-bs-row .bt-mini{flex:none;white-space:nowrap} -+.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} + .bt-bs-row .bt-model-trigger{flex:none;max-width:58%} +.bt-bs-row .bt-model-trigger :is(.bt-model-name,.bt-model-provider){white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} +${MODEL_TEST_CSS} -+.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} -+.bt-bs-input:hover{background:var(--bt-hover)} -+.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} -+.bt-bs-input::placeholder{color:var(--bt-ink-3)} -+.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} -+.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} -+.bt-bs-area:hover{background:var(--bt-active)} -+.bt-bs-danger .bt-bs-label{color:#c21d2e} -+.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} -+.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} -+.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} -+.bt-switch[aria-checked=true]{background:var(--bt-accent)} -+.bt-switch:disabled{opacity:.45;cursor:default} -+.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} -+.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} -+.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} -+.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} -+.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} -+.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} -+@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} + .bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} + .bt-bs-input:hover{background:var(--bt-hover)} + .bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} +@@ -8153,8 +8153,659 @@ + .bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} + .bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} + @media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} +/* #1347: the panel's floor is 240px, where a label and a 96px field cannot share a line honestly. Below + 300px the row stacks instead: the copy keeps its whole width on top, the control sits under it and + keeps its own words. Nothing is clipped to fit a width that is not there. */ @@ -14398,8 +12015,8 @@ 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 ROOM_MODES = [ @@ -14502,8 +12119,7 @@ diff --git a/client.js b/client.js + { 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_react18.createElement)( ++ ), + canPick ? (0, import_react29.createElement)("button", { type: "button", className: "bt-mini", "aria-expanded": open, onClick: () => setOpen(!open) }, open ? t("Done") : t("Change")) : null + ), + open && canPick ? seats.map((seat) => (0, import_react29.createElement)( @@ -14578,20 +12194,15 @@ diff --git a/client.js b/client.js + { className: "bt-bs-hero" }, + (0, import_react29.createElement)(RoomAvatar, { room: room2, roster, size: 64 }), + (0, import_react29.createElement)( - "div", - { className: "bt-bs-who" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-name" }, bot.name), -- (0, import_react18.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) ++ "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_react18.createElement)( ++ ) ++ ), + (0, import_react29.createElement)( - Card, - { title: t("Profile") }, -- (0, import_react18.createElement)(Row, { label: t("Name") }, (0, import_react18.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), -- (0, import_react18.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react18.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) ++ Card, ++ { title: t("Profile") }, + (0, import_react29.createElement)( + Row, + { label: t("Name"), hint: t("Leave it empty to name the group after its members.") }, @@ -14602,18 +12213,12 @@ diff --git a/client.js b/client.js + { 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_react18.createElement)( ++ ), + (0, import_react29.createElement)( - Card, -- { title: t("Model") }, -- (0, import_react18.createElement)( ++ Card, + { title: t("Who answers") }, + (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 }) ++ "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", { + key: id, @@ -14626,54 +12231,31 @@ diff --git a/client.js b/client.js + } + }, 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_react18.createElement)( ++ ) ++ ), + (0, import_react29.createElement)( - Card, - { title: t("Working folder") }, -- (0, import_react18.createElement)( ++ Card, ++ { title: t("Working folder") }, + (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 }) }) ++ "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_react18.createElement)( ++ ) ++ ), + face ? (0, import_react29.createElement)( - Card, -- { title: t("Instructions") }, -- (0, import_react18.createElement)( -- Row, -- { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, -- (0, import_react18.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) -- ) -- ), -- (0, import_react18.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), -- roster.memory === false ? null : (0, import_react18.createElement)(MemoryCard, { bot, actions }), -- (0, import_react18.createElement)(BotSchedules, { bot, roster, actions }), -- (0, import_react18.createElement)( ++ Card, + { title: t("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 })) + ) : null, + face ? null : (0, import_react29.createElement)( - Card, -- { title: t("Main Bot") }, -- onlyMain ? (0, import_react18.createElement)( ++ Card, + { title: t("Members · {count}", { count: members.length + humans.length + remotes.length }) }, + humans.map((person) => (0, import_react29.createElement)( - "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)( ++ "div", + { key: person.id, className: "bt-set-row bt-bs-row bt-gs-member" }, + (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.")) ++ "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")), @@ -14713,9 +12295,7 @@ diff --git a/client.js b/client.js + // #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")) - ), -- (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)) }) ++ ), + // 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 }] }) }, "×") + )), @@ -14760,29 +12340,19 @@ 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 }) - ), -- // The Main Bot tile is always first and always shown, like in the context menu. -- isTile ? null : (0, import_react18.createElement)( ++ ), + (0, import_react29.createElement)(StandingApprovalsCard, { scope: "room", target: room2, roster, actions, run, Card }), + (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 })) }) ++ 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_react18.createElement)( ++ ), + (0, import_react29.createElement)( - Card, -- { title: t("Manage") }, -- (0, import_react18.createElement)( ++ Card, + { title: t("More") }, + (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)) ++ "button", + { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, + (0, import_react29.createElement)( + "span", @@ -14920,11 +12490,11 @@ diff --git a/client.js b/client.js + const entry = outline[index2]; + if (hidden.includes(entry.turn)) continue; + for (const raw of [entry.response, entry.prompt]) { -+ const text = paletteText(raw, names); -+ const at = text.toLowerCase().indexOf(wanted); ++ const text2 = paletteText(raw, names); ++ const at = text2.toLowerCase().indexOf(wanted); + if (at < 0) continue; + const from = Math.max(0, at - 24); -+ hits.push([id, entry.turn, (from > 0 ? "…" : "") + text.slice(from, at + wanted.length + 60)]); ++ hits.push([id, entry.turn, (from > 0 ? "…" : "") + text2.slice(from, at + wanted.length + 60)]); + break; + } + } @@ -15042,17 +12612,7 @@ diff --git a/client.js b/client.js + setQuery(""); + setCursor(0); + } }, (0, import_react30.createElement)(CloseIcon)) : null - ), -- isMainBot ? null : (0, import_react18.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react18.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), -- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) -- ), -- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) ++ ), + (0, import_react30.createElement)( + "div", + { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, @@ -15095,47 +12655,15 @@ diff --git a/client.js b/client.js + ); + }) + )) - ) - ) - ); - } --var BOT_SETTINGS_CSS = ` --.bt-bs{display:flex;flex-direction:column;gap:18px} --.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} --.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} --.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-bs .bt-avedit{left:0;transform:none} --.bt-bs-row{min-height:48px} --label.bt-bs-row{cursor:text} --.bt-bs-row>.bt-set-copy{flex:1 1 auto} --.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} --.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} --.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} --.bt-bs-input:hover{background:var(--bt-hover)} --.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} --.bt-bs-input::placeholder{color:var(--bt-ink-3)} --.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} --.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} --.bt-bs-area:hover{background:var(--bt-active)} --.bt-bs-danger .bt-bs-label{color:#c21d2e} --.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} --.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} --.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} --.bt-switch[aria-checked=true]{background:var(--bt-accent)} --.bt-switch:disabled{opacity:.45;cursor:default} --.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} --.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} --.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} --.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} --.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} --.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} --@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} --`; - ++ ) ++ ) ++ ); ++} ++ // src/client/bot-draft.js function freshBotName(roster, base = "New Bot") { -@@ -8165,36 +8165,48 @@ function freshBotName(roster, base = "New Bot") { + const taken = new Set(roster.bots.map((bot) => bot.name.toLowerCase())); +@@ -8165,36 +8165,48 @@ } function startModel(roster) { const models = roster.models ?? []; @@ -15152,10 +12680,10 @@ diff --git a/client.js b/client.js - if (brief) payload.brief = brief; return payload; } -+async function afterCreate({ id, text, actions }) { ++async function afterCreate({ id, text: text2, actions }) { + actions.openSession(id); + actions.showDetails(id, "settings"); -+ const line2 = String(text ?? "").trim(); ++ const line2 = String(text2 ?? "").trim(); + if (line2 === "") return; + try { + await actions.send(id, line2); @@ -15199,7 +12727,7 @@ diff --git a/client.js b/client.js if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" }); }, []); const fallbackName = freshBotName(roster, t("New Bot")); -@@ -8206,8 +8206,8 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr +@@ -8206,8 +8206,8 @@ setError(""); setBusy(true); try { @@ -15210,7 +12738,7 @@ diff --git a/client.js b/client.js } catch (failure) { setError(failure?.message ?? String(failure)); setBusy(false); -@@ -8219,25 +8219,25 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr +@@ -8219,25 +8219,25 @@ avatarImage(file).then(setImage, (failure) => setError(failure?.message ?? String(failure))); }; const previewLook = image ? { ...look, image } : look; @@ -15244,7 +12772,7 @@ diff --git a/client.js b/client.js look, hasPhoto: Boolean(image), onShape: (shape) => { -@@ -8249,27 +8249,27 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr +@@ -8249,27 +8249,27 @@ setLook((current) => ({ ...current, color })); } }), @@ -15280,7 +12808,7 @@ diff --git a/client.js b/client.js className: "bt-input", autoFocus: !inline, value: name, -@@ -8284,75 +8284,113 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr +@@ -8284,75 +8284,113 @@ } } }), @@ -15424,7 +12952,7 @@ diff --git a/client.js b/client.js const pick2 = (bot) => { setPicked((current) => [...current, bot.id]); setQuery(""); -@@ -8386,13 +8386,17 @@ function NewChat({ mode, roster, actions }) { +@@ -8386,71 +8386,104 @@ }; const created = (bot) => { close(); @@ -15434,29 +12962,46 @@ diff --git a/client.js b/client.js }; const makeGroup = async () => { if (admin === null || picked.length === 0) return; - const text = message.trim(); +- const text = message.trim(); - const room2 = await actions.createRoom([admin, ...picked], { admin, ...text ? {} : { greet: true } }); +- if (text) await actions.send(room2.id, text); ++ const text2 = message.trim(); + const { members, remotes } = splitRoomPicks(roster, [admin, ...picked]); + const participants = remotes.length === 0 ? void 0 : [ + ...members.map((id) => ({ kind: "bot", id, role: id === admin ? "admin" : "member" })), + ...remotes + ]; + const room2 = await actions.createRoom(members, { admin, ...participants ? { participants } : {} }); - if (text) await actions.send(room2.id, text); ++ if (text2) await actions.send(room2.id, text2); actions.openSession(room2.id); close(); -@@ -8411,46 +8411,75 @@ function NewChat({ mode, roster, actions }) { + }; + const createGroup = () => run(makeGroup); + const sendMessage = () => run(async () => { +- const text = message.trim(); ++ const text2 = message.trim(); + if (group && admin !== null) { + await makeGroup(); + return; + } +- if (text === "" || picked.length === 0) return; ++ if (text2 === "" || picked.length === 0) return; + if (picked.length === 1) { +- await actions.send(picked[0], text); ++ await actions.send(picked[0], text2); + actions.openSession(picked[0]); close(); return; } - const room2 = await actions.createRoom(picked); +- await actions.send(room2.id, text); + const { members, remotes } = splitRoomPicks(roster, picked); + const participants = remotes.length === 0 ? void 0 : [ + ...members.map((id) => ({ kind: "bot", id })), + ...remotes + ]; + const room2 = await actions.createRoom(members, participants ? { participants } : {}); - await actions.send(room2.id, text); ++ await actions.send(room2.id, text2); actions.openSession(room2.id); close(); }); @@ -15532,7 +13077,7 @@ diff --git a/client.js b/client.js canGoBack: draft.fromList, onBack: draft.fromList ? () => { setDraft(null); -@@ -8467,33 +8467,41 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); +@@ -8467,33 +8467,41 @@ if (picked.length > 0) setPicked((current) => current.slice(0, -1)); else if (admin !== null) setAdmin(null); }; @@ -15589,7 +13134,7 @@ diff --git a/client.js b/client.js ref: inputRef, autoFocus: true, value: query, -@@ -8517,20 +8517,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); +@@ -8517,20 +8517,20 @@ } else if (event.key === "Backspace" && query === "") unpick(); } }), @@ -15615,7 +13160,7 @@ diff --git a/client.js b/client.js "button", { key: option.key, -@@ -8542,21 +8542,36 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); +@@ -8542,21 +8542,36 @@ onMouseEnter: () => setCursor(index), onClick: option.run }, @@ -15661,7 +13206,7 @@ diff --git a/client.js b/client.js rows: 1, value: message, placeholder, -@@ -8569,7 +8569,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); +@@ -8569,7 +8569,7 @@ } } }), @@ -15670,17 +13215,17 @@ diff --git a/client.js b/client.js ) ) ) -@@ -8577,18 +8577,41 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); +@@ -8577,18 +8577,41 @@ } // src/client/exchange.js -var import_react21 = require("react"); +var import_react33 = require("react"); var SHEET_EXIT_MS = 220; -+function exchangeText(text) { -+ if (typeof text !== "string") return ""; ++function exchangeText(text2) { ++ if (typeof text2 !== "string") return ""; + const CLOSE = ""; -+ let body = text; ++ let body = text2; + for (let previous; previous !== body; ) { + previous = body; + const open = body.indexOf(" track.stop()); return; -@@ -8716,18 +8716,18 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8716,18 +8716,18 @@ const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0; return entry ? `${outline.length}|${entry.response ?? ""}` : "0|"; }); @@ -15828,9 +13373,27 @@ diff --git a/client.js b/client.js const paneLeft = usePaneLeft(); const close = () => { turn.current += 1; -@@ -8747,12 +8747,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8735,24 +8735,24 @@ + actions.closeVoice(); + }; + useEscape(close); +- const submit = (text) => { +- setHeard(text); ++ const submit = (text2) => { ++ setHeard(text2); + setPhase("thinking"); + waiting.current = reply.current.split("|")[0]; +- void actions.send(sessionId, text.trim()).catch(() => setPhase("listening")); ++ void actions.send(sessionId, text2.trim()).catch(() => setPhase("listening")); + }; +- const dictation = useDictation((text, final) => { +- setHeard(text); ++ const dictation = useDictation((text2, final) => { ++ setHeard(text2); + if (!final) return; dictation.stop(); - submit(text); +- submit(text); ++ submit(text2); }); - (0, import_react22.useEffect)(() => { + (0, import_react34.useEffect)(() => { @@ -15843,7 +13406,7 @@ diff --git a/client.js b/client.js if (!speech) return void 0; let live = true; speechCatalog(speech).then(({ catalog: next, usable }) => { -@@ -8767,16 +8767,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8767,16 +8767,16 @@ }; }, [speech]); const listen = phase === "listening"; @@ -15863,22 +13426,36 @@ diff --git a/client.js b/client.js if (!speech || !catalog || !listen) return void 0; const own = turn.current; let capture = null; -@@ -8838,12 +8838,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8789,9 +8789,9 @@ + setPhase("transcribing"); + try { + const wave = await current.finish(catalog.maxDurationSeconds); +- const text = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : ""; ++ const text2 = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : ""; + if (turn.current !== own) return; +- if (text) submit(text); ++ if (text2) submit(text2); + else setPhase("listening"); + } catch (error) { + if (turn.current !== own) return; +@@ -8838,12 +8838,12 @@ setServiceLevel(0); }; }, [speech, catalog, listen, cycle]); - (0, import_react22.useEffect)(() => { + (0, import_react34.useEffect)(() => { if (phase !== "thinking" || running || waiting.current === null) return; - const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; - if (count === waiting.current || !text.trim()) return; +- const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; +- if (count === waiting.current || !text.trim()) return; ++ const [count, text2] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; ++ if (count === waiting.current || !text2.trim()) return; waiting.current = null; - const spoken = plainSpeech(text); -+ const spoken = speechOf(text, botNames(roster)); ++ const spoken = speechOf(text2, botNames(roster)); setSaid(spoken); setHeard(""); if (!window.speechSynthesis || !spoken) { -@@ -8858,7 +8858,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8858,7 +8858,7 @@ window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); }, [phase, running, lastReply]); @@ -15887,7 +13464,7 @@ diff --git a/client.js b/client.js const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening); const level = speech ? serviceLevel : micLevel; const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle"; -@@ -8875,291 +8875,605 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8875,291 +8875,605 @@ const openSetup = phase === "setup" ? actions.voiceSetup() : null; const caption = phase === "speaking" ? said : heard; const shownNote = note || dictation.note; @@ -16322,10 +13899,54 @@ diff --git a/client.js b/client.js - { title: t("Sidebar") }, - (0, import_react23.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_react23.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_react23.createElement)( +- Card, +- { title: t("Manage") }, +- (0, import_react23.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, +- (0, import_react23.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), +- (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)) +- ) + }) -+ ) -+ ); -+} + ) + ); + } +-function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { +- const [admin, setAdmin] = (0, import_react23.useState)(null); +- const [members, setMembers] = (0, import_react23.useState)([]); +- const [name, setName] = (0, import_react23.useState)(""); +- const [busy, setBusy] = (0, import_react23.useState)(false); +- const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const pickAdmin = (id) => { +- setAdmin(id); +- setMembers((list) => list.filter((item) => item !== id)); +- }; +- const create = async () => { +- if (busy || admin === null || members.length === 0) return; +- setBusy(true); +- let room2; +- const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { +- room2 = value; +- })); +- if (done && room2) onCreated(room2); +- else setBusy(false); +- }; +- const chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", { +- key: bot.id, +- type: "button", +- role, +- className: "bt-ng-chip", +- "aria-checked": on, +- onClick +- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null); +- return (0, import_react23.createElement)( +function Row2({ item, source, onOpen, onRename, onDelete }) { + return (0, import_react35.createElement)( + "button", @@ -16341,20 +13962,7 @@ diff --git a/client.js b/client.js + { 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_react23.createElement)( -- Card, -- { title: t("Manage") }, -- (0, import_react23.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react23.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), -- (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)) ++ ), + source === "own" ? (0, import_react35.createElement)( + "span", + { className: "bt-set-actions" }, @@ -16393,7 +14001,9 @@ diff --git a/client.js b/client.js + const yours = (pack?.workflows ?? []).filter((item) => !templateIds.has(item.id)); + const showUse = opened?.source === "factory" || opened?.fromFactory !== void 0; + return (0, import_react35.createElement)( -+ "div", + "div", +- { className: "bt-set-card bt-ng" }, +- (0, import_react23.createElement)( + { 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)( @@ -16432,14 +14042,18 @@ diff --git a/client.js b/client.js + { 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 })) + ) - ) -- ) ++ ) + ) : null, + graph ? (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-wf-editor" }, + (0, import_react35.createElement)( -+ "div", + "div", +- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, +- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) + { 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, @@ -16454,51 +14068,17 @@ diff --git a/client.js b/client.js + editing: opened?.source === "own" && editing === true + }) + ) : null - ); - } --function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { -- const [admin, setAdmin] = (0, import_react23.useState)(null); -- const [members, setMembers] = (0, import_react23.useState)([]); -- const [name, setName] = (0, import_react23.useState)(""); -- const [busy, setBusy] = (0, import_react23.useState)(false); -- const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); -- const pickAdmin = (id) => { -- setAdmin(id); -- setMembers((list) => list.filter((item) => item !== id)); -- }; -- const create = async () => { -- if (busy || admin === null || members.length === 0) return; -- setBusy(true); -- let room2; -- const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { -- room2 = value; -- })); -- if (done && room2) onCreated(room2); -- else setBusy(false); -- }; -- const chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", { -- key: bot.id, -- type: "button", -- role, -- className: "bt-ng-chip", -- "aria-checked": on, -- onClick -- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null); -- return (0, import_react23.createElement)( ++ ); ++} +function RenameModal({ workflow, onClose, onConfirm }) { + const [name, setName] = (0, import_react35.useState)(workflow?.name ?? ""); + return (0, import_react35.createElement)( - "div", -- { className: "bt-set-card bt-ng" }, -- (0, import_react23.createElement)( ++ "div", + { className: "bt-modal-mask", onClick: (event) => { + if (event.target === event.currentTarget) onClose?.(); + } }, + (0, import_react35.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))), -- (0, import_react23.createElement)( ++ "div", + { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Rename workflow") }, + (0, import_react35.createElement)("h3", null, t("Rename workflow")), + (0, import_react35.createElement)( @@ -16508,15 +14088,17 @@ diff --git a/client.js b/client.js + (0, import_react35.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) + ), + (0, import_react35.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")) ++ "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")) ) - ), - admin === null ? null : (0, import_react23.createElement)( +- "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), +- (0, import_react23.createElement)( + ) + ); +} @@ -16527,10 +14109,7 @@ diff --git a/client.js b/client.js + if (event.target === event.currentTarget) onClose?.(); + } }, + (0, import_react35.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), -- (0, import_react23.createElement)( ++ "div", + { className: "bt-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 ?? "" })), @@ -16729,14 +14308,16 @@ diff --git a/client.js b/client.js // src/client/usage-data.js var HOUR = 0; -@@ -9277,13 +9277,13 @@ function summarize(view, range, keep = () => true, seriesOf = () => "all") { +@@ -9277,13 +9277,13 @@ } return result; } -function trimmed(value) { +- const text = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2); +- return text.includes(".") ? text.replace(/\.?0+$/, "") : text; +function trimmed2(value) { - const text = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2); - return text.includes(".") ? text.replace(/\.?0+$/, "") : text; ++ const text2 = value >= 100 ? value.toFixed(0) : value >= 10 ? value.toFixed(1) : value.toFixed(2); ++ return text2.includes(".") ? text2.replace(/\.?0+$/, "") : text2; } function compact(count) { for (const [size, unit] of [[1e9, "B"], [1e6, "M"], [1e3, "K"]]) { @@ -16745,7 +14326,7 @@ diff --git a/client.js b/client.js } return String(Math.round(count)); } -@@ -9294,11 +9294,11 @@ function percent(part, whole) { +@@ -9294,11 +9294,11 @@ } // src/client/usage-page.js @@ -16759,7 +14340,7 @@ diff --git a/client.js b/client.js var rangeLabel = (id) => ({ "24h": t("24 hours"), "7d": t("7 days"), "30d": t("30 days"), "12w": t("12 weeks"), "12m": t("12 months") })[id]; var unitLabel = (unit) => ({ hour: t("Per hour"), day: t("Per day"), week: t("Per week"), month: t("Per month") })[unit]; var MODEL_INKS = ["deepseek", "tangerine", "green", "violet", "sky", "rose", "yellow", "cyan", "magenta", "brown"]; -@@ -9341,9 +9341,9 @@ function titleOf(range, bucket) { +@@ -9341,9 +9341,9 @@ return dates({ year: "numeric", month: "long" }).format(bucket.start); } function useTween(value) { @@ -16772,7 +14353,7 @@ diff --git a/client.js b/client.js if (reducedMotion() || typeof requestAnimationFrame !== "function") { from.current = value; setShown(value); -@@ -9363,9 +9363,9 @@ function useTween(value) { +@@ -9363,9 +9363,9 @@ }, [value]); return shown; } @@ -16784,7 +14365,7 @@ diff --git a/client.js b/client.js className: "bt-us-model", "aria-hidden": true, style: { width: size, height: size, fontSize: Math.round(size * 0.42), background: `color-mix(in srgb, ${paint} 20%, var(--bt-card))`, color: `color-mix(in srgb, ${paint} 72%, var(--bt-ink))` } -@@ -9382,7 +9382,7 @@ function ownerFace(owner, roster) { +@@ -9382,7 +9382,7 @@ sub: owner.deleted ? t("Deleted") : role.toLowerCase() === name.toLowerCase() ? "" : role, paint: paintCss(colorOf(bot)), stops: GRADIENTS[colorOf(bot)], @@ -16793,7 +14374,7 @@ diff --git a/client.js b/client.js }; } if (owner.kind === "room") { -@@ -9392,7 +9392,7 @@ function ownerFace(owner, roster) { +@@ -9392,7 +9392,7 @@ name: owner.name || t("Deleted group chat"), sub: owner.deleted ? t("Deleted") : t("Group chat"), paint: "var(--bt-us-rest)", @@ -16802,8 +14383,12 @@ diff --git a/client.js b/client.js }; } if (owner.kind === "agent") { -@@ -9414,30 +9414,30 @@ function Tabs({ label, value, options, onChange, boxRef }) { - const names = options.map(([, text]) => text).join("\n"); +@@ -9411,33 +9411,33 @@ + return labels[kind] ?? kind; + } + function Tabs({ label, value, options, onChange, boxRef }) { +- const names = options.map(([, text]) => text).join("\n"); ++ const names = options.map(([, text2]) => text2).join("\n"); const glide = useGlide(`${value} ${names}`, { box: boxRef }); - return (0, import_react24.createElement)( @@ -16813,7 +14398,7 @@ diff --git a/client.js b/client.js - (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, text, icon3]) => (0, import_react36.createElement)("button", { key: id, type: "button", "aria-pressed": value === id, onClick: () => onChange(id) }, icon3 ? (0, import_react36.createElement)(icon3) : null, text)) ++ 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)) ); } function Stats({ range, total, previous }) { @@ -16843,7 +14428,7 @@ diff --git a/client.js b/client.js "span", { className: "bt-us-delta", title: compareText(range) }, `${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%` -@@ -9455,83 +9455,83 @@ function niceTop(max) { +@@ -9455,83 +9455,83 @@ return [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].map((scale) => scale * step).find((value) => value >= max); } function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) { @@ -16955,7 +14540,7 @@ diff --git a/client.js b/client.js onClick ? "button" : "div", { className: "bt-us-row", -@@ -9540,69 +9540,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick +@@ -9540,69 +9540,69 @@ title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), ...onClick ? { type: "button", onClick } : {} }, @@ -17053,7 +14638,7 @@ diff --git a/client.js b/client.js const onDown = (event) => { if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose(); }; -@@ -9628,17 +9628,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { +@@ -9628,17 +9628,17 @@ const [from, to] = [anchor, key].sort(); onPick(from, to); }; @@ -17074,7 +14659,7 @@ diff --git a/client.js b/client.js key, type: "button", className: "bt-us-day", -@@ -9654,15 +9654,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { +@@ -9654,15 +9654,15 @@ "data-has": flag(active.has(key)), onMouseEnter: () => setHover(key), onClick: () => press(key) @@ -17096,7 +14681,7 @@ diff --git a/client.js b/client.js ); }; const shortcuts = [ -@@ -9671,30 +9671,30 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { +@@ -9671,30 +9671,30 @@ ["year", t("This year"), `${today.slice(0, 4)}-01-01`, today] ]; const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1; @@ -17135,7 +14720,7 @@ diff --git a/client.js b/client.js const today = hourMs(`${view.now.slice(0, 10)} 00`); const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS; const from = dayKey(first); -@@ -9710,13 +9710,13 @@ function Heat({ view, keep, paint, index }) { +@@ -9710,13 +9710,13 @@ for (let week = 0; week < HEAT_WEEKS; week += 1) { const monday = first + week * 7 * DAY_MS; if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) { @@ -17151,7 +14736,7 @@ diff --git a/client.js b/client.js key: ms, className: "bt-us-cell", style: { "--w": week }, -@@ -9724,7 +9724,7 @@ function Heat({ view, keep, paint, index }) { +@@ -9724,7 +9724,7 @@ "data-future": ms > today ? "" : void 0, "data-today": ms === today ? "" : void 0, onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day }) @@ -17160,7 +14745,7 @@ diff --git a/client.js b/client.js } } const tipSide = hover ? { -@@ -9733,61 +9733,61 @@ function Heat({ view, keep, paint, index }) { +@@ -9733,61 +9733,61 @@ } : null; const active = [...daily.values()].filter((value) => value > 0).length; const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active }); @@ -17248,7 +14833,7 @@ diff --git a/client.js b/client.js let stopped = false; let timer; const resized = Promise.all((document.querySelector(".bt-settings")?.getAnimations() ?? []).filter((animation) => animation.transitionProperty === "width" || animation.transitionProperty === "height").map((animation) => animation.finished.catch(() => { -@@ -9797,7 +9797,7 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9797,7 +9797,7 @@ const value = await actions.usage(); await resized; if (stopped) return; @@ -17257,7 +14842,7 @@ diff --git a/client.js b/client.js setError(""); timer = setTimeout(load, value.scanning ? 3e3 : 2e4); } catch (failure) { -@@ -9814,7 +9814,7 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9814,7 +9814,7 @@ }, []); const today = view?.now.slice(0, 10); const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId); @@ -17266,7 +14851,7 @@ diff --git a/client.js b/client.js if (!view) return null; const faces2 = view.owners.map((owner) => ownerFace(owner, roster)); const lifetime = /* @__PURE__ */ new Map(); -@@ -9823,7 +9823,7 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9823,7 +9823,7 @@ const models2 = view.models.map((model, at) => { const name = model.name || model.model || t("Unknown model"); const paint = modelPaint.get(at) ?? "var(--bt-us-rest)"; @@ -17275,7 +14860,7 @@ diff --git a/client.js b/client.js }); const ownerAt2 = focus?.kind === "owner" ? view.owners.findIndex((owner) => owner.key === focus.key) : -1; const modelAt = focus?.kind === "model" ? view.models.findIndex((model) => model.key === focus.key) : -1; -@@ -9871,23 +9871,23 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9871,23 +9871,23 @@ }); return { faces: faces2, models: models2, ownerAt: ownerAt2, modelAt, face: face2, keep: keep2, all: all2, part: part2, series: series2, tipSeries: tipSeries2, fillOf: fillOf2, columns: columns2, ranked: ranked2, isBot: isBot2, activeDays: activeDays2, oldest: oldest2 }; }, [view, rangeId, span, by, focusId, roster]); @@ -17307,7 +14892,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -9912,7 +9912,7 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9912,7 +9912,7 @@ ["custom", rangeId === "custom" && span ? spanText(span.from, span.to) : t("Custom"), CalendarIcon] ]; const whole = part.total.total; @@ -17316,7 +14901,7 @@ diff --git a/client.js b/client.js key: `o${at}`, kind: faces[at].kind, glyph: faces[at].glyph(32), -@@ -9924,7 +9924,7 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9924,7 +9924,7 @@ fill: face ? soften(face.paint, 0.8) : fillOf.get(`o${at}`) ?? soften(faces[at].paint), onClick: () => pick2({ kind: "owner", key: view.owners[at].key }) }); @@ -17325,7 +14910,7 @@ diff --git a/client.js b/client.js key: `m${at}`, kind: "model", glyph: models[at].glyph(32), -@@ -9936,30 +9936,30 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9936,30 +9936,30 @@ fill: face ? soften(face.paint, 0.8) : fillOf.get(`m${at}`) ?? soften(models[at].paint), onClick: () => pick2({ kind: "model", key: view.models[at].key }) }); @@ -17366,7 +14951,7 @@ diff --git a/client.js b/client.js ) ]; } else { -@@ -9968,39 +9968,39 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9968,39 +9968,39 @@ const rest = owners.filter(([at]) => !isBot(at)); const usedModels = ranked(all.models); body = [ @@ -17420,7 +15005,7 @@ diff --git a/client.js b/client.js body ); } -@@ -10150,33 +10150,216 @@ button.bt-us-row:hover{background:var(--bt-hover)} +@@ -10150,33 +10150,216 @@ @keyframes bt-us-drop{from{opacity:0;transform:translateY(-4px) scale(.96)}} @keyframes bt-us-fade{from{opacity:0}} @keyframes bt-us-cell{from{opacity:0;transform:scale(.5)}} @@ -17589,15 +15174,15 @@ diff --git a/client.js b/client.js + const names = (0, import_react37.useRef)(new Map(APPS)); + const asked = (0, import_react37.useRef)(0); + (0, import_react37.useEffect)(() => { -+ const text = query.trim(); -+ if (text === "" || search === void 0) { ++ const text2 = query.trim(); ++ if (text2 === "" || search === void 0) { + setFound(null); + setFailed(null); + return void 0; + } + const ask = ++asked.current; + const timer = setTimeout(() => { -+ search(text).then((list) => { ++ search(text2).then((list) => { + if (ask !== asked.current) return; + for (const app of list) names.current.set(app.slug, app.name); + setFound(list); @@ -17648,7 +15233,7 @@ diff --git a/client.js b/client.js const run = (task, after) => { setBusy(true); setError(null); -@@ -10184,72 +10184,184 @@ function ConnectorRow({ connector, actions }) { +@@ -10184,83 +10184,195 @@ after?.(); }, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); }; @@ -17759,21 +15344,23 @@ diff --git a/client.js b/client.js + 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)( -+ "div", -+ { className: "bt-connector-form" }, + "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)( -+ "div", -+ { className: "bt-connector-form-row" }, + "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")) + ) + ) : null, + !on && form ? (0, import_react37.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)( ++ "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", { + key: entry.id, + type: "button", @@ -17795,10 +15382,8 @@ diff --git a/client.js b/client.js + })) : (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)( - "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: () => { ++ "div", ++ { className: "bt-connector-form-row" }, + (0, import_react37.createElement)( + "a", + { className: "bt-connector-link", href: channel ? chosen?.link ?? FEISHU_WEBHOOK_URL : gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, @@ -17857,18 +15442,24 @@ diff --git a/client.js b/client.js + (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" }) ); - function download(name, text) { - const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" })); -@@ -10260,7 +10260,7 @@ function download(name, text) { +-function download(name, text) { +- const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" })); ++function download(name, text2) { ++ const url = URL.createObjectURL(new Blob([text2], { type: "text/plain;charset=utf-8" })); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = name; + anchor.click(); setTimeout(() => URL.revokeObjectURL(url), 1e3); } - var copy = (text) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text).then(() => true, () => false); +-var copy = (text) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text).then(() => true, () => false); -function whenText(time) { ++var copy = (text2) => navigator.clipboard === void 0 ? Promise.resolve(false) : navigator.clipboard.writeText(text2).then(() => true, () => false); +function whenText2(time) { const date = new Date(time); const today = (/* @__PURE__ */ new Date()).toDateString() === date.toDateString(); const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "2-digit", minute: "2-digit" }); -@@ -10270,18 +10270,18 @@ function entrySummary(entry) { +@@ -10270,18 +10270,18 @@ if (entry.source === "turn") return failureLabel(entry.code, entry.text); return entry.text; } @@ -17894,7 +15485,7 @@ diff --git a/client.js b/client.js void run(load()); }, []); const copyReport = () => run(fetchReport().then(async (next) => { -@@ -10289,7 +10289,7 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { +@@ -10289,7 +10289,7 @@ setNote(copied ? t("Copied the diagnostics.") : t("Could not copy. Select the text below and copy it.")); if (!copied) setPreview(true); })); @@ -17903,7 +15494,18 @@ diff --git a/client.js b/client.js const openIssue = () => { const win = window.open("", "_blank"); if (win !== null) win.opener = null; -@@ -10317,59 +10317,59 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { +@@ -10307,8 +10307,8 @@ + if (!inline && !copied) setPreview(true); + })); + }; +- const downloadLog = () => run(actions.diagnosticsLog().then((text) => { +- download("ds-bot-diagnostics.log", text || "No entries.\n"); ++ const downloadLog = () => run(actions.diagnosticsLog().then((text2) => { ++ download("ds-bot-diagnostics.log", text2 || "No entries.\n"); + setNote(t("Downloaded the log. Keys, tokens, and your home folder are hidden in it.")); + })); + const clearLog = () => run(actions.diagnosticsClear().then(clearClientErrors).then(load).then(() => setNote(t("Cleared the error log.")))); +@@ -10317,59 +10317,59 @@ const total = (report?.total ?? 0) + browserErrors.length; const shown = entries2.slice(0, 8); return [ @@ -17991,7 +15593,7 @@ diff --git a/client.js b/client.js ) ) ]; -@@ -10377,8 +10377,8 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { +@@ -10377,8 +10377,8 @@ var TOAST_MS = 6e3; function ErrorToast({ useUi, actions }) { const toast = useUi((state) => state.toast); @@ -18002,7 +15604,7 @@ diff --git a/client.js b/client.js if (!toast) return void 0; setLeaving(false); const fade = setTimeout(() => setLeaving(true), TOAST_MS); -@@ -10389,15 +10389,16 @@ function ErrorToast({ useUi, actions }) { +@@ -10389,15 +10389,16 @@ }; }, [toast?.token]); if (!toast) return null; @@ -18024,7 +15626,7 @@ diff --git a/client.js b/client.js ); } var FEEDBACK_CSS = ` -@@ -10416,47 +10416,56 @@ var FEEDBACK_CSS = ` +@@ -10416,47 +10416,56 @@ .bt-fb-text{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word} .bt-fb-code{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px} .bt-event-error{color:#e02135} @@ -18095,7 +15697,7 @@ diff --git a/client.js b/client.js let live = true; actions.repoStars().then((count) => { if (live && Number.isInteger(count)) setStars(count); -@@ -10466,87 +10466,72 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { +@@ -10466,87 +10466,72 @@ live = false; }; }, []); @@ -18206,7 +15808,7 @@ diff --git a/client.js b/client.js .bt-about-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} .bt-about-name{font-size:16px;line-height:22px;font-weight:600} .bt-about-version{font-size:12px;line-height:16px;color:var(--bt-ink-2);font-variant-numeric:tabular-nums} -@@ -10569,54 +10569,59 @@ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-s +@@ -10569,54 +10569,59 @@ `; // src/client/settings.js @@ -18284,7 +15886,7 @@ diff --git a/client.js b/client.js key: ink, type: "button", className: "bt-swatch", -@@ -10626,75 +10626,86 @@ function GeneralPage({ roster, actions, run }) { +@@ -10626,75 +10626,86 @@ style: { background: INKS[ink][0] }, onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] })) })), @@ -18394,7 +15996,7 @@ diff --git a/client.js b/client.js key: id, type: "button", "aria-pressed": motion === id, -@@ -10706,69 +10706,69 @@ function MotionSection({ roster, actions, run }) { +@@ -10706,69 +10706,69 @@ ); } function BotsPage({ roster, actions, run }) { @@ -18494,7 +16096,7 @@ diff --git a/client.js b/client.js useEscape(close); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); -@@ -10779,104 +10779,113 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false +@@ -10779,104 +10779,113 @@ }); const bot = page === "bots" && id ? roster.byId[id] : void 0; const room2 = page === "groups" && id ? roster.roomsById[id] : void 0; @@ -18538,7 +16140,7 @@ diff --git a/client.js b/client.js - (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, text]) => (0, import_react40.createElement)("button", { ++ pages.map(([key, text2]) => (0, import_react40.createElement)("button", { key, type: "button", className: "bt-settings-item", @@ -18546,7 +16148,7 @@ 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(text))), ++ }, (0, import_react40.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", { @@ -18649,7 +16251,7 @@ diff --git a/client.js b/client.js const bot = roster.byId[menu.id]; const room2 = roster.roomsById[menu.id]; const entry = bot ?? room2; -@@ -10884,13 +10884,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { +@@ -10884,13 +10884,22 @@ const isMainBot = isMainOf(roster, menu.id); const isTile = roster.mainBotId === menu.id; const mainCount = (roster.mainBotIds ?? []).length; @@ -18674,7 +16276,7 @@ diff --git a/client.js b/client.js bot ? [t("Edit Bot"), GearIcon, () => actions.openSettings("bots", bot.id)] : null, room2 ? [t("Group settings"), GearIcon, () => actions.openSettings("groups", room2.id)] : null, room2 ? [t("Rename group"), PencilIcon, () => actions.renameBot(room2.id)] : null, -@@ -10901,20 +10901,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { +@@ -10901,20 +10901,22 @@ bot && !isMainBot ? [t("Make Main Bot"), StarLineIcon, () => actions.setMain(bot.id, true)] : null, bot && isMainBot && mainCount > 1 ? [t("Remove Main Bot role"), StarLineIcon, () => actions.setMain(bot.id, false)] : null ], @@ -18703,7 +16305,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -10923,14 +10923,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { +@@ -10923,14 +10923,14 @@ actions.closeMenu(); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } @@ -18722,7 +16324,7 @@ diff --git a/client.js b/client.js const box = ref.current?.getBoundingClientRect(); if (!box) return; setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); -@@ -10952,14 +10952,14 @@ function AgentMenu({ menu, roster, actions, leaving }) { +@@ -10952,14 +10952,14 @@ [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true] ] ]; @@ -18742,7 +16344,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -10968,16 +10968,31 @@ function AgentMenu({ menu, roster, actions, leaving }) { +@@ -10968,16 +10968,31 @@ actions.closeMenu(); void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error)); } @@ -18775,40 +16377,10 @@ diff --git a/client.js b/client.js prefs: "ds-bot:prefs", surface: "ds-bot:surface", agentSessions: "ds-bot:agent-sessions", -@@ -11008,274 +11008,38 @@ var migrateBrowsers = (storage) => { - })); - storage.setItem(STORAGE.browsers, JSON.stringify(tabs)); - for (const earlier of LEGACY_BROWSER_KEYS) storage.removeItem(earlier); -- return; -- } catch { -- } -- } --}; --function migrateStorage(storage = globalThis.localStorage) { -- for (const key of Object.values(STORAGE)) { -- if (key === STORAGE.browsers) continue; -- for (const legacy of legacyKeys(key)) { -- try { -- const value = storage.getItem(legacy); -- if (value !== null && storage.getItem(key) === null) storage.setItem(key, value); -- storage.removeItem(legacy); -- } catch { -- } -- } -- } -- try { -- migrateBrowsers(storage); -- } catch { -- } -- try { -- const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); -- if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { -- storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); -- } -- } catch { -- } --} -- +@@ -11038,244 +11038,8 @@ + } + } + -// src/client/member-status.js -var import_react30 = require("react"); -var NO_WORK = { bots: {}, rooms: {}, approvals: [] }; @@ -18974,34 +16546,7 @@ diff --git a/client.js b/client.js - (0, import_react30.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`), - entry.approvals.map((approval) => (0, import_react30.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) - ); -+ return; -+ } catch { -+ } -+ } -+}; -+function migrateStorage(storage = globalThis.localStorage) { -+ for (const key of Object.values(STORAGE)) { -+ if (key === STORAGE.browsers) continue; -+ for (const legacy of legacyKeys(key)) { -+ try { -+ const value = storage.getItem(legacy); -+ if (value !== null && storage.getItem(key) === null) storage.setItem(key, value); -+ storage.removeItem(legacy); -+ } catch { -+ } -+ } -+ } -+ try { -+ migrateBrowsers(storage); -+ } catch { -+ } -+ try { -+ const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); -+ if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { -+ storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); -+ } -+ } catch { - } +- } - if (entry.state !== "work") return null; - const where = entry.doing.roomId === room2.id ? "" : entry.doing.roomId ? t("in {name}", { name: roster.roomsById[entry.doing.roomId]?.name ?? t("a group chat") }) : t("in its own chat"); - const stop = () => { @@ -19024,7 +16569,7 @@ diff --git a/client.js b/client.js - const entries2 = room2 ? memberWork(room2, work) : []; - const now = useNow2(entries2.some((entry) => entry.state !== "idle")); - return { entries: entries2, now }; - } +-} -var MEMBER_CSS = ` -.bt-mw-dock-stack{box-sizing:border-box;display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto 6px;pointer-events:none} -.bt-mw-dock-stack>.bt-event.bt-mw-dock{align-self:stretch;margin:0} @@ -19060,7 +16605,7 @@ diff --git a/client.js b/client.js -@keyframes bt-mw-in{from{opacity:0;transform:translateY(4px)}} -@media (prefers-reduced-motion:reduce){.bt-mw-dock>button,.bt-mw-row{animation:none}} -`; - +- // src/client/details-drawer.js -var import_react31 = require("react"); -function transcriptOf(list, id, name, hiddenTurns = []) { @@ -19078,7 +16623,7 @@ diff --git a/client.js b/client.js var TAB_LABELS = { details: "Details", browser: "Browser" }; var hostOf = (url) => { try { -@@ -11289,10 +11289,10 @@ var EMPTY_TABS = { ids: [], active: null }; +@@ -11289,10 +11289,10 @@ var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" }; var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]); function PermissionRow({ bot, roster, actions, run }) { @@ -19091,7 +16636,7 @@ diff --git a/client.js b/client.js if (pending === saved) setPending(null); }, [pending, saved]); if (!catalog?.options?.length) return null; -@@ -11302,14 +11302,14 @@ function PermissionRow({ bot, roster, actions, run }) { +@@ -11302,14 +11302,14 @@ setPending(value); void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null)); }; @@ -19110,7 +16655,7 @@ diff --git a/client.js b/client.js key: option.value, type: "button", "aria-pressed": option.value === current, -@@ -11323,42 +11323,42 @@ function PermissionRow({ bot, roster, actions, run }) { +@@ -11323,42 +11323,42 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); const pages = useBrowserPages((map) => map); @@ -19168,7 +16713,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-bcard-x", "aria-label": t("Close tab"), -@@ -11367,87 +11367,195 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow +@@ -11367,87 +11367,195 @@ void actions.browserCloseTab(bot.id, tabId); } }, "×"), @@ -19189,19 +16734,24 @@ diff --git a/client.js b/client.js + (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")) ); } - function About({ text, title = "About" }) { +-function About({ text, title = "About" }) { - const [open, setOpen] = (0, import_react31.useState)(false); -+ const [open, setOpen] = (0, import_react42.useState)(false); - const long = text.split("\n").length > 5 || text.length > 240; +- 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 long = text2.split("\n").length > 5 || text2.length > 240; + const body = (0, import_react42.useRef)(null); useMorph(body, open); - return (0, import_react31.createElement)( + return (0, import_react42.createElement)( -+ "div", -+ null, + "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 }, text), ++ (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 + ); +} @@ -19252,11 +16802,8 @@ diff --git a/client.js b/client.js + const people2 = facePeople(room2, roster); + const pick2 = (bot) => call("social/face-lead", { roomId: room2.id, bot }); + return (0, import_react42.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 ++ "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, { @@ -19407,7 +16954,7 @@ diff --git a/client.js b/client.js 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; -@@ -11457,7 +11457,7 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a +@@ -11457,7 +11457,7 @@ const bot = owner; const running = useSessionStatus((map) => map.get(part)?.running === true); const doing = useActivity((map) => map[bot?.id ?? sessionId]); @@ -19416,17 +16963,17 @@ diff --git a/client.js b/client.js const usage = useSessions((list) => usageOf(list, [part, ...bot?.parts ?? room2?.parts ?? []])); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); -@@ -11467,41 +11467,67 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a +@@ -11467,41 +11467,67 @@ return false; }); const share = () => { - void navigator.clipboard?.writeText(transcript).then(() => { -+ const text = transcript; -+ if (!text) { ++ const text2 = transcript; ++ if (!text2) { + actions.notify?.(t("Nothing to copy yet")); + return; + } -+ void copiedToClipboard(text).then((written) => { ++ void copiedToClipboard(text2).then((written) => { + if (!written) { + actions.reportError?.(new Error(t("Could not copy the conversation")), "share"); + return; @@ -19500,7 +17047,7 @@ diff --git a/client.js b/client.js ]; } const current = bot?.name ?? room2.name; -@@ -11515,12 +11515,12 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a +@@ -11515,12 +11515,12 @@ const subtitle = bot ? bot.role : roomSubtitle(room2, roster); const tabs = bot ? ["details", "browser"] : ["details"]; return [ @@ -19517,7 +17064,7 @@ diff --git a/client.js b/client.js className: "bt-drawer-name-input", autoFocus: true, value: draftName, -@@ -11535,98 +11535,97 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a +@@ -11535,98 +11535,97 @@ setEditing(false); } } @@ -19661,7 +17208,7 @@ diff --git a/client.js b/client.js var room = { rule: null, selector: "" }; var roomRule = () => { const live = room.rule?.parentStyleSheet?.ownerNode?.isConnected; -@@ -11674,11 +11674,11 @@ var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(pref +@@ -11674,11 +11674,11 @@ var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab"); function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) { const pages = useBrowserPages((map) => map); @@ -19678,7 +17225,7 @@ diff --git a/client.js b/client.js const box = wrap.current; const capsule = ind.current; if (!box || !capsule) return void 0; -@@ -11707,7 +11707,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, +@@ -11707,7 +11707,7 @@ trail.cancel(); }; }); @@ -19687,7 +17234,7 @@ diff --git a/client.js b/client.js const box = wrap.current; if (!box) return void 0; const observer = new ResizeObserver(() => { -@@ -11721,13 +11721,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, +@@ -11721,13 +11721,13 @@ observer.observe(box); return () => observer.disconnect(); }, []); @@ -19704,7 +17251,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -11737,13 +11737,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, +@@ -11737,13 +11737,13 @@ title: bot.name, onClick: () => onView("details") }, @@ -19721,7 +17268,7 @@ diff --git a/client.js b/client.js "button", { key: tabId, -@@ -11764,9 +11764,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, +@@ -11764,9 +11764,9 @@ } } }, @@ -19734,7 +17281,7 @@ diff --git a/client.js b/client.js className: "bt-ptab-x", role: "button", "aria-label": t("Close tab"), -@@ -11778,20 +11778,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, +@@ -11778,20 +11778,20 @@ }, "×") ); }), @@ -19762,7 +17309,7 @@ diff --git a/client.js b/client.js const element = frame.current; if (!element || leaving || !tabId) { void actions.browserShow(null); -@@ -11817,7 +11817,7 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) +@@ -11817,43 +11817,43 @@ window.removeEventListener("resize", schedule); }; }, [tabId, showGuest, leaving]); @@ -19771,7 +17318,18 @@ diff --git a/client.js b/client.js setDraft(null); if (landed && !page.url) input.current?.focus(); }, [tabId, landed]); -@@ -11833,27 +11833,27 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) + const go = (event) => { + event.preventDefault(); +- const text = draft ?? ""; +- if (text.trim() === "") return; ++ const text2 = draft ?? ""; ++ if (text2.trim() === "") return; + setDraft(null); + input.current?.blur(); +- void actions.browserNavigate(tabId, text).then((opened) => { ++ void actions.browserNavigate(tabId, text2).then((opened) => { + if (opened) actions.browserFocus(tabId); + }); }; const editing = draft !== null; const secure = /^https:/i.test(page.url); @@ -19808,7 +17366,7 @@ diff --git a/client.js b/client.js ref: input, type: "text", spellCheck: false, -@@ -11877,25 +11877,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) +@@ -11877,25 +11877,25 @@ } }) ), @@ -19844,7 +17402,7 @@ diff --git a/client.js b/client.js ) : null ) ); -@@ -11907,41 +11907,41 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu +@@ -11907,41 +11907,41 @@ const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2); const active = tabState.active; const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE); @@ -19902,7 +17460,7 @@ diff --git a/client.js b/client.js let timer; const onResize = () => { setResizing(true); -@@ -11955,28 +11955,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu +@@ -11955,28 +11955,28 @@ window.removeEventListener("resize", onResize); }; }, []); @@ -19938,7 +17496,7 @@ diff --git a/client.js b/client.js if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0; setReading(true); const timer = setTimeout(() => setReading(false), READ_FLASH_MS); -@@ -12013,7 +12013,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu +@@ -12013,7 +12013,7 @@ return actions[name](...args); }; } @@ -19947,7 +17505,7 @@ diff --git a/client.js b/client.js const move = fly.current; if (!move) return; fly.current = null; -@@ -12096,7 +12096,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu +@@ -12096,7 +12096,7 @@ for (const name of ends) grip.addEventListener(name, onEnd); }; const hasStrip = canBrowse && tabState.ids.length > 0; @@ -19956,7 +17514,7 @@ diff --git a/client.js b/client.js "aside", { ref: root, -@@ -12113,15 +12113,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu +@@ -12113,15 +12113,15 @@ if (event.target === event.currentTarget) setLanded(true); } }, @@ -19977,7 +17535,7 @@ diff --git a/client.js b/client.js sessionId: pane.id, startRenaming: pane.renaming, startTab: pane.tab, -@@ -12217,9 +12217,79 @@ var BROWSER_CSS = ` +@@ -12217,9 +12217,79 @@ @media (prefers-reduced-motion:reduce){.bt-ptabs,.bt-panel-view,.bt-bcard,.bt-ptab-name{animation:none}} `; @@ -20059,7 +17617,7 @@ diff --git a/client.js b/client.js var DIALOG_EXIT_MS = 160; var MENU_EXIT_MS3 = 120; function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useWork, useRegistry, useBrowserPages, useBrowserTabs, useUpdate, actions }) { -@@ -12227,15 +12227,15 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity +@@ -12227,15 +12227,15 @@ const ui2 = useUi((value) => value); useRegistry((value) => value); const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null); @@ -20079,7 +17637,7 @@ diff --git a/client.js b/client.js if (open) { setKept(open); return void 0; -@@ -12245,19 +12245,26 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity +@@ -12245,19 +12245,26 @@ }, [ui2.details, ui2.view, ui2.renaming, ui2.tab]); const pane = open ?? kept; const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS); @@ -20116,16 +17674,45 @@ diff --git a/client.js b/client.js ); } -@@ -12304,7 +12304,7 @@ function readPage(limits) { +@@ -12294,19 +12294,19 @@ + const auto = String(el.getAttribute("autocomplete") || "").toLowerCase(); + return el.type === "password" || /(^|\s)(cc-|one-time-code)/.test(auto) || SECRET_HINT.test(`${el.getAttribute("name") || ""} ${el.id || ""}`); + }; +- const keep = (text2, end = "") => { +- if (text2 === "") return ""; +- stash.push(text2); ++ const keep = (text3, end = "") => { ++ if (text3 === "") return ""; ++ stash.push(text3); + return ` + + ${stash.length - 1}${end} + `; }; - const expand = (text2) => text2.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); +- const expand = (text2) => text2.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); - const flow = (text2) => text2.split("\n").map((line) => line.replace(/[ \t\f\v\u00a0\u2028\u2029]+/g, " ").trim().replace(/^· /, "")).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\u0003\n\n(?=\u0001\d+\u0003)/g, "\n").trim(); -+ const flow = (text2) => text2.split("\n").map((line2) => line2.replace(/[ \t\f\v\u00a0\u2028\u2029]+/g, " ").trim().replace(/^· /, "")).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\u0003\n\n(?=\u0001\d+\u0003)/g, "\n").trim(); - const block = (text2) => expand(flow(text2)); - const oneLine = (text2) => block(text2).replace(/\s*\n+\s*/g, " ").trim(); +- const block = (text2) => expand(flow(text2)); +- const oneLine = (text2) => block(text2).replace(/\s*\n+\s*/g, " ").trim(); ++ const expand = (text3) => text3.replace(/\u0001(\d+)[\u0002\u0003]/g, (_, index) => expand(stash[Number(index)])); ++ const flow = (text3) => text3.split("\n").map((line2) => line2.replace(/[ \t\f\v\u00a0\u2028\u2029]+/g, " ").trim().replace(/^· /, "")).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\u0003\n\n(?=\u0001\d+\u0003)/g, "\n").trim(); ++ const block = (text3) => expand(flow(text3)); ++ const oneLine = (text3) => block(text3).replace(/\s*\n+\s*/g, " ").trim(); const styleOf = (el) => (el.ownerDocument.defaultView || window).getComputedStyle(el); -@@ -12372,7 +12372,7 @@ ${fence}`; + const childrenOf = (el) => { + if (el.shadowRoot) return el.shadowRoot.childNodes; +@@ -12340,8 +12340,8 @@ + items.push(item); + return { item, number: items.length, secret }; + }; +- const nameOf = (el, text2, secret) => collapse( +- el.getAttribute("aria-label") || text2 || (el.localName === "input" && !secret && NO_VALUE_TYPES.has(el.type) ? el.value : "") || el.getAttribute("placeholder") || el.title || el.getAttribute("alt") || el.querySelector?.("img[alt]")?.alt || el.getAttribute("name") || "" ++ const nameOf = (el, text3, secret) => collapse( ++ el.getAttribute("aria-label") || text3 || (el.localName === "input" && !secret && NO_VALUE_TYPES.has(el.type) ? el.value : "") || el.getAttribute("placeholder") || el.title || el.getAttribute("alt") || el.querySelector?.("img[alt]")?.alt || el.getAttribute("name") || "" + ).slice(0, 80); + const fenced = (code, lang) => { + const longest = Math.max(2, ...(code.match(/`{3,}/g) || []).map((run) => run.length)); +@@ -12372,7 +12372,7 @@ const marker = ordered ? `${number++}. ` : "- "; if (body2 === "") continue; const pad2 = " ".repeat(marker.length); @@ -20134,7 +17721,7 @@ diff --git a/client.js b/client.js } return keep(lines.join("\n")); }; -@@ -12398,11 +12398,11 @@ ${children(cell, ctx)} +@@ -12398,11 +12398,11 @@ let width = 0; for (const row of rows.slice(0, limits.tableRows + 1)) { if (over()) break; @@ -20148,13 +17735,19 @@ diff --git a/client.js b/client.js carry[column].left -= 1; column += 1; } -@@ -12414,14 +12414,14 @@ ${children(cell, ctx)} +@@ -12410,18 +12410,18 @@ + for (const cell of row.cells) { + place(); + if (styleOf(cell).display === "none") continue; +- const text2 = cellText(cell, ctx); ++ const text3 = cellText(cell, ctx); const span = Math.max(1, Math.min(limits.tableCols, Number(cell.colSpan) || 1)); const down = Math.max(1, Math.min(limits.tableRows, Number(cell.rowSpan) || 1)); for (let offset = 0; offset < span; offset += 1) { - line[column] = offset === 0 ? text2 : ""; -+ line2[column] = offset === 0 ? text2 : ""; - if (down > 1) carry[column] = { text: offset === 0 ? text2 : "", left: down - 1 }; +- if (down > 1) carry[column] = { text: offset === 0 ? text2 : "", left: down - 1 }; ++ line2[column] = offset === 0 ? text3 : ""; ++ if (down > 1) carry[column] = { text: offset === 0 ? text3 : "", left: down - 1 }; column += 1; } } @@ -20166,7 +17759,20 @@ diff --git a/client.js b/client.js } width = Math.min(width, limits.tableCols); if (grid.length === 0 || width === 0) return caption ? ` -@@ -12552,7 +12552,7 @@ ${"#".repeat(Math.min(6, Number(level) || 2))} ${title}${mark} +@@ -12480,9 +12480,9 @@ + if (node.nodeType === 3) { + if (!ctx.shown) return ""; + const data = node.data.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, ""); +- const text2 = ctx.pre ? data : data.replace(/[\t\n\r ]+/g, " "); +- chars += text2.length; +- return text2; ++ const text3 = ctx.pre ? data : data.replace(/[\t\n\r ]+/g, " "); ++ chars += text3.length; ++ return text3; + } + if (node.nodeType !== 1) return ""; + const el = node; +@@ -12552,7 +12552,7 @@ } if (tag === "blockquote") { const quoted = block(body2); @@ -20175,7 +17781,57 @@ diff --git a/client.js b/client.js } const end = body2.trimEnd().length; const marked = mark ? `${body2.slice(0, end)}${mark}${body2.slice(end)}` : body2; -@@ -13052,11 +13052,118 @@ function serveBrowserReads({ browsers, post, clientId = globalThis.crypto.random +@@ -12570,12 +12570,12 @@ + return marked; + } + const body = document.body; +- let text = ""; ++ let text2 = ""; + if (body) { +- text = block(render(body, { depth: 0, frames: 0, shown: true, pre: false })).replace(/\u00a0/g, " "); ++ text2 = block(render(body, { depth: 0, frames: 0, shown: true, pre: false })).replace(/\u00a0/g, " "); + } +- if (text.length > limits.chars) { +- text = text.slice(0, limits.chars); ++ if (text2.length > limits.chars) { ++ text2 = text2.slice(0, limits.chars); + stop ||= `the text passed ${limits.chars} characters`; + } + const page = { +@@ -12585,7 +12585,7 @@ + description: document.querySelector('meta[name="description"]')?.content || "", + selection: String(getSelection() || "").slice(0, 4e3), + format: "markdown", +- text, ++ text: text2, + items, + stats: { ...stats, ms: Math.round(performance.now() - started) } + }; +@@ -12613,15 +12613,15 @@ + var BLOCKED = "Cloud metadata addresses do not open here."; + var blockedAddress = (url) => URL.canParse(url) && METADATA_HOST.test(new URL(url).hostname); + function browserAddress(input) { +- const text = String(input ?? "").trim(); +- if (text === "") return null; +- if (/^[a-z][a-z\d+.-]*:\/\//i.test(text)) return /^https?:\/\//i.test(text) && URL.canParse(text) ? new URL(text).href : null; +- if (/^(about|javascript|data|file|blob|chrome|devtools|view-source):/i.test(text)) return null; +- const host = text.split(/[/?#]/)[0]; ++ const text2 = String(input ?? "").trim(); ++ if (text2 === "") return null; ++ if (/^[a-z][a-z\d+.-]*:\/\//i.test(text2)) return /^https?:\/\//i.test(text2) && URL.canParse(text2) ? new URL(text2).href : null; ++ if (/^(about|javascript|data|file|blob|chrome|devtools|view-source):/i.test(text2)) return null; ++ const host = text2.split(/[/?#]/)[0]; + const local = /^(localhost|127(\.\d+){3}|10(\.\d+){3}|192\.168(\.\d+){2}|172\.(1[6-9]|2\d|3[01])(\.\d+){2}|\[[\da-f:]+\])(:\d+)?$/i.test(host); +- const looksLikeHost = !/\s/.test(text) && (local || /^[^\s.]+(\.[^\s.]+)+(:\d+)?$/.test(host)); +- if (looksLikeHost && URL.canParse(`http://${text}`)) return new URL(`${local ? "http" : "https"}://${text}`).href; +- return `${SEARCH_URL}${encodeURIComponent(text)}`; ++ const looksLikeHost = !/\s/.test(text2) && (local || /^[^\s.]+(\.[^\s.]+)+(:\d+)?$/.test(host)); ++ if (looksLikeHost && URL.canParse(`http://${text2}`)) return new URL(`${local ? "http" : "https"}://${text2}`).href; ++ return `${SEARCH_URL}${encodeURIComponent(text2)}`; + } + function createBotBrowsers({ bridge, createSource: createSource2, storage = globalThis.localStorage, document: doc = globalThis.document }) { + const views = /* @__PURE__ */ new Map(); +@@ -13052,11 +13052,235 @@ }; } @@ -20228,8 +17884,8 @@ diff --git a/client.js b/client.js + const frame = col?.parentElement; + if (!frame) return null; + const name = frame.className; -+ const text = typeof name === "string" ? name : name?.baseVal ?? ""; -+ return text.includes("_frame") ? frame : null; ++ const text2 = typeof name === "string" ? name : name?.baseVal ?? ""; ++ return text2.includes("_frame") ? frame : null; +} +function levelShell(doc, from) { + const page = doc.scrollingElement || doc.documentElement; @@ -20270,6 +17926,123 @@ diff --git a/client.js b/client.js + }; +} + ++// src/client/project-board.js ++var import_react46 = require("react"); ++var PROJECT_BOARD_CSS = ` ++.bt-project-board{padding:16px 18px} ++.bt-project-group{margin:0 0 14px} ++.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} ++.bt-project-row{display:flex;gap:10px;align-items:center;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px;cursor:default} ++.bt-project-row[data-column="done"]{opacity:.7} ++.bt-project-row .bt-project-ref{margin-left:auto;font-size:11px;opacity:.6} ++.bt-project-empty{padding:22px 4px;opacity:.7} ++.bt-project-actions{display:flex;gap:8px;margin:0 0 14px} ++`; ++var PROJECT_SOURCES = ["bot", "group", "workflow"]; ++var SOURCE_LABELS = { bot: "Bots", group: "Groups", workflow: "Workflows" }; ++var text = (value) => typeof value === "string" ? value.trim() : ""; ++function projectBoardView({ project, rows, canManage } = {}) { ++ const list = Array.isArray(rows) ? rows.filter((row) => row !== null && typeof row === "object" && text(row.id) !== "") : []; ++ const groups = PROJECT_SOURCES.map((source) => ({ ++ source, ++ label: SOURCE_LABELS[source], ++ rows: list.filter((row) => row.source === source).map((row) => ({ ++ id: text(row.id), ++ key: `task:${text(row.id)}`, ++ column: text(row.column) || "not-started", ++ assignee: text(row.assignee) || null, ++ runId: text(row.runId) || null, ++ nodeId: text(row.nodeId) || null ++ })) ++ })).filter((group) => group.rows.length > 0); ++ return { ++ projectId: text(project?.id) || null, ++ title: text(project?.name) || "Project", ++ canManage: canManage === true, ++ groups, ++ count: list.length, ++ empty: list.length === 0, ++ emptyText: "Nothing under this Project yet." ++ }; ++} ++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)( ++ "section", ++ { key: group.source, className: "bt-project-group", "data-source": group.source }, ++ (0, import_react46.createElement)("h4", null, t(group.label)), ++ group.rows.map((row) => (0, import_react46.createElement)( ++ "div", ++ { ++ key: row.key, ++ className: "bt-project-row", ++ "data-task-id": row.id, ++ "data-column": row.column, ++ role: "button", ++ tabIndex: 0, ++ onClick: () => onOpen?.(row.id), ++ onKeyDown: (event) => { ++ if (event.key === "Enter" || event.key === " ") onOpen?.(row.id); ++ } ++ }, ++ (0, import_react46.createElement)("span", { className: "bt-project-column" }, t(row.column)), ++ (0, import_react46.createElement)("span", { className: "bt-project-title" }, row.assignee ? `@${row.assignee}` : "—"), ++ row.runId === null ? null : (0, import_react46.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`) ++ )) ++ )); ++} ++function ProjectBoardPane({ project, rows, canManage, onOpen }) { ++ const view = projectBoardView({ project, rows, canManage }); ++ return (0, import_react46.createElement)( ++ "div", ++ { className: "bt-project-board", "data-can-manage": view.canManage ? "true" : "false" }, ++ (0, import_react46.createElement)("h3", null, view.title), ++ // Management affordances are drawn only for the owner; the refusal itself is the host's. ++ view.canManage ? (0, import_react46.createElement)("div", { className: "bt-project-actions" }, (0, import_react46.createElement)("button", { type: "button" }, t("Project settings")), (0, import_react46.createElement)("button", { type: "button" }, t("Members"))) : null, ++ (0, import_react46.createElement)(ProjectBoardRows, { view, onOpen }) ++ ); ++} ++function ProjectBoardOverlay() { ++ const [state, setState] = (0, import_react46.useState)(null); ++ const close = (0, import_react46.useCallback)(() => { ++ if (typeof window !== "undefined") window.location.hash = ""; ++ setState(null); ++ }, []); ++ (0, import_react46.useEffect)(() => { ++ if (typeof window === "undefined") return; ++ if (!window.location.hash.startsWith("#project")) return; ++ const wanted = window.location.hash.slice("#project".length).replace(/^=/, ""); ++ let live = true; ++ void (async () => { ++ try { ++ const listing = await call("projects"); ++ const projects = listing?.projects ?? []; ++ const project = wanted === "" ? projects[0] : projects.find((item) => item.id === wanted); ++ if (project === void 0) { ++ if (live) setState({ project: null, rows: [], canManage: false }); ++ return; ++ } ++ const [read, board] = await Promise.all([call("project-read", { id: project.id }), call("project-board", { id: project.id })]); ++ if (live) setState({ project: read?.project ?? project, rows: board?.tasks ?? [], canManage: read?.canManage === true }); ++ } catch { ++ if (live) setState({ project: null, rows: [], canManage: false }); ++ } ++ })(); ++ return () => { ++ live = false; ++ }; ++ }, []); ++ if (state === null) return null; ++ return (0, import_react46.createElement)( ++ "div", ++ { className: "bt-overlay bt-project-overlay", role: "dialog", "aria-label": t("Project board") }, ++ (0, import_react46.createElement)("button", { type: "button", className: "bt-project-close", onClick: close }, t("Close")), ++ state.project === null ? (0, import_react46.createElement)("div", { className: "bt-project-empty" }, t("No Project here.")) : (0, import_react46.createElement)(ProjectBoardPane, { project: state.project, rows: state.rows, canManage: state.canManage, onOpen: (id) => { ++ window.location.hash = `#task=${id}`; ++ } }) ++ ); ++} ++ +// src/client/friend-lines.js +function friendLineNews(bots, known, isOpen, selfId) { + const next = { ...known }; @@ -20295,7 +18068,7 @@ diff --git a/client.js b/client.js function disposeAll(disposers, label) { for (const dispose of [...disposers].reverse()) { try { -@@ -13075,8 +13075,8 @@ function surfaceGroups(install) { +@@ -13075,8 +13075,8 @@ }; return { reconcile(wanted) { @@ -20306,7 +18079,7 @@ diff --git a/client.js b/client.js if (!wanted[group] || mounted[group].length > 0) continue; try { mounted[group] = install[group](); -@@ -13086,7 +13086,7 @@ function surfaceGroups(install) { +@@ -13086,7 +13086,7 @@ } }, dispose() { @@ -20315,16 +18088,16 @@ diff --git a/client.js b/client.js } }; } -@@ -13105,7 +13105,7 @@ function writeMode(mode, storage = globalThis.localStorage) { +@@ -13105,7 +13105,7 @@ } // src/client/user-message.js -var import_react34 = require("react"); -+var import_react46 = require("react"); ++var import_react47 = require("react"); var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1); var canDrawUserMessages = () => typeof ui.projectUserText === "function"; function contentParts(content) { -@@ -13119,10 +13119,63 @@ function contentParts(content) { +@@ -13119,87 +13119,165 @@ } return { text: texts2.join("\n"), attachments, rest }; } @@ -20373,7 +18146,8 @@ diff --git a/client.js b/client.js + return faceAliases(record, seat.bot).map((name) => ({ ...target, name, mentionLabel })); + }); + return bots.concat(humans); -+} + } +-function userTextParts(text, bots, referenceLabels = []) { +function messageAuthor(data, roster, sessionId) { + const source = data?.source ?? {}; + const authorId = source.authorId; @@ -20388,68 +18162,80 @@ 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 = []; -@@ -13132,43 +13132,44 @@ function userTextParts(text, bots, referenceLabels = []) { - if (span.start > cursor) parts.push({ text: text.slice(cursor, span.start) }); - parts.push({ bot: span.bot, label: text.slice(span.start + 1, span.end) }); + let cursor = 0; +- for (const span of mentionSpans(text, bots, { references: referenceLabels })) { ++ for (const span of mentionSpans(text2, bots, { references: referenceLabels })) { + if (!span.bot) continue; +- if (span.start > cursor) parts.push({ text: text.slice(cursor, span.start) }); +- parts.push({ bot: span.bot, label: text.slice(span.start + 1, span.end) }); ++ if (span.start > cursor) parts.push({ text: text2.slice(cursor, span.start) }); ++ parts.push({ bot: span.bot, label: text2.slice(span.start + 1, span.end) }); cursor = span.end; -+ if (text.startsWith(" ", cursor)) cursor += 1; ++ if (text2.startsWith(" ", cursor)) cursor += 1; } - if (cursor < text.length) parts.push({ text: text.slice(cursor) }); +- if (cursor < text.length) parts.push({ text: text.slice(cursor) }); ++ if (cursor < text2.length) parts.push({ text: text2.slice(cursor) }); return parts; } - function UserText({ text, bots, referenceLabels, skillNames, references, actions }) { +-function UserText({ text, bots, referenceLabels, skillNames, references, actions }) { - return userTextParts(text, bots, referenceLabels).map((part, index) => part.bot ? (0, import_react34.createElement)( - "button", - { key: index, type: "button", className: "bt-umention", title: `@${part.bot.name}`, onClick: () => actions.openSession(part.bot.id) }, - (0, import_react34.createElement)(BotMark, { bot: part.bot, size: 16 }), - part.label - ) : (0, import_react34.createElement)(import_react34.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references))); -+ const chip = (part) => [(0, import_react46.createElement)(BotMark, { bot: part.bot.draw ?? part.bot, size: 16 }), `@${part.bot.mentionLabel ?? part.label}`]; -+ return userTextParts(text, bots, referenceLabels).map((part, index) => { -+ if (!part.bot) return (0, import_react46.createElement)(import_react46.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}`]; ++ 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)); + const title = `@${part.bot.name}`; -+ return typeof part.bot.id === "string" ? (0, import_react46.createElement)("button", { key: index, type: "button", className: "bt-umention", title, onClick: () => actions.openSession(part.bot.id) }, chip(part)) : (0, import_react46.createElement)("span", { key: index, className: "bt-umention", title }, chip(part)); ++ 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)); + }); } 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_react46.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => { ++ return (0, import_react47.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_react46.createElement)(import_react46.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; } 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_react46.createElement)( ++ return (0, import_react47.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_react46.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null, -+ (0, import_react46.createElement)("span", { className: "bt-ufile-text" }, (0, import_react46.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react46.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) ); })); } - function CopyButton({ text }) { +-function CopyButton({ text }) { - const [copied, setCopied] = (0, import_react34.useState)(false); -+ const [copied, setCopied] = (0, import_react46.useState)(false); ++function CopyButton({ text: text2 }) { ++ const [copied, setCopied] = (0, import_react47.useState)(false); const copy2 = () => { - void navigator.clipboard?.writeText(text).then(() => { +- void navigator.clipboard?.writeText(text).then(() => { ++ void navigator.clipboard?.writeText(text2).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1e3); }); }; - 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_react46.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_react46.createElement)(CheckIcon) : (0, import_react46.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)); } var clock3 = (time) => typeof time === "number" ? new Date(time).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }) : void 0; function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageImages, openFile, openSkill }) { -@@ -13178,28 +13178,52 @@ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageIma + const roster = useRoster((value) => value); + const data = node.data ?? {}; +- const { text, attachments, rest } = contentParts(data.content); ++ const { text: text2, attachments, rest } = contentParts(data.content); const referenceLabels = data.referenceLabels ?? []; const references = openFile && openSkill ? { openFile, openSkill } : void 0; const steering = node.kind === "steering"; @@ -20461,7 +18247,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_react46.createElement)( ++ return (0, import_react47.createElement)( "div", - { className: "bt-umsg", "data-steering": steering || void 0 }, - (0, import_react34.createElement)( @@ -20473,28 +18259,28 @@ diff --git a/client.js b/client.js + "data-author-kind": author?.authorKind, + "data-author-name": author?.name + }, -+ person ? (0, import_react46.createElement)("span", { className: "bt-lead", title: author.name }, (0, import_react46.createElement)(PersonMark, { name: author.name })) : null, -+ faceMine ? (0, import_react46.createElement)("span", { className: "bt-lead", title: myName || void 0 }, (0, import_react46.createElement)(PersonMark, { name: myName || void 0 })) : null, -+ (0, import_react46.createElement)( ++ 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)( "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_react46.createElement)("span", { className: "bt-uauthor" }, myName) : author && (incoming || multiHuman) ? (0, import_react46.createElement)("span", { className: "bt-uauthor" }, author.name, author.ownerName ? (0, import_react46.createElement)("span", { className: "bt-chip" }, t("Friend's Bot")) : null) : null, -+ attachments.length > 0 ? (0, import_react46.createElement)(Attachments, { attachments, renderMessageImages }) : null, -+ text !== "" || rest.length > 0 ? (0, import_react46.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)( "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_react46.createElement)(UserText, { text, bots: mentionableBots(roster, sessionId, { fromFriend: Boolean(incoming && author.ownerName) }), referenceLabels, skillNames: data.skillNames ?? [], references, actions }), -+ rest.map((block, index) => ui.JsonBlock ? (0, import_react46.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) ) : 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_react46.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 ), - text === "" ? null : (0, import_react34.createElement)("div", { className: "bt-utools" }, (0, import_react34.createElement)(CopyButton, { text })) -+ text === "" ? null : (0, import_react46.createElement)("div", { className: "bt-utools" }, (0, import_react46.createElement)(CopyButton, { text })) ++ text2 === "" ? null : (0, import_react47.createElement)("div", { className: "bt-utools" }, (0, import_react47.createElement)(CopyButton, { text: text2 })) ); } var USER_MESSAGE_CSS = ` @@ -20511,7 +18297,7 @@ diff --git a/client.js b/client.js .bt-ubody a{color:inherit} .bt-umention{display:inline-flex;align-items:center;gap:3px;vertical-align:-3px;max-width:100%;padding:0 1px;border:0;background:none;color:inherit;font:inherit;font-weight:600;line-height:inherit;cursor:pointer;border-radius:6px;white-space:nowrap} .bt-umention:hover{text-decoration:underline;text-underline-offset:2px} -@@ -13216,42 +13216,115 @@ var USER_MESSAGE_CSS = ` +@@ -13216,42 +13216,115 @@ `; // src/client/approval-notices.js @@ -20596,33 +18382,33 @@ diff --git a/client.js b/client.js +} + +// src/client/stats-dock.js -+var import_react47 = require("react"); ++var import_react48 = require("react"); +var STATS_DOCK = "conversation.composer.dock"; +var STATS_ID = "stats"; +var STATS_IDS = [STATS_ID, "activity"]; +var noun = (count, one, many) => `${count} ${Number(count) === 1 ? one : many}`; +function pluralStats(t2) { + return (key, params) => { -+ const text = t2(key, params); -+ if (key !== "stats.counts" || !params) return text; ++ const text2 = t2(key, params); ++ if (key !== "stats.counts" || !params) return text2; + const { turns, steps } = params; -+ return text === `${turns} turns ${steps} steps` ? `${noun(turns, "turn", "turns")} ${noun(steps, "step", "steps")}` : text; ++ return text2 === `${turns} turns ${steps} steps` ? `${noun(turns, "turn", "turns")} ${noun(steps, "step", "steps")}` : text2; + }; - } ++} +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_react47.useMemo)(() => typeof props.t === "function" ? pluralStats(props.t) : props.t, [props.t]); ++ const t2 = (0, import_react48.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_react47.createElement)(entry.component, { ...props, t: t2 }); ++ return (0, import_react48.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_react48 = require("react"); ++var import_react49 = require("react"); var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout"]; function apply(ctx) { + const flybot = createClientApi2(); @@ -20640,7 +18426,7 @@ diff --git a/client.js b/client.js migrateStorage(); const UNREAD_KEY = STORAGE.unread; let savedUnread = {}; -@@ -13274,7 +13274,13 @@ function apply(ctx) { +@@ -13274,7 +13274,13 @@ } catch { seenAt = {}; } @@ -20655,7 +18441,7 @@ diff --git a/client.js b/client.js const surface = createSource({ mode: readMode(), plain: false }); let agentSessionCache = /* @__PURE__ */ new Set(); try { -@@ -13308,6 +13308,8 @@ function apply(ctx) { +@@ -13308,6 +13308,8 @@ return () => window.removeEventListener("pagehide", onPageHide); }, "ds-bot: remember visits on page hide"); const activity = createSource({}); @@ -20664,7 +18450,7 @@ diff --git a/client.js b/client.js const work = createSource({ bots: {}, approvals: [] }); let workKey = ""; const live = createSource({}); -@@ -13344,13 +13344,25 @@ function apply(ctx) { +@@ -13344,13 +13344,25 @@ } return { ...state, unread }; }); @@ -20691,7 +18477,7 @@ diff --git a/client.js b/client.js if (order < applied) return; applied = order; const nextActivity = JSON.stringify(value.activity ?? {}); -@@ -13358,6 +13358,11 @@ function apply(ctx) { +@@ -13358,6 +13358,11 @@ activityKey = nextActivity; activity.set(value.activity ?? {}); } @@ -20703,7 +18489,7 @@ diff --git a/client.js b/client.js const nextWork = JSON.stringify(value.work ?? null); if (nextWork !== workKey) { workKey = nextWork; -@@ -13368,10 +13368,15 @@ function apply(ctx) { +@@ -13368,10 +13368,15 @@ const live2 = {}; if (value.connectors !== void 0 && JSON.stringify(value.connectors) !== JSON.stringify(current.connectors ?? [])) live2.connectors = value.connectors; if (value.capabilityStatuses !== void 0 && JSON.stringify(value.capabilityStatuses) !== JSON.stringify(current.capabilityStatuses ?? {})) live2.capabilityStatuses = value.capabilityStatuses; @@ -20719,7 +18505,7 @@ diff --git a/client.js b/client.js try { const ids = (value.agents ?? []).flatMap((agent) => agent.sessions ?? []); localStorage.setItem(STORAGE.agentSessions, JSON.stringify(ids)); -@@ -13488,20 +13488,12 @@ function apply(ctx) { +@@ -13488,20 +13488,12 @@ const style = installStyles("", "mentions"); let last = ""; const render = () => { @@ -20742,7 +18528,7 @@ diff --git a/client.js b/client.js return () => { offRoster(); offRegistry(); -@@ -13520,7 +13520,7 @@ function apply(ctx) { +@@ -13520,7 +13520,7 @@ const accent = isHex(prefs.accent) ? prefs.accent : void 0; const motion = prefs.motion === "quiet" || prefs.motion === "lively" ? prefs.motion : "normal"; document.body.dataset.btMotion = motion; @@ -20751,7 +18537,7 @@ diff --git a/client.js b/client.js if (next === key) return; key = next; style.textContent = themeCss(themes[id], accent); -@@ -13536,7 +13536,7 @@ function apply(ctx) { +@@ -13536,7 +13536,7 @@ }; render(); const offRoster = roster.subscribe(render); @@ -20760,7 +18546,7 @@ diff --git a/client.js b/client.js return () => { offRoster(); offRegistry(); -@@ -13588,6 +13588,7 @@ function apply(ctx) { +@@ -13588,6 +13588,7 @@ // or the click appears to do nothing. isAgentSession: (id) => isAgentSessionId(id), openSession: (id) => { @@ -20768,7 +18554,7 @@ diff --git a/client.js b/client.js if (isAgentSessionId(id)) return actions.openAgentSession(id); ui2.set((state) => state.newChat || state.exchange || state.menu || state.palette ? { ...state, newChat: null, exchange: null, menu: null, palette: null } : state); setPlain(false); -@@ -13653,12 +13653,12 @@ function apply(ctx) { +@@ -13653,12 +13653,12 @@ }, // `last` is the Bot conversation the view held before it cleared: when that part // was archived for a fresh one, the view goes on in the fresh one. @@ -20784,7 +18570,7 @@ diff --git a/client.js b/client.js expected = target; ctx.uiWorkspace.openSession(target); }, -@@ -13708,10 +13708,15 @@ function apply(ctx) { +@@ -13708,10 +13708,15 @@ }, // `id` opens one Bot (page 'bots') or one group chat (page 'groups'). openSettings: (page = "general", id = null) => { @@ -20802,7 +18588,7 @@ diff --git a/client.js b/client.js }, openMemory: (botId) => { ui2.set((state) => ({ ...state, memory: botId, menu: null, palette: null })); -@@ -13719,6 +13719,19 @@ function apply(ctx) { +@@ -13719,6 +13719,19 @@ closeMemory: () => { ui2.set((state) => state.memory ? { ...state, memory: null } : state); }, @@ -20822,7 +18608,7 @@ diff --git a/client.js b/client.js openConnectors: () => { ui2.set((state) => ({ ...state, settings: { page: "connectors", id: null }, menu: null, palette: null, newChat: null })); }, -@@ -13728,7 +13728,7 @@ function apply(ctx) { +@@ -13728,7 +13728,7 @@ if (mode !== "bot" && mode !== "agent") return; if (surface.getSnapshot().mode === mode) return; if (mode === "agent") { @@ -20831,7 +18617,7 @@ diff --git a/client.js b/client.js } writeMode(mode); surface.set((state) => ({ ...state, mode })); -@@ -13757,6 +13757,12 @@ function apply(ctx) { +@@ -13757,6 +13757,12 @@ const seen = seenAt[seenKey(id)] ?? (made?.createdBy && Number.isFinite(made.createdAt) ? made.createdAt : null); ui2.set((state) => ({ ...state, visits: { ...state.visits, [id]: { seen, entered: Date.now() } } })); }, @@ -20844,9 +18630,21 @@ diff --git a/client.js b/client.js endVisit: (id) => { leaveVisits([id]); ui2.set((state) => { -@@ -13810,8 +13810,14 @@ function apply(ctx) { +@@ -13780,7 +13780,7 @@ + } + return { ...state, reactions }; + }), +- quote: (sessionId, text) => ui2.set((state) => ({ ...state, quote: { sessionId, text, token: Date.now() } })), ++ quote: (sessionId, text2) => ui2.set((state) => ({ ...state, quote: { sessionId, text: text2, token: Date.now() } })), + clearQuote: (token) => ui2.set((state) => state.quote?.token === token ? { ...state, quote: null } : state), + clearUnread: (id) => setUnread(id, false), + setFlags: (id, flags) => after(call("set-flags", { id, ...flags })), +@@ -13808,10 +13808,16 @@ + setPrefs: (prefs) => after(call("set-prefs", prefs)), + setMain: (id, main = true, options = {}) => after(call("set-main", { id, main, ...options })), updateRoom: (id, patch) => after(call("update-room", { id, ...patch })), - send: (sessionId, text) => after(call("send", { sessionId: partFor(sessionId), text })), +- send: (sessionId, text) => after(call("send", { sessionId: partFor(sessionId), text })), ++ send: (sessionId, text2) => after(call("send", { sessionId: partFor(sessionId), text: text2 })), answerQuestion: (sessionId, questionId, answer) => after(call("answer-question", { sessionId: partFor(sessionId), questionId, answer })), + answerTaskInput: (id, questionId, answer) => after(call("task-answer-input", { id, questionId, answer })), + // The Done list is the acceptance surface (#401): a result is accepted, or sent back with a note @@ -20859,7 +18657,7 @@ diff --git a/client.js b/client.js stopMemberWork: (roomId, botId) => after(call("stop-member-work", { roomId, botId })), // Values go out in these requests and never come back in a response. secretSet: (payload) => after(call("secret-set", payload)), -@@ -13819,15 +13819,31 @@ function apply(ctx) { +@@ -13819,15 +13819,31 @@ secretCancel: (requestId) => after(call("secret-cancel", { requestId })), secretUpdate: (name, scope) => after(call("secret-update", { name, scope })), secretDelete: (name) => after(call("secret-delete", { name })), @@ -20874,7 +18672,7 @@ diff --git a/client.js b/client.js + connectorBlock: (id, openId) => after(call("connector-block", { id, openId })), connectorDisconnect: (id) => after(call("connector-disconnect", { id })), connectorRetry: (id) => after(call("connector-retry", { id })), -+ connectorSend: (id, text) => after(call("connector-send", { id, text })), ++ connectorSend: (id, text2) => after(call("connector-send", { id, text: text2 })), saveCapability: (capability) => after(call("capability-save", capability)), deleteCapability: (name) => after(call("capability-delete", { name })), approveCapability: (name) => after(call("capability-approve", { name })), @@ -20892,16 +18690,25 @@ diff --git a/client.js b/client.js scheduleDelete: (sessionId, id) => call("schedule-delete", { sessionId, id }), exchange: (a, b) => call("exchange", { a, b }).catch(() => []), roomProgress: (roomId) => call("room-progress", { roomId }).catch(() => []), -@@ -13846,6 +13846,8 @@ function apply(ctx) { +@@ -13836,7 +13836,7 @@ + usage: () => call("usage", { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }), + // `summarize` starts a summary when the entries changed; `regenerate` always does. + memoryView: (botId, flags = {}) => call("memory-view", { botId, ...flags }), +- memoryAsk: (botId, text) => call("memory-ask", { botId, text }), ++ memoryAsk: (botId, text2) => call("memory-ask", { botId, text: text2 }), + memoryForget: (botId, entry) => call("memory-forget", { botId, scope: entry.scope, topic: entry.topic, text: entry.text }), + diagnostics: (payload) => call("diagnostics", payload), + diagnosticsLog: () => call("diagnostics-log"), +@@ -13846,6 +13846,8 @@ noteClientError(where, error); ui2.set((state) => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } })); }, + // A short confirmation (e.g. "Invite link copied") in the same toast, without the error styling. -+ notify: (text) => ui2.set((state) => ({ ...state, toast: { text: String(text), tone: "info", token: Date.now() } })), ++ notify: (text2) => ui2.set((state) => ({ ...state, toast: { text: String(text2), tone: "info", token: Date.now() } })), dismissToast: (token) => ui2.set((state) => state.toast && (token === void 0 || state.toast.token === token) ? { ...state, toast: null } : state) }; if (browsers) { -@@ -13890,6 +13890,7 @@ function apply(ctx) { +@@ -13890,6 +13890,7 @@ actions.closePanel = () => { ui2.set((state) => state.details ? { ...state, details: null, view: "details", renaming: false, tab: null } : state); }; @@ -20909,7 +18716,7 @@ diff --git a/client.js b/client.js actions.signInEntry = () => ctx.slots.entries?.("settings.models.sign-in")?.[0] ?? null; actions.canSignIn = () => Boolean(actions.signInEntry()); actions.signIn = () => ui2.set((state) => ({ ...state, signIn: (state.signIn ?? 0) + 1 })); -@@ -13922,6 +13922,9 @@ function apply(ctx) { +@@ -13922,6 +13922,9 @@ wakeUpdate(); }; actions.repoStars = () => call("repo-stars"); @@ -20919,7 +18726,7 @@ diff --git a/client.js b/client.js actions.noteTurnTime = (sessionId, turn, key, time) => { if (sessionId === void 0 || !Number.isFinite(turn) || !Number.isFinite(time)) return; turnClock.set((state) => { -@@ -13964,7 +13964,13 @@ function apply(ctx) { +@@ -13964,7 +13964,13 @@ }); const browserPages = browsers?.pages ?? createSource({}); const browserTabs = browsers?.tabs ?? createSource({}); @@ -20934,7 +18741,7 @@ diff --git a/client.js b/client.js ctx.effect(() => noticeApprovals({ work, roster, -@@ -13989,7 +13989,17 @@ function apply(ctx) { +@@ -13989,7 +13989,17 @@ } }; }; const shadow = (name, options, component) => ctx.slots.inject(name, () => ctx.slots.register({ name, ...options }, component)); @@ -20952,7 +18759,7 @@ diff --git a/client.js b/client.js const installAtMenu = () => { const fiber = ctx.inject(["inputTriggers"], (scope) => { scope.effect(() => { -@@ -14001,7 +14001,12 @@ function apply(ctx) { +@@ -14001,7 +14001,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 }; @@ -20965,7 +18772,7 @@ diff --git a/client.js b/client.js atMenu = null; for (const off of offs) off(); }; -@@ -14010,6 +14010,14 @@ function apply(ctx) { +@@ -14010,6 +14010,14 @@ return () => fiber.dispose(); }; actions.canReferenceChat = () => atMenu !== null; @@ -20980,7 +18787,7 @@ diff --git a/client.js b/client.js actions.referenceChat = (sessionId, span) => { const controller = atMenu?.controllerOf(sessionId); if (controller === void 0 || span === void 0) return; -@@ -14019,33 +14019,46 @@ function apply(ctx) { +@@ -14019,33 +14019,48 @@ installCompiledStyles(SHELL_CSS, "shell"), installCompiledStyles(SIDEBAR_CSS, "sidebar"), installCompiledStyles(SECRET_CSS, "secrets"), @@ -20997,6 +18804,7 @@ diff --git a/client.js b/client.js installCompiledStyles(USER_MESSAGE_CSS, "user-messages"), + installCompiledStyles(MOBILE_CSS, "mobile"), + installCompiledStyles(PANE_MIN_WIDTH_CSS, "panes"), ++ installCompiledStyles(PROJECT_BOARD_CSS, "project-board"), + typeof document !== "undefined" ? watchShellLevel(document) : null, browsers ? installCompiledStyles(BROWSER_CSS, "browser") : null, installTheme(), @@ -21016,7 +18824,8 @@ diff --git a/client.js b/client.js shadow("shell.overlay", { id: "bot-signin", order: 55, inject: face }, DeepSeekSignIn), - shadow("shell.overlay", { id: "bot-toast", order: 60, inject: face }, ErrorToast) + shadow("shell.overlay", { id: "bot-toast", order: 60, inject: face }, ErrorToast), -+ shadow("shell.overlay", { id: "flybot-approvals", order: 58, inject: face }, FlyBotApprovals) ++ shadow("shell.overlay", { id: "flybot-approvals", order: 58, inject: face }, FlyBotApprovals), ++ shadow("shell.overlay", { id: "project-board", order: 57 }, ProjectBoardOverlay) ]; const installConversation = () => [ installCompiledStyles(CONVERSATION_CSS, "conversation"), @@ -21031,7 +18840,7 @@ diff --git a/client.js b/client.js shadow("conversation.chat.node", { key: "assistant-step", priority: -10, locale: "chat", inject: assistantFace }, AssistantCell), shadow("conversation.chat.node", { key: "turn-trigger", priority: -10, locale: "chat", inject: face }, TriggerCell), shadow("conversation.chat.node", { key: "tool-call", priority: -10, locale: "chat", inject: face }, ToolCell), -@@ -14057,15 +14057,24 @@ function apply(ctx) { +@@ -14057,15 +14057,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), @@ -21059,7 +18868,7 @@ diff --git a/client.js b/client.js shadow("conversation.input.right", { id: "bot-voice", order: 100, inject: face }, ComposerVoice), shadow("conversation.composer", { priority: -10, -@@ -14074,8 +14074,17 @@ function apply(ctx) { +@@ -14074,9 +14074,18 @@ shadow("conversation.input.dock", { id: "bot-members", order: -120, inject: face }, MemberDock), shadow("conversation.input.dock", { id: "bot-model", order: -110, inject: face }, ModelChoiceDock), shadow("conversation.input.dock", { id: "bot-question", order: -100, inject: face }, QuestionDock), @@ -21067,14 +18876,15 @@ diff --git a/client.js b/client.js + shadow("conversation.input.dock", { id: "bot-secret", order: -90, inject: face }, SecretDock), + shadow("conversation.input.dock", { id: "flybot-session", order: -80, inject: face }, FlyBotSessionDock) ]; -+ ctx.effect(() => { + 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_react48.createElement)(TurnFailures, { ...props, work }) }); ++ const off = registry().register("flybot.session.dock", { id: "flybot-turn-failures", order: 20, component: (props) => (0, import_react49.createElement)(TurnFailures, { ...props, work }) }); + return () => { + off(); + style.remove(); + }; + }, "ds-bot: dead turn lines"); - ctx.effect(() => { ++ ctx.effect(() => { const style = installStyles(RETURN_CSS, "return"); return () => style.remove(); + }, "ds-bot: return styles"); diff --git a/patches/ds-bot/files/src/client/project-board.js b/patches/ds-bot/files/src/client/project-board.js new file mode 100644 index 000000000..192f6fc19 --- /dev/null +++ b/patches/ds-bot/files/src/client/project-board.js @@ -0,0 +1,117 @@ +// The Project board view (#1710): one Project, the rows its read model already aggregated +// (host `project-board`), grouped by where the work lives. The rows are the tasks themselves — +// `row.id` is the underlying task id — so opening one opens that task and nothing is copied here. +import { createElement as h, useCallback, useEffect, useState } from 'react' +import { call } from './api.js' +import { t } from './i18n.js' + +export const PROJECT_BOARD_CSS = ` +.bt-project-board{padding:16px 18px} +.bt-project-group{margin:0 0 14px} +.bt-project-group h4{margin:0 0 6px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;opacity:.7} +.bt-project-row{display:flex;gap:10px;align-items:center;padding:8px 10px;border-radius:8px;background:var(--bt-card,rgba(127,127,127,.08));margin:0 0 6px;cursor:default} +.bt-project-row[data-column="done"]{opacity:.7} +.bt-project-row .bt-project-ref{margin-left:auto;font-size:11px;opacity:.6} +.bt-project-empty{padding:22px 4px;opacity:.7} +.bt-project-actions{display:flex;gap:8px;margin:0 0 14px} +` + +// Sources in the order the board reads them; a source with no rows is left out so three empty +// groups never crowd out the one that has work. +export const PROJECT_SOURCES = ['bot', 'group', 'workflow'] +export const SOURCE_LABELS = { bot: 'Bots', group: 'Groups', workflow: 'Workflows' } + +const text = value => (typeof value === 'string' ? value.trim() : '') + +// Pure: no fetch, no React, no store. `rows` is the host's read model, `canManage` comes from +// `project-read` and only decides what the caller may *ask* for — the host refuses regardless. +export function projectBoardView({ project, rows, canManage } = {}) { + const list = Array.isArray(rows) ? rows.filter(row => row !== null && typeof row === 'object' && text(row.id) !== '') : [] + const groups = PROJECT_SOURCES + .map(source => ({ + source, + label: SOURCE_LABELS[source], + rows: list + .filter(row => row.source === source) + .map(row => ({ + id: text(row.id), + key: `task:${text(row.id)}`, + column: text(row.column) || 'not-started', + assignee: text(row.assignee) || null, + runId: text(row.runId) || null, + nodeId: text(row.nodeId) || null, + })), + })) + .filter(group => group.rows.length > 0) + return { + projectId: text(project?.id) || null, + title: text(project?.name) || 'Project', + canManage: canManage === true, + groups, + count: list.length, + empty: list.length === 0, + emptyText: 'Nothing under this Project yet.', + } +} + +function ProjectBoardRows({ view, onOpen }) { + // An empty Project is a state, not an error: the view says so and renders no groups. + if (view.empty) return h('div', { className: 'bt-project-empty' }, t(view.emptyText)) + return view.groups.map(group => h('section', { key: group.source, className: 'bt-project-group', 'data-source': group.source }, + h('h4', null, t(group.label)), + group.rows.map(row => h('div', { + key: row.key, + className: 'bt-project-row', + 'data-task-id': row.id, + 'data-column': row.column, + role: 'button', + tabIndex: 0, + onClick: () => onOpen?.(row.id), + onKeyDown: event => { if (event.key === 'Enter' || event.key === ' ') onOpen?.(row.id) }, + }, + h('span', { className: 'bt-project-column' }, t(row.column)), + h('span', { className: 'bt-project-title' }, row.assignee ? `@${row.assignee}` : '—'), + row.runId === null ? null : h('span', { className: 'bt-project-ref' }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`))))) +} + +export function ProjectBoardPane({ project, rows, canManage, onOpen }) { + const view = projectBoardView({ project, rows, canManage }) + return h('div', { className: 'bt-project-board', 'data-can-manage': view.canManage ? 'true' : 'false' }, + h('h3', null, view.title), + // Management affordances are drawn only for the owner; the refusal itself is the host's. + view.canManage ? h('div', { className: 'bt-project-actions' }, h('button', { type: 'button' }, t('Project settings')), h('button', { type: 'button' }, t('Members'))) : null, + h(ProjectBoardRows, { view, onOpen })) +} + +// The pane entry point (#1710): `#project=` opens one Project's board, `#project` the first. +// Reading is a POST through the same host channel every other view uses. +export function ProjectBoardOverlay() { + const [state, setState] = useState(null) + const close = useCallback(() => { + if (typeof window !== 'undefined') window.location.hash = '' + setState(null) + }, []) + useEffect(() => { + if (typeof window === 'undefined') return + if (!window.location.hash.startsWith('#project')) return + const wanted = window.location.hash.slice('#project'.length).replace(/^=/, '') + let live = true + void (async () => { + try { + const listing = await call('projects') + const projects = listing?.projects ?? [] + const project = wanted === '' ? projects[0] : projects.find(item => item.id === wanted) + if (project === undefined) { if (live) setState({ project: null, rows: [], canManage: false }); return } + const [read, board] = await Promise.all([call('project-read', { id: project.id }), call('project-board', { id: project.id })]) + if (live) setState({ project: read?.project ?? project, rows: board?.tasks ?? [], canManage: read?.canManage === true }) + } catch { if (live) setState({ project: null, rows: [], canManage: false }) } + })() + return () => { live = false } + }, []) + if (state === null) return null + return h('div', { className: 'bt-overlay bt-project-overlay', role: 'dialog', 'aria-label': t('Project board') }, + h('button', { type: 'button', className: 'bt-project-close', onClick: close }, t('Close')), + state.project === null + ? h('div', { className: 'bt-project-empty' }, t('No Project here.')) + : h(ProjectBoardPane, { project: state.project, rows: state.rows, canManage: state.canManage, onOpen: id => { window.location.hash = `#task=${id}` } })) +} diff --git a/patches/ds-bot/files/test/project-board-view.test.js b/patches/ds-bot/files/test/project-board-view.test.js new file mode 100644 index 000000000..f42af09b2 --- /dev/null +++ b/patches/ds-bot/files/test/project-board-view.test.js @@ -0,0 +1,107 @@ +// The Project board view (#1710): one view over the read model Stage 2.2 shipped. It groups the +// rows by where the work lives, carries the run/node a workflow row came from, and keeps the row +// id equal to the underlying task id, so opening a row opens that task and no row is a copy. +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/project-board.js', import.meta.url))], + bundle: true, format: 'cjs', platform: 'node', write: false, + external: ['react', 'react-dom'], +}) +const module = { exports: {} } +runInNewContext(bundle.outputFiles[0].text, { + module, exports: module.exports, setTimeout, clearTimeout, setInterval, clearInterval, Date, Promise, console, + require: (name) => { + const platform = { + react: { + createElement: (type, props, ...children) => ({ type, props: props ?? {}, children: children.flat(Infinity) }), + useCallback: fn => fn, + useEffect: () => {}, + useState: initial => [initial, () => {}], + }, + } + if (!(name in platform)) throw new Error(`no platform module ${name}`) + return platform[name] + }, +}) +const { ProjectBoardPane, projectBoardView } = module.exports + +const rows = [ + { id: 'task-b1', source: 'bot', column: 'in-progress', assignee: 'Dev4', updatedAt: 3 }, + { id: 'task-g1', source: 'group', column: 'done', assignee: 'Chief', updatedAt: 2 }, + { id: 'task-w1', source: 'workflow', column: 'not-started', assignee: null, runId: 'run-7', nodeId: 'build', updatedAt: 1 }, +] + +const all = (node, out = []) => { + if (Array.isArray(node)) { for (const item of node) all(item, out); return out } + if (node === null || typeof node !== 'object') return out + // The fake createElement does not render components; the test asks them for their tree. + if (typeof node.type === 'function') return all(node.type(node.props), out) + out.push(node) + all(node.children, out) + return out +} +// Arrays minted inside the vm have another realm's prototype, so compare plain copies. +const plain = value => JSON.parse(JSON.stringify(value)) +const textOf = node => (typeof node === 'string' ? node : node && typeof node === 'object' ? (node.children ?? []).map(textOf).join('') : '') + +test('the board groups rows from all three sources and keeps each task id', () => { + const view = plain(projectBoardView({ project: { id: 'project-1', name: 'FlyBot' }, rows, canManage: true })) + assert.equal(view.projectId, 'project-1') + assert.equal(view.title, 'FlyBot') + assert.equal(view.canManage, true) + assert.equal(view.empty, false) + assert.equal(view.count, 3) + assert.deepEqual(view.groups.map(group => group.source), ['bot', 'group', 'workflow']) + assert.deepEqual(view.groups.map(group => group.rows.length), [1, 1, 1]) + const ids = view.groups.flatMap(group => group.rows.map(row => row.id)) + assert.deepEqual(ids, ['task-b1', 'task-g1', 'task-w1'], 'the row id is the task id') + assert.deepEqual(view.groups.flatMap(group => group.rows.map(row => row.key)), ['task:task-b1', 'task:task-g1', 'task:task-w1']) +}) + +test('a workflow row carries its run and node; other rows carry none', () => { + const view = plain(projectBoardView({ project: { id: 'p', name: 'P' }, rows, canManage: false })) + const [botRow] = view.groups[0].rows + const workflowRow = view.groups[2].rows[0] + assert.equal(botRow.runId, null) + assert.equal(botRow.nodeId, null) + assert.equal(workflowRow.runId, 'run-7') + assert.equal(workflowRow.nodeId, 'build') + assert.equal(workflowRow.assignee, null) + assert.equal(botRow.column, 'in-progress') +}) + +test('a Project with nothing under it is an empty state, not an error', () => { + const view = plain(projectBoardView({ project: { id: 'p', name: 'P' }, rows: [], canManage: false })) + assert.equal(view.empty, true) + assert.deepEqual(view.groups, []) + assert.equal(view.count, 0) + const missing = plain(projectBoardView({})) + assert.equal(missing.empty, true) + assert.equal(missing.projectId, null) + assert.equal(missing.canManage, false, 'no canManage answer is read as read-only') +}) + +test('the pane draws the grouped rows and only the owner sees the management controls', () => { + const owner = all(ProjectBoardPane({ project: { id: 'p', name: 'FlyBot' }, rows, canManage: true })) + const member = all(ProjectBoardPane({ project: { id: 'p', name: 'FlyBot' }, rows, canManage: false })) + assert.match(textOf(owner[0]), /FlyBot/) + const ownerRows = owner.filter(node => node.props?.['data-task-id'] !== undefined) + assert.deepEqual(ownerRows.map(node => node.props['data-task-id']), ['task-b1', 'task-g1', 'task-w1']) + const workflow = ownerRows[2] + assert.match(textOf(workflow), /run-7 · build/) + assert.equal(owner.some(node => textOf(node) === 'Project settings'), true) + const memberRows = member.filter(node => node.props?.['data-task-id'] !== undefined) + assert.deepEqual(memberRows.map(node => node.props['data-task-id']), ['task-b1', 'task-g1', 'task-w1'], 'a member still reads the board') + assert.equal(member.some(node => textOf(node) === 'Project settings'), false) +}) + +test('an empty Project renders the empty text and no row', () => { + const nodes = all(ProjectBoardPane({ project: { id: 'p', name: 'P' }, rows: [], canManage: false })) + assert.equal(nodes.filter(node => node.props?.['data-task-id'] !== undefined).length, 0) + assert.match(nodes.map(textOf).join(' '), /Nothing under this Project yet/) +}) diff --git a/patches/ds-bot/files/test/projects.test.js b/patches/ds-bot/files/test/projects.test.js index 4c6e29242..fd3457c9f 100644 --- a/patches/ds-bot/files/test/projects.test.js +++ b/patches/ds-bot/files/test/projects.test.js @@ -5,15 +5,31 @@ import assert from 'node:assert/strict' import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { createHash } from 'node:crypto' import { test } from 'node:test' import { apply } from '../index.js' import { fakeContext } from './fake-ctx.js' -const seed = () => ({ +const hashToken = token => createHash('sha256').update(token).digest('hex') + +// #1710: a second member and two signed-in sessions, so an owner and a non-owner caller both +// exist without a second process. +const seed = (sessions = true) => ({ version: 2, revision: 1, mainBotIds: ['a'], - members: { 'owner-1': { id: 'owner-1', createdAt: 1, designatedChiefId: 'a' } }, + members: { + 'owner-1': { id: 'owner-1', createdAt: 1, designatedChiefId: 'a' }, + 'owner-2': { id: 'owner-2', createdAt: 2, designatedChiefId: null }, + }, + ...(sessions + ? { + authSessions: { + 's-a': { id: 's-a', memberId: 'owner-1', tokenHash: hashToken('tok-a'), createdAt: 1 }, + 's-b': { id: 's-b', memberId: 'owner-2', tokenHash: hashToken('tok-b'), createdAt: 2 }, + }, + } + : {}), bots: { a: { id: 'a', name: 'Chief', role: 'Boss', sessionId: 'a', createdAt: 1, ownerId: 'owner-1' }, b: { id: 'b', name: 'Writer', role: 'Write', sessionId: 'b', createdAt: 2, ownerId: 'owner-1' }, @@ -22,8 +38,8 @@ const seed = () => ({ agents: {}, exchanges: [], exchangeTurns: {}, questions: {}, prefs: {}, grants: {}, }) -async function start(home = mkdtempSync(join(tmpdir(), 'ds-bot-projects-'))) { - if (!existsSync(join(home, 'state.json'))) writeFileSync(join(home, 'state.json'), JSON.stringify(seed())) +async function start(home = mkdtempSync(join(tmpdir(), 'ds-bot-projects-')), { sessions = true } = {}) { + if (!existsSync(join(home, 'state.json'))) writeFileSync(join(home, 'state.json'), JSON.stringify(seed(sessions))) const ctx = fakeContext() const agents = new Map() const agentOf = (sessionId) => { @@ -41,7 +57,11 @@ async function start(home = mkdtempSync(join(tmpdir(), 'ds-bot-projects-'))) { const register = ctx.connection.fetch.register ctx.connection.fetch.register = (options) => { route = options; return register(options) } apply(ctx, { home }) - const call = async (endpoint, payload) => (await route.fetch(new Request('http://localhost/api/bot', { method: 'POST', body: JSON.stringify({ endpoint, payload }) }))).json() + const call = async (endpoint, payload, token = 'tok-a') => (await route.fetch(new Request('http://localhost/api/bot', { + method: 'POST', + headers: token ? { 'x-flybot-member-session': token } : {}, + body: JSON.stringify({ endpoint, payload }), + }))).json() const stop = async ({ keepHome = false } = {}) => { await ctx.dispose() if (!keepHome) rmSync(home, { recursive: true, force: true, maxRetries: 5 }) @@ -167,7 +187,8 @@ test('an id that is not an own key resolves as absent and never writes Object.pr '({}).projectId': ({}).projectId ?? null, 'state.json bots.b has own projectId': Object.hasOwn(saved.bots.b, 'projectId'), }) - assert.deepEqual(protoRead.value, { project: null }, observed) + // #1710: project-read also answers canManage; an absent Project is never manageable. + assert.deepEqual(protoRead.value, { project: null, canManage: false }, observed) assert.equal(protoBind.ok, false, observed) assert.equal(protoBind.error?.code, 'bot/not-found', observed) assert.deepEqual(protoOf.value, { project: null }, observed) @@ -209,3 +230,42 @@ test('a run binds under an own key, not through the prototype chain (#1704)', as assert.equal(value(of).project?.id, project.id, observed) } finally { await stop() } }) + +// #1710: the owner of a Project may manage it, a member reads the same board read-only, and the +// refusal comes from the host action — not from a control the page happens to hide. +test('the caller becomes the owner, and only the owner may change the Project', async () => { + const { call, stop } = await start() + try { + const project = created(await call('project-create', { name: 'Owned', repository: 'acme/owned', ownerUserId: 'owner-2' }, 'tok-a')) + assert.equal(project.ownerUserId, 'owner-1', 'the caller is stamped as the owner, not the body') + + assert.equal(value(await call('project-read', { id: project.id }, 'tok-a')).canManage, true) + assert.equal(value(await call('project-read', { id: project.id }, 'tok-b')).canManage, false) + + const refused = await call('project-bind', { kind: 'bot', id: 'a', projectId: project.id }, 'tok-b') + assert.equal(refused.ok, false) + assert.equal(refused.error.code, 'bot/forbidden') + assert.equal(value(await call('project-of', { kind: 'bot', id: 'a' })).project, null, 'the refusal wrote nothing') + + assert.equal(created(await call('project-bind', { kind: 'bot', id: 'a', projectId: project.id }, 'tok-a')).id, project.id) + } finally { await stop() } +}) + +test('an install with no signed-in session falls back to its first member as the owner', async () => { + const { call, stop } = await start(undefined, { sessions: false }) + try { + const project = created(await call('project-create', { name: 'Solo', repository: 'acme/solo' })) + assert.equal(project.ownerUserId, 'owner-1', 'the single-member install stamps its member') + assert.equal(value(await call('project-read', { id: project.id })).canManage, true) + } finally { await stop() } +}) + +test('a signed-in member may read the Project board even though it may not manage it', async () => { + const { call, stop } = await start() + try { + const project = created(await call('project-create', { name: 'Board', repository: 'acme/board' }, 'tok-a')) + const board = await call('project-board', { id: project.id }, 'tok-b') + assert.equal(board.ok, true, JSON.stringify(board), 'project-board is a known endpoint once someone signed in') + assert.deepEqual(value(board).tasks, []) + } finally { await stop() } +})