ds-bot 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -3,9 +3,11 @@ window.__ModuleLoader__.load({
3
3
  id: 'ds-bot',
4
4
  factory: (require) => {
5
5
  const module = { exports: {} }
6
+ var __create = Object.create;
6
7
  var __defProp = Object.defineProperty;
7
8
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
9
  var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __getProtoOf = Object.getPrototypeOf;
9
11
  var __hasOwnProp = Object.prototype.hasOwnProperty;
10
12
  var __export = (target, all) => {
11
13
  for (var name in all)
@@ -19,6 +21,14 @@ var __copyProps = (to, from, except, desc) => {
19
21
  }
20
22
  return to;
21
23
  };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
22
32
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
23
33
 
24
34
  // src/client/index.js
@@ -57,6 +67,9 @@ var ZH = {
57
67
  Close: "关闭",
58
68
  Copy: "复制",
59
69
  Copied: "已复制",
70
+ Content: "内容",
71
+ "Referenced {labels}": "引用了 {labels}",
72
+ ", ": "、",
60
73
  You: "你",
61
74
  you: "你",
62
75
  Bot: "Bot",
@@ -417,6 +430,7 @@ var ZH = {
417
430
  "Replies from": "群聊回复:",
418
431
  "Reply from": "收到回复:",
419
432
  "Message from": "收到消息:",
433
+ "Stopped by": "叫停者:",
420
434
  "{count} messages with": "{count} 条消息,来自",
421
435
  "Routine started": "例行任务开始",
422
436
  Update: "更新",
@@ -443,6 +457,8 @@ var ZH = {
443
457
  "Checking the keys": "查看密钥",
444
458
  "Asking {name}": "询问 {name}",
445
459
  "Asking another Bot": "询问其他 Bot",
460
+ "Stopping {name}": "叫停 {name}",
461
+ "Stopping another Bot": "叫停其他 Bot",
446
462
  "Creating {name}": "创建 {name}",
447
463
  "Updating {name}": "更新 {name}",
448
464
  "Checking the team": "查看团队",
@@ -451,8 +467,19 @@ var ZH = {
451
467
  "{name} looked up its own chat": "{name} 查看了自己的对话",
452
468
  "{name} looked up its own chat for “{text}”": "{name} 在自己的对话里查找「{text}」",
453
469
  "{name} is typing…": "{name} 正在输入…",
470
+ "{name} didn't answer in time": "{name} 没有及时回应",
471
+ "{name} paused for your approval": "{name} 停下来等你批准",
472
+ "{name} is waiting for your approval": "{name} 等你批准",
473
+ "{first} and {second} are waiting for your approval": "{first}、{second} 等你批准",
474
+ "{first}, {second} and {count} more are waiting for your approval": "{first}、{second} 等 {total} 个等你批准",
475
+ "Waiting for your approval": "等你批准",
476
+ "in its own chat": "在自己的对话里",
477
+ Reject: "拒绝",
478
+ "Allow once": "允许一次",
454
479
  "Couldn't message {name}": "没能给 {name} 发消息",
455
480
  Messaged: "已发消息给",
481
+ "Couldn't stop {name}": "没能叫停 {name}",
482
+ Stopped: "已叫停",
456
483
  Created: "已创建",
457
484
  Updated: "已更新",
458
485
  "Created group": "已创建群聊",
@@ -683,6 +710,25 @@ var ZH = {
683
710
  "Compared with the day before": "与前一天相比",
684
711
  "Compared with the {count} days before": "与再往前 {count} 天相比",
685
712
  "{share}% of all usage from {span}": "占 {span} 全部用量的 {share}%",
713
+ // About
714
+ "About DS Bot": "关于 DS Bot",
715
+ "Version {version}": "版本 {version}",
716
+ "Star DS Bot on GitHub": "在 GitHub 上给 DS Bot 点 Star",
717
+ "{count} stars": "{count} 个 Star",
718
+ "If DS Bot helps you, a star on GitHub helps more people find it.": "如果 DS Bot 对你有用,欢迎在 GitHub 上点个 Star,让更多人看到它。",
719
+ Updates: "更新",
720
+ "Check for updates": "检查更新",
721
+ "Checking…": "正在检查…",
722
+ "Up to date": "已是最新版本",
723
+ "Checked {time}": "检查于 {time}",
724
+ "Could not check for updates.": "没能检查更新。",
725
+ "Update checks are off.": "已关闭更新检查。",
726
+ "Installed from a local folder; update it there.": "从本地文件夹安装,请在那里更新。",
727
+ "Updates are not offered for this install.": "这种安装方式不提供更新。",
728
+ Links: "链接",
729
+ "Source code": "源代码",
730
+ "Release notes": "发布说明",
731
+ Open: "打开",
686
732
  // Feedback and errors
687
733
  Feedback: "反馈",
688
734
  "Report a problem": "报告问题",
@@ -727,6 +773,8 @@ var ZH = {
727
773
  "Its group conversation could not open": "它的群聊会话打不开",
728
774
  // The composer's +
729
775
  "Add files": "添加文件",
776
+ "Reference a chat": "引用对话",
777
+ "Chat with {name}": "与 {name} 的对话",
730
778
  "Scheduled task": "定时任务",
731
779
  "Send to": "发给",
732
780
  "Scheduled task: {text}": "定时任务:{text}",
@@ -794,6 +842,7 @@ var HOST_ERRORS = [
794
842
  [/^Model (.+) is not available\. Available: (.*)$/, "模型 $1 不可用。可用的:$2"],
795
843
  [/^A Bot named (.+) already exists$/, "已经有叫 $1 的 Bot 了"],
796
844
  [/^Name (.*) is not available$/, "名字 $1 不可用"],
845
+ [/^"(.+)" calls everyone in a group, so it cannot be a Bot name$/, "「$1」在群里表示所有人,不能用作 Bot 名字"],
797
846
  [/^Another group chat is already called "(.+)"$/, "已经有叫「$1」的群聊了"],
798
847
  [/^A group chat holds at most (\d+) Bots$/, "一个群聊最多 $1 个 Bot"],
799
848
  [/^Reply mode must be one of (.+)$/, "回复模式只能是 $1"],
@@ -822,6 +871,7 @@ var HOST_ZH = {
822
871
  "Avatar images must be PNG, JPEG, WebP, or GIF": "头像图片必须是 PNG、JPEG、WebP 或 GIF",
823
872
  "Avatar image is too large": "头像图片太大了",
824
873
  "A Bot needs a name": "Bot 需要一个名字",
874
+ "A Bot name cannot contain @, ` or a line break": "Bot 名字里不能有 @、` 或换行",
825
875
  "Unknown Bot": "找不到这个 Bot",
826
876
  "Unknown conversation": "找不到这个对话",
827
877
  "A group chat needs at least two Bots": "群聊至少要两个 Bot",
@@ -940,12 +990,17 @@ function failureLabel(code, message) {
940
990
  var URL_LIMIT = 7e3;
941
991
  var WHAT_MAX = 2e3;
942
992
  var PASTE_NOTE = "DS Bot copied the diagnostics to the clipboard. Paste them here.";
993
+ var REST_NOTE = "The recent errors do not fit in the link. DS Bot copied the diagnostics to the clipboard: paste them below this line.";
943
994
  function issueUrl(base, { what = "", diagnostics }) {
944
995
  const text = what.trim().slice(0, WHAT_MAX);
945
996
  const title = text.split("\n")[0].slice(0, 80);
946
997
  const build = (body) => `${base}?${new URLSearchParams({ template: "bug.yml", ...title ? { title } : {}, ...text ? { what: text } : {}, diagnostics: body })}`;
947
998
  const full2 = build(diagnostics);
948
- return full2.length <= URL_LIMIT ? { url: full2, inline: true } : { url: build(PASTE_NOTE), inline: false };
999
+ if (full2.length <= URL_LIMIT) return { url: full2, inline: true };
1000
+ const facts = build(`${diagnostics.split("\n\n")[0]}
1001
+
1002
+ ${REST_NOTE}`);
1003
+ return { url: facts.length <= URL_LIMIT ? facts : build(PASTE_NOTE), inline: false };
949
1004
  }
950
1005
 
951
1006
  // src/client/api.js
@@ -1094,8 +1149,8 @@ function bodyOf(def, paint, flat = false) {
1094
1149
  return "";
1095
1150
  }
1096
1151
  }
1097
- var eyeMarkup = ([cx, cy, w, h29, tilt]) => `<g transform="translate(${cx} ${cy}) rotate(${tilt})"><rect class="bt-eye" x="${-w / 2}" y="${-h29 / 2}" width="${w}" height="${h29}" rx="${w / 2}"/></g>`;
1098
- var happyEyeMarkup = ([cx, cy, w, h29, tilt]) => `<g transform="translate(${cx} ${cy}) rotate(${tilt})"><path class="bt-eye-happy" opacity="0" d="M${-w * 0.62} ${h29 * 0.14}Q0 ${-h29 * 0.42} ${w * 0.62} ${h29 * 0.14}" fill="none" stroke="#000" stroke-width="${w * 0.5}" stroke-linecap="round"/></g>`;
1152
+ var eyeMarkup = ([cx, cy, w, h32, tilt]) => `<g transform="translate(${cx} ${cy}) rotate(${tilt})"><rect class="bt-eye" x="${-w / 2}" y="${-h32 / 2}" width="${w}" height="${h32}" rx="${w / 2}"/></g>`;
1153
+ var happyEyeMarkup = ([cx, cy, w, h32, tilt]) => `<g transform="translate(${cx} ${cy}) rotate(${tilt})"><path class="bt-eye-happy" opacity="0" d="M${-w * 0.62} ${h32 * 0.14}Q0 ${-h32 * 0.42} ${w * 0.62} ${h32 * 0.14}" fill="none" stroke="#000" stroke-width="${w * 0.5}" stroke-linecap="round"/></g>`;
1099
1154
  var maskBox = (size) => `maskUnits="userSpaceOnUse" x="${-size}" y="${-size}" width="${size * 3}" height="${size * 3}"`;
1100
1155
  var holes = (def, eyes) => `<rect x="${-def.size}" y="${-def.size}" width="${def.size * 3}" height="${def.size * 3}" fill="#fff"/>${eyes}`;
1101
1156
  var MASK_ID = "%MASK%";
@@ -1281,20 +1336,65 @@ function registerTheme(id, theme) {
1281
1336
  }
1282
1337
  var allThemes = () => ({ ...THEMES, ...customThemes });
1283
1338
 
1339
+ // src/shared/mentions.js
1340
+ var ALL_WORDS = ["all", "everyone", "所有人", "全体"];
1341
+ var NAME_GOES_ON = /^(?:[a-z0-9_\-/\\]|\.[a-z0-9_-])/i;
1342
+ var ADDRESS_BEFORE = /[a-z0-9_./]/i;
1343
+ var LITERAL = /```[\s\S]*?(?:```|$)|`[^`\n]*`|\[[^\]\n]*\]\([^)\s]*(?:\s+(?:"[^"\n]*"|'[^'\n]*'))?\)/g;
1344
+ var fitsAt = (text, at, word) => text.slice(at, at + word.length).toLowerCase() === word.toLowerCase() && !NAME_GOES_ON.test(text.slice(at + word.length));
1345
+ var overlaps = (spans, start, end) => spans.some((span) => start < span.end && span.start < end);
1346
+ function nameProblem(name) {
1347
+ const text = String(name ?? "").trim();
1348
+ if (text === "") return "A Bot needs a name";
1349
+ if (/[@`\r\n]/.test(text)) return "A Bot name cannot contain @, ` or a line break";
1350
+ if (ALL_WORDS.includes(text.toLowerCase())) return `"${text}" calls everyone in a group, so it cannot be a Bot name`;
1351
+ return void 0;
1352
+ }
1353
+ function referenceSpans(text, labels = []) {
1354
+ const spans = [];
1355
+ for (const label of [...labels].sort((a, b) => b.length - a.length)) {
1356
+ const mention = `@${label}`;
1357
+ for (let at = text.indexOf(mention); at !== -1; at = text.indexOf(mention, at + 1)) {
1358
+ const end = at + mention.length;
1359
+ if (at > 0 && ADDRESS_BEFORE.test(text[at - 1]) || NAME_GOES_ON.test(text.slice(end)) || overlaps(spans, at, end)) continue;
1360
+ spans.push({ start: at, end, label });
1361
+ break;
1362
+ }
1363
+ }
1364
+ return spans.sort((a, b) => a.start - b.start);
1365
+ }
1366
+ function mentionSpans(text, bots, { references = [] } = {}) {
1367
+ const value = String(text ?? "");
1368
+ const skip = [
1369
+ ...[...value.matchAll(LITERAL)].map((match) => ({ start: match.index, end: match.index + match[0].length })),
1370
+ ...referenceSpans(value, references)
1371
+ ];
1372
+ const byLength = bots.filter((bot) => bot?.name).sort((a, b) => b.name.length - a.name.length);
1373
+ const spans = [];
1374
+ for (let at = value.indexOf("@"); at !== -1; at = value.indexOf("@", at + 1)) {
1375
+ if (at > 0 && ADDRESS_BEFORE.test(value[at - 1]) || overlaps(skip, at, at + 1)) continue;
1376
+ const bot = byLength.find((candidate) => fitsAt(value, at + 1, candidate.name));
1377
+ const all = bot ? void 0 : ALL_WORDS.find((word) => fitsAt(value, at + 1, word));
1378
+ if (!bot && !all) continue;
1379
+ const end = at + 1 + (bot ? bot.name.length : all.length);
1380
+ spans.push(bot ? { start: at, end, bot } : { start: at, end, all: true });
1381
+ at = end - 1;
1382
+ }
1383
+ return spans;
1384
+ }
1385
+
1284
1386
  // src/client/text.js
1285
1387
  var MENTION_ORIGIN = "https://bots.local/";
1286
- var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1287
1388
  function linkMentions(text, bots, selfId) {
1288
- if (!text || bots.length === 0 || text.includes("```") || !text.includes("@")) return text;
1289
- const names = bots.filter((bot) => bot.name).sort((a, b) => b.name.length - a.name.length);
1290
- if (!names.some((bot) => bot.id !== selfId)) return text;
1291
- const byName = new Map(names.map((bot) => [bot.name.toLowerCase(), bot]));
1292
- const pattern = new RegExp(`(\\[[^\\]]*\\]\\([^)]*\\)|\`[^\`]*\`)|(?<![A-Za-z0-9_.])@(${names.map((bot) => escapeRegExp(bot.name)).join("|")})(?![A-Za-z0-9_-])`, "gi");
1293
- return text.replace(pattern, (match, skip, name) => {
1294
- if (skip) return skip;
1295
- const bot = byName.get(name.toLowerCase());
1296
- return bot && bot.id !== selfId ? `[${name}](${MENTION_ORIGIN}${bot.id})` : match;
1297
- });
1389
+ if (!text || !text.includes("@")) return text;
1390
+ let out = "";
1391
+ let cursor = 0;
1392
+ for (const span of mentionSpans(text, bots)) {
1393
+ if (!span.bot || span.bot.id === selfId) continue;
1394
+ out += `${text.slice(cursor, span.start)}[${text.slice(span.start + 1, span.end).replace(/[[\]\\]/g, "\\$&")}](${MENTION_ORIGIN}${span.bot.id})`;
1395
+ cursor = span.end;
1396
+ }
1397
+ return cursor === 0 ? text : out + text.slice(cursor);
1298
1398
  }
1299
1399
  function mentionTarget(event) {
1300
1400
  const anchor = event.target instanceof Element ? event.target.closest(`a[href^="${MENTION_ORIGIN}"]`) : null;
@@ -1386,6 +1486,30 @@ function toolArgs(root) {
1386
1486
  value: (key) => object()?.[key]
1387
1487
  };
1388
1488
  }
1489
+ var RELAY_ROLES = /* @__PURE__ */ new Set(["request", "reply", "stopped"]);
1490
+ var isRelayContext = (data) => {
1491
+ const source = data?.source;
1492
+ if (source === null || typeof source !== "object" || source.kind !== "bot" || source.handoff === true || source.deferred === true) return false;
1493
+ return RELAY_ROLES.has(source.role) && typeof source.senderSessionId === "string";
1494
+ };
1495
+ var stepShows = (step) => (step?.blocks ?? []).some((block) => {
1496
+ if (block === void 0 || block.kind === "tool-call") return false;
1497
+ return block.kind === "text" || block.kind === "reasoning" ? String(block.text ?? "").trim() !== "" : true;
1498
+ });
1499
+ var stepHasReasoning = (step) => (step?.blocks ?? []).some((block) => block?.kind === "reasoning" && String(block.text ?? "").trim() !== "");
1500
+ var relayPart = (data, groupPart) => groupPart === void 0 || (stepHasReasoning(data) ? groupPart === "reasoning" : groupPart === "response");
1501
+ function relaysBefore(contexts, steps, step) {
1502
+ const index = steps.findIndex((entry) => entry?.step === step?.step);
1503
+ if (index === -1) return [];
1504
+ let prev = -Infinity;
1505
+ for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
1506
+ if (stepShows(steps[cursor])) {
1507
+ prev = steps[cursor].time;
1508
+ break;
1509
+ }
1510
+ }
1511
+ return (contexts ?? []).filter((data) => isRelayContext(data) && typeof data.time === "number" && data.time >= prev && data.time < step.time);
1512
+ }
1389
1513
  function commRunOf(order, peers, turn, hiddenTurns = []) {
1390
1514
  const turns = [];
1391
1515
  for (const entry of order) {
@@ -1470,12 +1594,12 @@ function liveSprings(apply2) {
1470
1594
  const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3));
1471
1595
  last = now;
1472
1596
  const steps = Math.ceil(dt * 240);
1473
- const h29 = dt / steps;
1597
+ const h32 = dt / steps;
1474
1598
  let moving = false;
1475
1599
  for (const value of values.values()) {
1476
1600
  for (let index = 0; index < steps; index += 1) {
1477
- value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h29;
1478
- value.x += value.v * h29;
1601
+ value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h32;
1602
+ value.x += value.v * h32;
1479
1603
  }
1480
1604
  if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) {
1481
1605
  value.x = value.to;
@@ -1821,39 +1945,52 @@ button.bt-lead{border:0;background:none;padding:0;cursor:pointer}
1821
1945
  .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")}}
1822
1946
  .bt-x:hover{background:var(--bt-hover);color:var(--bt-ink)}
1823
1947
 
1824
- .bt-q{margin:0 auto 8px;width:100%;max-width:var(--dsh-chat-content-width,748px);display:flex;flex-direction:column;gap:10px;background:var(--bt-bubble);border-radius:16px;padding:12px;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}
1948
+ .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}
1949
+ 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)}
1825
1950
  @keyframes bt-q-in{from{transform:translateY(10px) scale(.97)}}
1826
1951
  .bt-q-settled{animation:none}
1827
1952
  .bt-q-head{display:flex;align-items:flex-start;gap:8px;min-width:0}
1828
1953
  .bt-q-text{flex:1 1 0;min-width:0;display:flex;flex-direction:column}
1829
- .bt-q-title{margin:0;font-size:14px;line-height:20px;font-weight:500;overflow-wrap:anywhere}
1954
+ .bt-q-title{margin:0;font-size:15px;line-height:22px;font-weight:600;overflow-wrap:anywhere}
1830
1955
  .bt-q-detail{font-size:13px;line-height:18px;color:var(--bt-ink-2)}
1831
1956
  .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")}}
1832
1957
  .bt-q-x svg{width:14px;height:14px}
1833
1958
  .bt-q-x:hover{background:var(--bt-hover);color:var(--bt-ink)}
1834
1959
  .bt-q-x:focus-visible,.bt-q-opt:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px}
1835
- .bt-q-list{display:flex;flex-direction:column;width:100%;min-width:0;box-sizing:border-box;background:var(--bt-main);border:.5px solid var(--bt-line-weak);border-radius:8px;overflow:hidden}
1836
- .bt-q-opt{display:flex;align-items:center;gap:8px;width:100%;min-width:0;box-sizing:border-box;margin:0;border:0;border-radius:0;background:transparent;padding:9px 8px;font:inherit;color:var(--bt-ink);text-align:start;cursor:pointer;transition:background-color .12s ease}
1837
- .bt-q-opt+.bt-q-opt{border-top:.5px solid var(--bt-line-weak)}
1838
- .bt-q-opt:first-child{border-top-left-radius:7.5px;border-top-right-radius:7.5px}
1839
- .bt-q-opt:last-child{border-bottom-left-radius:7.5px;border-bottom-right-radius:7.5px}
1960
+ .bt-q-list{display:flex;flex-direction:column;gap:1px;min-width:0;box-sizing:border-box;margin:0 -8px}
1961
+ .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}
1962
+ .bt-q:not(.bt-q-raised)>.bt-q-head:has(+.bt-q-list){padding-bottom:12px;border-bottom:1px solid var(--bt-q-rule)}
1963
+ .bt-q:not(.bt-q-raised)>.bt-q-head+.bt-q-list{margin-top:-6px}
1964
+ .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}
1965
+ 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}
1966
+ .bt-q-raised .bt-q-opt::before{display:none}
1840
1967
  button.bt-q-opt:hover{background:var(--bt-hover)}
1841
1968
  button.bt-q-opt:active{background:var(--bt-active)}
1842
1969
  .bt-q-opt[aria-pressed=true]{background:var(--bt-active)}
1843
- .bt-key{flex:none;box-sizing:border-box;min-width:18px;height:18px;padding:1px 4px;border-radius:4px;border:.5px solid color-mix(in srgb,var(--bt-ink) 5%,transparent);background:var(--bt-active);color:color-mix(in srgb,var(--bt-ink) 61%,transparent);font-size:11px;line-height:15px;text-align:center;font-variant-numeric:tabular-nums}
1844
- .bt-q-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column}
1845
- .bt-q-label{font-size:14px;line-height:20px;white-space:normal;overflow-wrap:anywhere}
1846
- .bt-q-desc{font-size:13px;line-height:18px;color:var(--bt-ink-2);overflow-wrap:anywhere}
1970
+ .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)}
1971
+ .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}
1972
+ .bt-q-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px;padding-top:2px}
1973
+ /* With a description the key spans the two text lines less 2px above and below. */
1974
+ .bt-q-opt:has(.bt-q-desc){--bt-key:34px}
1975
+ .bt-q-opt:has(.bt-q-desc) .bt-key{margin-top:2px;border-radius:10px;font-size:13px}
1976
+ .bt-q-opt:has(.bt-q-desc) .bt-q-body{padding-top:0}
1977
+ .bt-q-label{font-size:14px;line-height:20px;font-weight:500;white-space:normal;overflow-wrap:anywhere}
1978
+ .bt-q-desc{font-size:12px;line-height:17px;color:var(--bt-ink-2);overflow-wrap:anywhere}
1847
1979
  .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}
1848
1980
  .bt-q-settled .bt-q-check{animation:none}
1849
1981
  .bt-q-check svg{width:16px;height:16px}
1850
1982
  .bt-q-settled .bt-key{opacity:.4}
1851
1983
  .bt-q-settled .bt-q-label{color:color-mix(in srgb,var(--bt-ink) 45%,transparent)}
1852
1984
  .bt-q-dismissed .bt-q-title{color:var(--bt-ink-2)}
1985
+ .bt-q.bt-q-settled{gap:6px;padding:12px 16px}
1986
+ .bt-q-settled .bt-q-title{font-size:14px;line-height:20px;font-weight:500}
1987
+ .bt-q.bt-q-settled>.bt-q-head:has(+.bt-q-list){padding-bottom:0;border-bottom:0}
1988
+ .bt-q.bt-q-settled>.bt-q-head+.bt-q-list{margin-top:0}
1989
+ .bt-q-settled .bt-q-opt{padding:4px 8px}
1853
1990
  .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}
1854
1991
  .bt-q-custom{display:flex;align-items:flex-start;gap:8px;width:100%;min-width:0}
1855
- .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-weak);background:var(--bt-main);cursor:text;transition:border-color .12s ease}
1856
- .bt-q-field:focus-within{border-color:var(--bt-line-2)}
1992
+ .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}
1993
+ .bt-q-field:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)}
1857
1994
  .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}
1858
1995
  .bt-q-input:not(:placeholder-shown){field-sizing:content}
1859
1996
  .bt-q-input::placeholder{color:var(--bt-ink-3)}
@@ -1865,16 +2002,14 @@ button.bt-q-opt:active{background:var(--bt-active)}
1865
2002
  .bt-q-btn{background:var(--bt-hover);color:var(--bt-ink)}
1866
2003
  .bt-q-btn:hover{background:var(--bt-active)}
1867
2004
  .bt-q-foot{display:flex;justify-content:flex-end;gap:8px}
1868
- .bt-q-raised{width:calc(var(--dsh-chat-content-width,748px) - 32px);max-width:calc(100% - 32px);gap:0;padding:0;background:var(--bt-main);box-shadow:0 2px 8px rgba(20,20,20,.035),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 5%,transparent)}
2005
+ .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)}
1869
2006
  .bt-q-raised .bt-q-head{padding:14px 16px 4px 20px}
1870
2007
  .bt-q-raised .bt-q-head:last-child{padding-bottom:16px}
1871
- .bt-q-raised .bt-q-title{font-size:15px;line-height:22px;font-weight:600}
1872
- .bt-q-raised .bt-q-list{gap:1px;padding:8px;border:0;border-radius:0;background:none;overflow:visible}
1873
- .bt-q-raised .bt-q-opt{align-items:flex-start;gap:4px;padding:7px;border:0;border-radius:16px;color:var(--bt-ink-2)}
2008
+ .bt-q-raised .bt-q-list{gap:1px;margin:0;padding:8px}
2009
+ .bt-q-raised .bt-q-opt{gap:4px;padding:7px;border-radius:16px;color:var(--bt-ink-2)}
1874
2010
  .bt-q-raised button.bt-q-opt:hover{color:var(--bt-ink)}
1875
2011
  .bt-q-raised .bt-q-opt[aria-pressed=true]{background:transparent;color:var(--bt-ink)}
1876
2012
  .bt-q-raised .bt-q-opt[aria-pressed=true]:hover{background:var(--bt-hover)}
1877
- .bt-q-raised .bt-key{width:24px;min-width:24px;height:24px;padding:0;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink-2);font-size:12px;line-height:24px;font-weight:500;transition:background-color .12s ease,color .12s ease}
1878
2013
  .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)}
1879
2014
  .bt-q-raised .bt-q-body{padding:2px 6px 0}
1880
2015
  .bt-q-raised .bt-q-desc{color:color-mix(in srgb,var(--bt-ink) 61%,transparent)}
@@ -1938,6 +2073,7 @@ button.bt-q-opt:active{background:var(--bt-active)}
1938
2073
  @keyframes bt-sheet-out{to{opacity:0;transform:translateY(16px);filter:blur(4px)}}
1939
2074
  .bt-exchange .bt-dialog{position:relative}
1940
2075
  .bt-exchange .bt-dialog-log{max-width:none;margin:0;padding:39px max(16px,calc((100% - 960px)/2)) 76px;scrollbar-width:thin}
2076
+ .bt-exchange .bt-tools{display:none}
1941
2077
  .bt-exchange .bt-dialog-foot{position:absolute;left:0;right:0;bottom:0;height:76px;box-sizing:border-box;align-items:flex-end;padding:0 0 28px;background:var(--bt-main);z-index:3;animation:bt-foot-in .3s cubic-bezier(.22,1,.36,1) .3s both}
1942
2078
  .bt-exchange .bt-dialog-foot::before{content:"";position:absolute;left:0;right:0;bottom:100%;height:40px;background:linear-gradient(to top,var(--bt-main),transparent);pointer-events:none}
1943
2079
  .bt-exchange[data-leaving] .bt-dialog-foot{animation:bt-foot-out .22s cubic-bezier(.4,0,1,1) forwards}
@@ -2403,6 +2539,11 @@ var CONVERSATION_CSS = `
2403
2539
  .bt-event button{border:0;background:none;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:4px;cursor:pointer;height:24px;padding:4px 6px 4px 4px;border-radius:999px;box-sizing:border-box;transition:background-color .12s ease}
2404
2540
  .bt-event button:hover{background:var(--bt-hover)}
2405
2541
  .bt-event button:active{background:var(--bt-active)}
2542
+ .bt-event button.bt-event-whole{min-width:0;max-width:100%;padding:4px 6px 4px 8px}
2543
+ .bt-event-whole>*{flex:none}
2544
+ .bt-event-whole>span:first-child{margin-right:2px}
2545
+ /* One button cannot wrap like separate parts, so a long name is cut to fit the chat. */
2546
+ .bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2406
2547
  .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}
2407
2548
  [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}
2408
2549
  .bt-activity{display:flex;align-items:center;gap:10px;margin:8px 0 4px 2px;font-size:14px;color:var(--bt-ink-2)}
@@ -2645,7 +2786,7 @@ function installCompiledStyles(text, tag) {
2645
2786
  var import_react2 = require("react");
2646
2787
  var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"];
2647
2788
  var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle";
2648
- var toolState = (name) => /^(message_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";
2789
+ 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";
2649
2790
  function useGaze(ref, enabled) {
2650
2791
  (0, import_react2.useEffect)(() => {
2651
2792
  if (!enabled) return void 0;
@@ -2892,6 +3033,7 @@ function PinMarkIcon() {
2892
3033
  }
2893
3034
  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"]);
2894
3035
  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"]);
3036
+ 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"]);
2895
3037
  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"]);
2896
3038
  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"]);
2897
3039
  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"]);
@@ -3677,25 +3819,54 @@ function ThinkingRow({ text }) {
3677
3819
  (0, import_react6.createElement)("div", { className: "bt-think-text" }, text)
3678
3820
  );
3679
3821
  }
3680
- function AssistantCell({ node, groupPart, sessionId, useRoster, useUi, useTurnSteps, actions }) {
3822
+ function AssistantCell({ node, groupPart, sessionId, useRoster, useUi, useTurnSteps, useTurnContext, actions }) {
3681
3823
  const roster = useRoster((value) => value);
3682
3824
  const data = node.data;
3683
3825
  const steps = useTurnSteps(String(data.turn));
3826
+ const contexts = useTurnContext(String(data.turn));
3684
3827
  const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn);
3685
3828
  const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text);
3686
3829
  const running = data.status === "running";
3687
3830
  useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null);
3688
3831
  if (hidden) return null;
3689
3832
  const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data);
3690
- if (texts2.length === 0 && !reasoning) return null;
3833
+ const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : [];
3834
+ if (texts2.length === 0 && !reasoning && relays.length === 0) return null;
3835
+ const selfId = roster.byId[sessionId]?.id ?? sessionId;
3691
3836
  return (0, import_react6.createElement)(
3692
3837
  import_react6.Fragment,
3693
3838
  null,
3839
+ relays.map((relay) => {
3840
+ const source = relay.source;
3841
+ const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
3842
+ return (0, import_react6.createElement)(EventLine, {
3843
+ key: relay.seq,
3844
+ prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"),
3845
+ bots: [sender],
3846
+ onOpen: () => {
3847
+ if (sender.id) actions.openExchange(selfId, sender.id);
3848
+ },
3849
+ whole: true
3850
+ });
3851
+ }),
3694
3852
  reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null,
3695
- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId: roster.byId[sessionId]?.id ?? sessionId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : null
3853
+ texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : null
3696
3854
  );
3697
3855
  }
3698
- function EventLine({ prefix, bots, joiner = t("and"), onOpen }) {
3856
+ function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) {
3857
+ if (whole && bots.length === 1) {
3858
+ return (0, import_react6.createElement)(
3859
+ "div",
3860
+ { className: "bt-event" },
3861
+ (0, import_react6.createElement)(
3862
+ "button",
3863
+ { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) },
3864
+ (0, import_react6.createElement)("span", null, prefix),
3865
+ (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }),
3866
+ (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name)
3867
+ )
3868
+ );
3869
+ }
3699
3870
  const parts = [];
3700
3871
  parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix));
3701
3872
  bots.forEach((bot, index) => {
@@ -3834,9 +4005,10 @@ function EarlierParts({ sessionId, roster, actions }) {
3834
4005
  case "event": {
3835
4006
  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 });
3836
4007
  const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" };
3837
- return (0, import_react6.createElement)(EventLine, { key, prefix: row.role === "reply" ? t("Reply from") : t("Message from"), bots: [other], onOpen: () => {
4008
+ const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from");
4009
+ return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => {
3838
4010
  if (selfId && other.id) actions.openExchange(selfId, other.id);
3839
- } });
4011
+ }, whole: true });
3840
4012
  }
3841
4013
  default:
3842
4014
  return null;
@@ -3850,7 +4022,8 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us
3850
4022
  const data = node.data;
3851
4023
  const source = data.source !== null && typeof data.source === "object" ? data.source : {};
3852
4024
  const selfId = roster.byId[sessionId]?.id ?? sessionId;
3853
- const relayed = source.kind === "bot" && source.handoff !== true && (source.role === "request" || source.role === "reply") && typeof source.senderSessionId === "string";
4025
+ const deferred = source.kind === "bot" && source.deferred === true;
4026
+ const relayed = source.kind === "bot" && source.handoff !== true && !deferred && (source.role === "request" || source.role === "reply" || source.role === "stopped") && typeof source.senderSessionId === "string";
3854
4027
  const turn = node.location?.turn?.turn ?? data.turn;
3855
4028
  (0, import_react6.useLayoutEffect)(() => {
3856
4029
  if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId);
@@ -3876,8 +4049,9 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us
3876
4049
  if (source.kind === "bot") {
3877
4050
  if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions });
3878
4051
  if (source.role === "kickoff" || source.role === "room") return null;
3879
- if (source.role === "secret") return null;
4052
+ if (source.role === "secret" || deferred) return null;
3880
4053
  if (source.role === "post") {
4054
+ if (source.failed !== void 0) return null;
3881
4055
  const room2 = roster.roomsById[sessionId];
3882
4056
  const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" };
3883
4057
  return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi });
@@ -3899,9 +4073,10 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us
3899
4073
  if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer);
3900
4074
  }
3901
4075
  return (0, import_react6.createElement)(EventLine, {
3902
- prefix: count > 1 ? t("{count} messages with", { count }) : t("Message from"),
4076
+ prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"),
3903
4077
  bots,
3904
- onOpen: (bot) => actions.openExchange(selfId, bot.id)
4078
+ onOpen: (bot) => actions.openExchange(selfId, bot.id),
4079
+ whole: true
3905
4080
  });
3906
4081
  }
3907
4082
  if (source.kind === "schedule") {
@@ -3941,11 +4116,11 @@ function clockLabel(time, now = Date.now()) {
3941
4116
  const date = new Date(time);
3942
4117
  const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime();
3943
4118
  const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5);
3944
- const clock3 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" });
3945
- if (days === 0) return t("Today {time}", { time: clock3 });
3946
- if (days === 1) return t("Yesterday {time}", { time: clock3 });
4119
+ const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" });
4120
+ if (days === 0) return t("Today {time}", { time: clock4 });
4121
+ if (days === 1) return t("Yesterday {time}", { time: clock4 });
3947
4122
  const sameYear = date.getFullYear() === new Date(now).getFullYear();
3948
- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock3}`;
4123
+ return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`;
3949
4124
  }
3950
4125
  function nextTurnStart(turns, turn) {
3951
4126
  let next;
@@ -4005,7 +4180,7 @@ var TOOL_PHRASES = [
4005
4180
  [/image|photo|draw/i, "Working on an image"]
4006
4181
  ];
4007
4182
  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" };
4008
- function toolActivity(name, arg, findBot) {
4183
+ function toolActivity(name, arg, findBot, self2) {
4009
4184
  switch (name) {
4010
4185
  case "ask_user":
4011
4186
  return null;
@@ -4021,10 +4196,14 @@ function toolActivity(name, arg, findBot) {
4021
4196
  const target = findBot(arg("to"));
4022
4197
  return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" };
4023
4198
  }
4199
+ case "stop_bot": {
4200
+ const target = findBot(arg("bot"));
4201
+ return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" };
4202
+ }
4024
4203
  case "create_bot":
4025
4204
  return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" };
4026
4205
  case "update_bot": {
4027
- const target = findBot(arg("name")) ?? findBot(arg("bot"));
4206
+ const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self2);
4028
4207
  return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" };
4029
4208
  }
4030
4209
  case "list_bots":
@@ -4045,6 +4224,10 @@ function toolActivity(name, arg, findBot) {
4045
4224
  return { label: phrase ? t(phrase) : null, state: toolState(name) };
4046
4225
  }
4047
4226
  }
4227
+ function workPhrase(tool) {
4228
+ if (!tool) return t("Thinking about it");
4229
+ return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it");
4230
+ }
4048
4231
  function useLiveNote(actions, sessionId, id, entry) {
4049
4232
  const key = entry ? JSON.stringify(entry) : "";
4050
4233
  (0, import_react6.useEffect)(() => {
@@ -4053,7 +4236,7 @@ function useLiveNote(actions, sessionId, id, entry) {
4053
4236
  return () => actions.noteLive(sessionId, id, null);
4054
4237
  }, [sessionId, id, key]);
4055
4238
  }
4056
- function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi }) {
4239
+ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) {
4057
4240
  const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null });
4058
4241
  (0, import_react6.useEffect)(() => {
4059
4242
  if (done) return void 0;
@@ -4086,6 +4269,7 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi
4086
4269
  }
4087
4270
  }
4088
4271
  const pending = done || !live.speaking ? [] : [live.speaking];
4272
+ const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0;
4089
4273
  if (replies.length === 0 && pending.length === 0) return null;
4090
4274
  const room2 = roster.roomsById[sessionId];
4091
4275
  return (0, import_react6.createElement)(
@@ -4093,6 +4277,13 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi
4093
4277
  { className: "bt-group-replies" },
4094
4278
  replies.map((reply, index) => {
4095
4279
  const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" };
4280
+ if (reply.kind === "user") {
4281
+ return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)(
4282
+ "div",
4283
+ { className: "bt-ububble" },
4284
+ (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" })
4285
+ ));
4286
+ }
4096
4287
  if (reply.kind === "failed") {
4097
4288
  return (0, import_react6.createElement)(
4098
4289
  "div",
@@ -4107,6 +4298,21 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi
4107
4298
  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 })
4108
4299
  );
4109
4300
  }
4301
+ if (reply.kind === "missed") {
4302
+ return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name }));
4303
+ }
4304
+ if (reply.kind === "approval") {
4305
+ return (0, import_react6.createElement)(
4306
+ "div",
4307
+ { key: `${reply.botId}-${index}`, className: "bt-event" },
4308
+ (0, import_react6.createElement)(
4309
+ "button",
4310
+ { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) },
4311
+ (0, import_react6.createElement)(BotMark, { bot, size: 16 }),
4312
+ (0, import_react6.createElement)("span", { className: "bt-event-name" }, t("{name} paused for your approval", { name: bot.name }))
4313
+ )
4314
+ );
4315
+ }
4110
4316
  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 });
4111
4317
  }),
4112
4318
  pending.map((id) => {
@@ -4115,13 +4321,13 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi
4115
4321
  return (0, import_react6.createElement)(
4116
4322
  "div",
4117
4323
  { key: `pending-${id}`, className: "bt-activity" },
4118
- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: "thinking" }),
4119
- (0, import_react6.createElement)(Shimmer, null, t("{name} is typing…", { name: bot.name }))
4324
+ (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }),
4325
+ (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name }))
4120
4326
  );
4121
4327
  })
4122
4328
  );
4123
4329
  }
4124
- function ToolCell({ node, sessionId, useRoster, useUi, actions }) {
4330
+ function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) {
4125
4331
  const roster = useRoster((value) => value);
4126
4332
  const root = node.data.root;
4127
4333
  const done = root.kind === "tool-result";
@@ -4130,13 +4336,18 @@ function ToolCell({ node, sessionId, useRoster, useUi, actions }) {
4130
4336
  const tool = toolName(root);
4131
4337
  const { text: arg, value: argValue } = toolArgs(root);
4132
4338
  const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase());
4133
- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, done || hidden ? null : toolActivity(tool, arg, findBot));
4339
+ useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, done || hidden ? null : toolActivity(tool, arg, findBot, roster.byId[sessionId]));
4134
4340
  if (hidden) return null;
4135
4341
  switch (tool) {
4136
4342
  case "message_bot": {
4137
4343
  const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" };
4138
4344
  if (done && root.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name }));
4139
- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id) });
4345
+ return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true });
4346
+ }
4347
+ case "stop_bot": {
4348
+ const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" };
4349
+ if (done && root.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name }));
4350
+ return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(roster.byId[sessionId]?.id ?? sessionId, bot.id), whole: true });
4140
4351
  }
4141
4352
  case "create_bot": {
4142
4353
  const created = findBot(arg("name"));
@@ -4144,11 +4355,11 @@ function ToolCell({ node, sessionId, useRoster, useUi, actions }) {
4144
4355
  return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) });
4145
4356
  }
4146
4357
  case "update_bot": {
4147
- const target = findBot(arg("name")) ?? findBot(arg("bot"));
4358
+ const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : roster.byId[sessionId]);
4148
4359
  return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null;
4149
4360
  }
4150
4361
  case "group_relay":
4151
- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: root.callId, done, content: root.content, roster, actions, useUi });
4362
+ return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: root.callId, done, content: root.content, roster, actions, useUi, useWork });
4152
4363
  case "create_group":
4153
4364
  case "update_group":
4154
4365
  case "delete_group":
@@ -4283,10 +4494,11 @@ function useRowEntrances(replacing) {
4283
4494
  };
4284
4495
  }, []);
4285
4496
  }
4286
- function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, actions }) {
4497
+ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) {
4287
4498
  const roster = useRoster((value) => value);
4288
4499
  const running = useSessionStatus((map) => map.get(sessionId)?.running === true);
4289
4500
  const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {}));
4501
+ const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time)));
4290
4502
  const bot = roster.byId[sessionId];
4291
4503
  const room2 = roster.roomsById[sessionId];
4292
4504
  const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]);
@@ -4295,7 +4507,10 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, ac
4295
4507
  const relaying = tools.some((entry) => entry.relay);
4296
4508
  const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0);
4297
4509
  const tool = tools.at(-1);
4298
- const want = !visible ? null : 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" };
4510
+ const inputRef = (0, import_react7.useRef)(0);
4511
+ const inputAt = Math.max(inputRef.current, pendingInput);
4512
+ inputRef.current = inputAt;
4513
+ 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" };
4299
4514
  const [shown, setShown] = (0, import_react7.useState)(null);
4300
4515
  const wantKey = want?.key ?? null;
4301
4516
  const markState = (0, import_react7.useRef)("thinking");
@@ -4369,6 +4584,16 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, ac
4369
4584
  ), host);
4370
4585
  }
4371
4586
 
4587
+ // src/client/dsh-offer.js
4588
+ var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]);
4589
+ var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0];
4590
+ var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b);
4591
+ function showsDshRow(dsh, app) {
4592
+ if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false;
4593
+ if (dsh.kind === "npm") return true;
4594
+ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified);
4595
+ }
4596
+
4372
4597
  // src/client/sidebar.js
4373
4598
  var import_react8 = require("react");
4374
4599
  var import_react_dom3 = require("react-dom");
@@ -4722,7 +4947,7 @@ function UpdateButton({ useUpdate, actions }) {
4722
4947
  const phase = update?.phase ?? "idle";
4723
4948
  const dsh = update?.dsh;
4724
4949
  const dshPhase = dsh?.phase ?? "idle";
4725
- const dshRow = dsh && dsh.verified && dsh.current && dsh.verified !== dsh.current;
4950
+ const dshRow = showsDshRow(dsh, update?.app);
4726
4951
  if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null;
4727
4952
  const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase;
4728
4953
  const toggle = () => {
@@ -4741,7 +4966,7 @@ function UpdateButton({ useUpdate, actions }) {
4741
4966
  const label = phase === "installing" ? t("Updating DS Bot…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("DS Bot {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : update?.available ? t("DS Bot {version} is available", { version: update.latest }) : t("DSH {version} is available", { version: dsh?.verified });
4742
4967
  const message = error || (phase === "failed" ? update.error : "");
4743
4968
  const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : "");
4744
- const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy && dsh.kind === "npm" ? t("Update DS Bot {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current });
4969
+ const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update DS Bot {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current });
4745
4970
  return (0, import_react8.createElement)(
4746
4971
  import_react8.Fragment,
4747
4972
  null,
@@ -4786,7 +5011,8 @@ function UpdateButton({ useUpdate, actions }) {
4786
5011
  (0, import_react8.createElement)(
4787
5012
  "div",
4788
5013
  { className: "bt-update-actions" },
4789
- dsh.kind === "app" ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => actions.openAppUpdates() }, t("Open updates")) : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", {
5014
+ dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => {
5015
+ }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", {
4790
5016
  type: "button",
4791
5017
  className: "bt-update-go",
4792
5018
  disabled: dshPhase === "installing",
@@ -5089,7 +5315,7 @@ var SIDEBAR_CSS = `
5089
5315
  .bt-update-go:hover:not(:disabled){opacity:.9}
5090
5316
  .bt-update-go:active:not(:disabled){transform:scale(.96)}
5091
5317
  .bt-update-go:disabled{opacity:.6;cursor:default}
5092
- .bt-update-dsh{margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)}
5318
+ .bt-update-dsh:not(:first-child){margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)}
5093
5319
  @media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}}
5094
5320
 
5095
5321
  /* The DSH Agent row and its expanding Session list. The black whale is the
@@ -5145,7 +5371,7 @@ function MemoryNotice({ shown }) {
5145
5371
  (0, import_react9.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react9.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react9.createElement)(MemoryIcon)), label)
5146
5372
  );
5147
5373
  }
5148
- function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) {
5374
+ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) {
5149
5375
  const roster = useRoster((value) => value);
5150
5376
  const title = useSessions((list) => list.byId[sessionId]?.displayTitle ?? list.byId[sessionId]?.title ?? "");
5151
5377
  const firstTurns = useSessions((list) => {
@@ -5209,7 +5435,7 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useAc
5209
5435
  ),
5210
5436
  (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""),
5211
5437
  (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }),
5212
- (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useLive, actions })
5438
+ (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions })
5213
5439
  );
5214
5440
  }
5215
5441
  function useTranscriptView() {
@@ -5441,7 +5667,16 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) {
5441
5667
  dictation.note ? (0, import_react10.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null
5442
5668
  );
5443
5669
  }
5444
- function ComposerPlus({ sessionId, useRoster, actions }) {
5670
+ function menuIcons(roster) {
5671
+ const marks = /* @__PURE__ */ new Map();
5672
+ return (row) => {
5673
+ if (!row.bot) return PeopleIcon;
5674
+ const id = row.bot.id;
5675
+ if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react10.createElement)(BotMark, { bot: roster.getSnapshot().byId[id] ?? row.bot, size }));
5676
+ return marks.get(id);
5677
+ };
5678
+ }
5679
+ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) {
5445
5680
  const roster = useRoster((value) => value);
5446
5681
  const [open, setOpen] = (0, import_react10.useState)(null);
5447
5682
  const [shown, leaving] = useLinger(open, 120);
@@ -5483,6 +5718,11 @@ function ComposerPlus({ sessionId, useRoster, actions }) {
5483
5718
  setOpen(null);
5484
5719
  actions.openSettings("schedules", target);
5485
5720
  };
5721
+ const reference = () => {
5722
+ setOpen(null);
5723
+ actions.referenceChat(sessionId, inputActions?.captureInsertion?.());
5724
+ };
5725
+ const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat();
5486
5726
  return (0, import_react10.createElement)(
5487
5727
  import_react10.Fragment,
5488
5728
  null,
@@ -5508,11 +5748,100 @@ function ComposerPlus({ sessionId, useRoster, actions }) {
5508
5748
  "data-leaving": leaving || void 0
5509
5749
  },
5510
5750
  (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react10.createElement)(PaperclipIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Add files"))),
5751
+ canReference ? (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react10.createElement)(ChatsIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null,
5511
5752
  (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react10.createElement)(ClockIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task")))
5512
5753
  ), document.body) : null
5513
5754
  );
5514
5755
  }
5515
5756
 
5757
+ // src/client/at-menu.js
5758
+ var BOTS_SOURCE = "ds-bot";
5759
+ var CHATS_SOURCE = "ds-bot-chats";
5760
+ var ROOM_ALL = ["all", "everyone", "所有人", "全体"];
5761
+ var OTHER_CHATS_MAX = 8;
5762
+ var conversationOf = (roster, sessionId) => {
5763
+ if (!roster.ready) return null;
5764
+ const room2 = roster.roomsById[sessionId];
5765
+ if (room2) return { room: room2 };
5766
+ const self2 = roster.byId[sessionId];
5767
+ return self2 ? { self: self2 } : null;
5768
+ };
5769
+ function mentionRows(roster, sessionId, query = "") {
5770
+ const here = conversationOf(roster, sessionId);
5771
+ if (here === null) return null;
5772
+ const bots = here.room ? here.room.members.map((id) => roster.byId[id]).filter(Boolean) : roster.bots.filter((bot) => bot.id !== here.self.id);
5773
+ const needle = query.trim().toLowerCase();
5774
+ const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle)).map((bot) => ({ bot, text: `@${bot.name} ` }));
5775
+ const everyone = t("Everyone");
5776
+ if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) {
5777
+ rows.push({ room: here.room, text: `@${everyone} ` });
5778
+ }
5779
+ return rows;
5780
+ }
5781
+ function chatRows(roster, list, sessionId) {
5782
+ const here = conversationOf(roster, sessionId);
5783
+ if (here === null) return [];
5784
+ const byId = list?.byId ?? {};
5785
+ const bots = roster.bots.filter((bot) => bot.id !== here.self?.id).map((bot) => {
5786
+ const id = currentPart(bot);
5787
+ return { sessionId: id, label: t("Chat with {name}", { name: bot.name }), bot, updatedAt: byId[id]?.updatedAt };
5788
+ });
5789
+ const team = /* @__PURE__ */ new Set([
5790
+ ...roster.bots.flatMap((bot) => [bot.id, currentPart(bot), ...bot.parts ?? []]),
5791
+ ...roster.rooms.flatMap((room2) => [room2.id, ...room2.parts ?? []])
5792
+ ]);
5793
+ const others = (list?.ids ?? []).map((id) => byId[id]).filter((row) => row !== void 0 && row.id !== sessionId && !team.has(row.id) && row.origin !== "subagent" && row.blank !== true).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, OTHER_CHATS_MAX).map((row) => ({ sessionId: row.id, label: row.displayTitle || row.title || row.id, updatedAt: row.updatedAt }));
5794
+ return [...bots, ...others];
5795
+ }
5796
+ function sessionMention(sessionId, label) {
5797
+ let binary = "";
5798
+ for (const byte of new TextEncoder().encode(JSON.stringify(sessionId))) binary += String.fromCharCode(byte);
5799
+ const payload = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5800
+ return `@[${label.replace(/[\\\]]/g, (mark) => `\\${mark}`)}](dsh-session:${payload})`;
5801
+ }
5802
+ function atSources({ roster, sessions, launched, iconFor }) {
5803
+ const bots = {
5804
+ trigger: "@",
5805
+ name: BOTS_SOURCE,
5806
+ order: -20,
5807
+ showGroupTitle: false,
5808
+ async candidates(session, { query }) {
5809
+ return (mentionRows(roster.getSnapshot(), session.sessionId, query) ?? []).map((row) => ({
5810
+ name: row.bot ? row.bot.name : t("Everyone"),
5811
+ ...row.bot?.role ? { description: row.bot.role } : {},
5812
+ ...row.room ? { description: t("{count} members", { count: row.room.members.length }) } : {},
5813
+ icon: iconFor(row),
5814
+ section: t("Bots"),
5815
+ value: row.text
5816
+ }));
5817
+ },
5818
+ onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0
5819
+ };
5820
+ const chats = {
5821
+ trigger: "@",
5822
+ name: CHATS_SOURCE,
5823
+ order: -10,
5824
+ showGroupTitle: false,
5825
+ async candidates(session) {
5826
+ if (!launched(session.sessionId)) return [];
5827
+ return chatRows(roster.getSnapshot(), sessions.list.getSnapshot(), session.sessionId).map((row) => ({
5828
+ name: row.bot ? row.bot.name : row.label,
5829
+ ...row.updatedAt ? { description: relativeShort(row.updatedAt) } : {},
5830
+ icon: row.bot ? iconFor(row) : "session",
5831
+ section: row.bot ? t("Bots") : t("Conversations"),
5832
+ value: JSON.stringify({ sessionId: row.sessionId, label: row.label })
5833
+ }));
5834
+ },
5835
+ onPick({ candidate }) {
5836
+ const { sessionId, label } = JSON.parse(candidate.value);
5837
+ const mention = sessionMention(sessionId, label);
5838
+ return { insert: { source: CHATS_SOURCE, ref: mention, label, appearance: "session", clipboardText: mention } };
5839
+ },
5840
+ codec: { clipboardText: (ref) => ref, serialize: (ref) => Promise.resolve(ref) }
5841
+ };
5842
+ return [bots, chats];
5843
+ }
5844
+
5516
5845
  // src/client/speech.js
5517
5846
  var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle";
5518
5847
  var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]);
@@ -7330,8 +7659,9 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
7330
7659
  const fallbackName = freshBotName(roster, t("New Bot"));
7331
7660
  const finalName = name.trim() || fallbackName;
7332
7661
  const clash = roster.bots.some((bot) => bot.name.toLowerCase() === finalName.toLowerCase());
7662
+ const problem = nameProblem(finalName);
7333
7663
  const create = async () => {
7334
- if (busy || clash) return;
7664
+ if (busy || clash || problem) return;
7335
7665
  setError("");
7336
7666
  setBusy(true);
7337
7667
  try {
@@ -7413,7 +7743,7 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
7413
7743
  }
7414
7744
  }
7415
7745
  }),
7416
- clash ? (0, import_react17.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null
7746
+ problem ? (0, import_react17.createElement)("span", { className: "bt-note bt-danger" }, hostText(problem)) : clash ? (0, import_react17.createElement)("span", { className: "bt-note bt-danger" }, t("A Bot named {name} already exists", { name: finalName })) : null
7417
7747
  ),
7418
7748
  (0, import_react17.createElement)(
7419
7749
  "div",
@@ -7430,7 +7760,7 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
7430
7760
  "div",
7431
7761
  { className: "bt-create-foot" },
7432
7762
  (0, import_react17.createElement)("button", { type: "button", className: "bt-soft", onClick: onBack }, canGoBack ? t("Back") : t("Cancel")),
7433
- (0, import_react17.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash, onClick: () => void create() }, busy ? t("Creating…") : t("Create"))
7763
+ (0, import_react17.createElement)("button", { type: "button", className: "bt-send bt-create-go", disabled: busy || clash || Boolean(problem), onClick: () => void create() }, busy ? t("Creating…") : t("Create"))
7434
7764
  )
7435
7765
  );
7436
7766
  if (inline) return (0, import_react17.createElement)("div", { ref: frame, className: "bt-create-inline", role: "group", "aria-label": t("Create Bot") }, card);
@@ -8813,6 +9143,7 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) {
8813
9143
  );
8814
9144
  }
8815
9145
  function Heat({ view, keep, paint, index }) {
9146
+ const [hover, setHover] = (0, import_react22.useState)(null);
8816
9147
  const today = hourMs(`${view.now.slice(0, 10)} 00`);
8817
9148
  const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS;
8818
9149
  const from = dayKey(first);
@@ -8838,12 +9169,17 @@ function Heat({ view, keep, paint, index }) {
8838
9169
  key: ms,
8839
9170
  className: "bt-us-cell",
8840
9171
  style: { "--w": week },
9172
+ "data-on": hover?.ms === ms ? "" : void 0,
8841
9173
  "data-future": ms > today ? "" : void 0,
8842
9174
  "data-today": ms === today ? "" : void 0,
8843
- title: ms > today ? void 0 : `${dates({ month: "long", day: "numeric", weekday: "short" }).format(ms)} · ${t("{count} tokens", { count: full(value) })}`
9175
+ onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day })
8844
9176
  }, level > 0 ? (0, import_react22.createElement)("i", { style: { background: soften(paint, levels[level - 1]) } }) : null));
8845
9177
  }
8846
9178
  }
9179
+ const tipSide = hover ? {
9180
+ ...hover.week >= HEAT_WEEKS / 2 ? { right: `calc(${(HEAT_WEEKS - hover.week) / HEAT_WEEKS * 100}% + 8px)` } : { left: `calc(${(hover.week + 1) / HEAT_WEEKS * 100}% + 8px)` },
9181
+ ...hover.day < 4 ? { top: `calc(${hover.day / 7 * 100}% - 2px)` } : { top: "auto", bottom: `calc(${(6 - hover.day) / 7 * 100}% - 2px)` }
9182
+ } : null;
8847
9183
  const active = [...daily.values()].filter((value) => value > 0).length;
8848
9184
  const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active });
8849
9185
  return (0, import_react22.createElement)(
@@ -8851,7 +9187,17 @@ function Heat({ view, keep, paint, index }) {
8851
9187
  { className: "bt-us-card bt-us-heat", style: { "--i": index } },
8852
9188
  (0, import_react22.createElement)("div", { className: "bt-us-card-head" }, (0, import_react22.createElement)("h3", null, t("Activity")), (0, import_react22.createElement)("span", { className: "bt-us-head-note" }, summary)),
8853
9189
  (0, import_react22.createElement)("div", { className: "bt-us-heat-months", "aria-hidden": true }, months),
8854
- (0, import_react22.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
9190
+ (0, import_react22.createElement)(
9191
+ "div",
9192
+ { className: "bt-us-heat-plot", onMouseLeave: () => setHover(null) },
9193
+ (0, import_react22.createElement)("div", { className: "bt-us-heat-grid", role: "img", "aria-label": summary }, cells),
9194
+ hover ? (0, import_react22.createElement)(
9195
+ "div",
9196
+ { className: "bt-us-tip bt-us-heat-tip", style: tipSide },
9197
+ (0, import_react22.createElement)("div", { className: "bt-us-tip-title" }, dates({ month: "long", day: "numeric", weekday: "short" }).format(hover.ms)),
9198
+ (0, import_react22.createElement)("div", { className: "bt-us-tip-total" }, t("{count} tokens", { count: full(daily.get(dayKey(hover.ms)) ?? 0) }))
9199
+ ) : null
9200
+ ),
8855
9201
  (0, import_react22.createElement)(
8856
9202
  "div",
8857
9203
  { className: "bt-us-heat-scale", "aria-hidden": true },
@@ -8866,9 +9212,18 @@ function Skeleton2() {
8866
9212
  "div",
8867
9213
  { className: "bt-us", "aria-busy": true },
8868
9214
  (0, import_react22.createElement)("div", { className: "bt-us-skel", style: { height: 32, width: 320, borderRadius: 999 } }),
8869
- (0, import_react22.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
8870
- (0, import_react22.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
8871
- (0, import_react22.createElement)("div", { className: "bt-us-pair" }, (0, import_react22.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }), (0, import_react22.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }))
9215
+ (0, import_react22.createElement)(
9216
+ "div",
9217
+ { className: "bt-us-sheet bt-us-skel-sheet" },
9218
+ (0, import_react22.createElement)("div", { className: "bt-us-skel", style: { height: 94 } }),
9219
+ (0, import_react22.createElement)("div", { className: "bt-us-skel", style: { height: 318 } }),
9220
+ (0, import_react22.createElement)(
9221
+ "div",
9222
+ { className: "bt-us-pair" },
9223
+ (0, import_react22.createElement)("div", { className: "bt-us-skel", style: { height: 200 } }),
9224
+ (0, import_react22.createElement)("div", { className: "bt-us-skel", style: { height: 200 } })
9225
+ )
9226
+ )
8872
9227
  );
8873
9228
  }
8874
9229
  var focusOf = (id) => typeof id !== "string" ? null : id.startsWith("model:") ? { kind: "model", key: id.slice("model:".length) } : id.startsWith("owner:") ? { kind: "owner", key: id.slice("owner:".length) } : null;
@@ -9038,19 +9393,23 @@ function UsagePage({ roster, actions, focusId }) {
9038
9393
  const owners = ranked(part.owners);
9039
9394
  body = [
9040
9395
  (0, import_react22.createElement)(Profile, { key: "profile", face, range, share: all.total.total > 0 ? whole / all.total.total : 0 }),
9041
- (0, import_react22.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
9042
- (0, import_react22.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
9043
9396
  (0, import_react22.createElement)(
9044
9397
  "div",
9045
- { key: "pair", className: "bt-us-pair" },
9046
- ownerAt >= 0 ? (0, import_react22.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react22.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing),
9398
+ { key: "sheet", className: "bt-us-sheet" },
9399
+ (0, import_react22.createElement)(Stats, { key: "stats", range, total: part.total, previous: part.previous }),
9400
+ (0, import_react22.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, onPick: pick2 }),
9047
9401
  (0, import_react22.createElement)(
9048
- ListCard,
9049
- { title: t("What for"), index: 3 },
9050
- kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react22.createElement)(Row2, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing
9051
- )
9052
- ),
9053
- (0, import_react22.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 })
9402
+ "div",
9403
+ { key: "pair", className: "bt-us-pair" },
9404
+ ownerAt >= 0 ? (0, import_react22.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 2 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing) : (0, import_react22.createElement)(ListCard, { title: t("Who used it"), count: owners.length, index: 2 }, owners.length > 0 ? owners.map((entry, index) => ownerRow(entry, index)) : nothing),
9405
+ (0, import_react22.createElement)(
9406
+ ListCard,
9407
+ { title: t("What for"), index: 3 },
9408
+ kinds.length > 0 ? kinds.map(([at, sum], index) => (0, import_react22.createElement)(Row2, { key: at, name: kindLabel(view.kinds[at], ownerAt >= 0 && isBot(ownerAt)), sum, whole, index, fill: soften(face.paint, 0.8) })) : nothing
9409
+ )
9410
+ ),
9411
+ (0, import_react22.createElement)(Heat, { key: "heat", view, keep, paint: face.paint, index: 4 })
9412
+ )
9054
9413
  ];
9055
9414
  } else {
9056
9415
  const owners = ranked(all.owners);
@@ -9058,22 +9417,26 @@ function UsagePage({ roster, actions, focusId }) {
9058
9417
  const rest = owners.filter(([at]) => !isBot(at));
9059
9418
  const usedModels = ranked(all.models);
9060
9419
  body = [
9061
- (0, import_react22.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
9062
- (0, import_react22.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
9063
9420
  (0, import_react22.createElement)(
9064
9421
  "div",
9065
- { key: "pair", className: "bt-us-pair" },
9422
+ { key: "sheet", className: "bt-us-sheet" },
9423
+ (0, import_react22.createElement)(Stats, { key: "stats", range, total: all.total, previous: all.previous }),
9424
+ (0, import_react22.createElement)(Chart, { key: "chart", range, columns, series, tipSeries, by, onBy: setBy, onPick: pick2 }),
9066
9425
  (0, import_react22.createElement)(
9067
- ListCard,
9068
- { title: t("Bots"), count: bots.length, index: 2 },
9069
- owners.length > 0 ? null : nothing,
9070
- bots.map((entry, index) => ownerRow(entry, index)),
9071
- bots.length > 0 && rest.length > 0 ? (0, import_react22.createElement)("div", { className: "bt-us-split", role: "separator" }) : null,
9072
- rest.map((entry, index) => ownerRow(entry, bots.length + index))
9426
+ "div",
9427
+ { key: "pair", className: "bt-us-pair" },
9428
+ (0, import_react22.createElement)(
9429
+ ListCard,
9430
+ { title: t("Bots"), count: bots.length, index: 2 },
9431
+ owners.length > 0 ? null : nothing,
9432
+ bots.map((entry, index) => ownerRow(entry, index)),
9433
+ bots.length > 0 && rest.length > 0 ? (0, import_react22.createElement)("div", { className: "bt-us-split", role: "separator" }) : null,
9434
+ rest.map((entry, index) => ownerRow(entry, bots.length + index))
9435
+ ),
9436
+ (0, import_react22.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing)
9073
9437
  ),
9074
- (0, import_react22.createElement)(ListCard, { title: t("Models"), count: usedModels.length, index: 3 }, usedModels.length > 0 ? usedModels.map(modelRow) : nothing)
9075
- ),
9076
- (0, import_react22.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 })
9438
+ (0, import_react22.createElement)(Heat, { key: "heat", view, keep, paint: "var(--bt-accent)", index: 4 })
9439
+ )
9077
9440
  ];
9078
9441
  }
9079
9442
  return (0, import_react22.createElement)(
@@ -9134,6 +9497,10 @@ var USAGE_CSS = `
9134
9497
  .bt-us-tabs>.bt-thumb{top:3px;bottom:3px;border-radius:999px;background:var(--bt-card);box-shadow:0 1px 3px rgba(0,0,0,.08),0 0 0 .5px var(--bt-line-2)}
9135
9498
  .bt-us-note{display:inline-flex;align-items:center;gap:8px;font-size:12px;line-height:16px;color:var(--bt-ink-3)}
9136
9499
  .bt-us-note::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--bt-accent);animation:bt-us-pulse 1.4s ease-in-out infinite}
9500
+ .bt-us-sheet{display:flex;flex-direction:column;gap:4px}
9501
+ .bt-us-sheet>.bt-us-stats,.bt-us-sheet>.bt-us-card,.bt-us-sheet>.bt-us-pair>.bt-us-card{border-radius:0}
9502
+ .bt-us-sheet>:first-child{border-radius:16px 16px 0 0}
9503
+ .bt-us-sheet>:last-child{border-radius:0 0 16px 16px}
9137
9504
  .bt-us-stats{display:grid;grid-template-columns:1.6fr 1fr 1fr 1fr;border-radius:16px;border:1px solid var(--bt-line);background:var(--bt-card);animation:bt-us-in .45s var(--bt-us-ease) backwards}
9138
9505
  .bt-us-stat{display:flex;flex-direction:column;justify-content:flex-end;gap:6px;min-width:0;padding:16px 18px}
9139
9506
  .bt-us-stat+.bt-us-stat{border-left:1px solid var(--bt-line)}
@@ -9173,7 +9540,7 @@ var USAGE_CSS = `
9173
9540
  button.bt-us-key{cursor:pointer;transition:background-color .12s ease,color .12s ease}
9174
9541
  button.bt-us-key:hover{background:var(--bt-active);color:var(--bt-ink)}
9175
9542
  .bt-us-dot{flex:none;width:8px;height:8px;border-radius:50%}
9176
- .bt-us-pair{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
9543
+ .bt-us-pair{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:4px}
9177
9544
  .bt-us-rows{display:flex;flex-direction:column;margin:0 -8px}
9178
9545
  .bt-us-row{display:flex;align-items:center;gap:12px;width:100%;box-sizing:border-box;padding:8px;border:0;border-radius:12px;background:none;color:var(--bt-ink);font:inherit;text-align:left;animation:bt-us-in .4s var(--bt-us-ease) backwards;animation-delay:calc(var(--i,0) * 35ms + 90ms)}
9179
9546
  button.bt-us-row{cursor:pointer;transition:background-color .12s ease}
@@ -9207,7 +9574,10 @@ button.bt-us-row:hover{background:var(--bt-hover)}
9207
9574
  .bt-us-heat-months,.bt-us-heat-grid{display:grid;grid-template-columns:repeat(${HEAT_WEEKS},1fr);gap:3px}
9208
9575
  .bt-us-heat-months{height:14px;font-size:10px;line-height:14px;color:var(--bt-ink-3)}
9209
9576
  .bt-us-heat-months span{white-space:nowrap}
9577
+ .bt-us-heat-plot{position:relative}
9210
9578
  .bt-us-heat-grid{grid-template-rows:repeat(7,auto);grid-auto-flow:column}
9579
+ .bt-us-heat-grid .bt-us-cell[data-on]{outline:1.5px solid var(--bt-ink-2);outline-offset:1px}
9580
+ .bt-us-heat-tip{min-width:0;white-space:nowrap}
9211
9581
  .bt-us-cell{position:relative;aspect-ratio:1;border-radius:3px;background:var(--bt-hover);overflow:hidden}
9212
9582
  .bt-us-heat-grid .bt-us-cell{animation:bt-us-cell .5s var(--bt-us-ease) backwards;animation-delay:calc(var(--w,0) * 7ms + 120ms)}
9213
9583
  .bt-us-cell>i{position:absolute;inset:0}
@@ -9216,6 +9586,10 @@ button.bt-us-row:hover{background:var(--bt-hover)}
9216
9586
  .bt-us-heat-scale{display:flex;align-items:center;justify-content:flex-end;gap:4px;font-size:11px;line-height:14px;color:var(--bt-ink-3)}
9217
9587
  .bt-us-heat-scale .bt-us-cell{width:10px}
9218
9588
  .bt-us-skel{border-radius:16px;background:linear-gradient(90deg,var(--bt-hover) 0%,var(--bt-active) 50%,var(--bt-hover) 100%);background-size:200% 100%;animation:bt-us-shine 1.4s ease-in-out infinite}
9589
+ .bt-us-skel-sheet>.bt-us-skel:first-child{border-radius:16px 16px 0 0}
9590
+ .bt-us-skel-sheet>.bt-us-skel:not(:first-child){border-radius:0}
9591
+ .bt-us-skel-sheet>.bt-us-pair>.bt-us-skel:first-child{border-radius:0 0 0 16px}
9592
+ .bt-us-skel-sheet>.bt-us-pair>.bt-us-skel:last-child{border-radius:0 0 16px 0}
9219
9593
  .bt-us-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:72px 16px;text-align:center;font-size:13px;line-height:18px;color:var(--bt-ink-3)}
9220
9594
  .bt-us-empty b{margin-top:6px;font-size:15px;font-weight:600;color:var(--bt-ink)}
9221
9595
  @keyframes bt-us-in{from{opacity:0;transform:translateY(6px)}}
@@ -9338,8 +9712,8 @@ var copy = (text) => navigator.clipboard === void 0 ? Promise.resolve(false) : n
9338
9712
  function whenText(time) {
9339
9713
  const date = new Date(time);
9340
9714
  const today = (/* @__PURE__ */ new Date()).toDateString() === date.toDateString();
9341
- const clock3 = date.toLocaleTimeString(dateLocale(), { hour: "2-digit", minute: "2-digit" });
9342
- return today ? clock3 : `${date.toLocaleDateString(dateLocale(), { month: "short", day: "numeric" })} ${clock3}`;
9715
+ const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "2-digit", minute: "2-digit" });
9716
+ return today ? clock4 : `${date.toLocaleDateString(dateLocale(), { month: "short", day: "numeric" })} ${clock4}`;
9343
9717
  }
9344
9718
  function entrySummary(entry) {
9345
9719
  if (entry.source === "turn") return failureLabel(entry.code, entry.text);
@@ -9502,55 +9876,196 @@ var FEEDBACK_CSS = `
9502
9876
  @media (prefers-reduced-motion:reduce){.bt-toast{animation:none}.bt-toast[data-leaving]{transition:none}}
9503
9877
  `;
9504
9878
 
9505
- // src/client/settings.js
9879
+ // src/client/about.js
9506
9880
  var import_react25 = require("react");
9881
+ var CHEER_MS = 1800;
9882
+ var starCount = (count) => new Intl.NumberFormat(dateLocale(), { notation: "compact", maximumFractionDigits: 1 }).format(count);
9883
+ function botHint(update) {
9884
+ if (update.phase === "installing") return t("Updating DS Bot…");
9885
+ if (update.phase === "installed") return t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest });
9886
+ if (update.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(update.error ?? "") });
9887
+ if (update.available) return t("DS Bot {version} is available", { version: update.latest });
9888
+ if (update.enabled === false) return t("Update checks are off.");
9889
+ if (update.source === "path") return t("Installed from a local folder; update it there.");
9890
+ if (!update.checkedAt) return null;
9891
+ if (!update.latest && update.source !== "registry") return t("Updates are not offered for this install.");
9892
+ const checked = t("Checked {time}", { time: whenText(update.checkedAt) });
9893
+ return `${update.latest ? t("Up to date") : t("Could not check for updates.")} · ${checked}`;
9894
+ }
9895
+ function dshHint(dsh, shown) {
9896
+ if (dsh.phase === "installing") return t("Updating DSH…");
9897
+ if (dsh.phase === "installed") return t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified });
9898
+ if (dsh.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(dsh.error ?? "") });
9899
+ if (dsh.available === false && dsh.blockedBy) return t("Update DS Bot {version} first.", { version: dsh.blockedBy });
9900
+ if (shown) return t("DSH {version} is available", { version: dsh.verified });
9901
+ return t("Version {version}", { version: dsh.current });
9902
+ }
9903
+ var linkProps = (href) => ({ className: "bt-soft bt-soft-sm bt-about-link", href, target: "_blank", rel: "noopener noreferrer" });
9904
+ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) {
9905
+ const update = useUpdate((value) => value) ?? {};
9906
+ const [checking, setChecking] = (0, import_react25.useState)(false);
9907
+ const [stars, setStars] = (0, import_react25.useState)(null);
9908
+ (0, import_react25.useEffect)(() => {
9909
+ let live = true;
9910
+ actions.repoStars().then((count) => {
9911
+ if (live && Number.isInteger(count)) setStars(count);
9912
+ }, () => {
9913
+ });
9914
+ return () => {
9915
+ live = false;
9916
+ };
9917
+ }, []);
9918
+ const [cheer, setCheer] = (0, import_react25.useState)(true);
9919
+ (0, import_react25.useEffect)(() => {
9920
+ if (!cheer) return void 0;
9921
+ const timer = setTimeout(() => setCheer(false), CHEER_MS);
9922
+ return () => clearTimeout(timer);
9923
+ }, [cheer]);
9924
+ const check = () => {
9925
+ setChecking(true);
9926
+ void run(actions.checkUpdate()).then((done) => {
9927
+ setChecking(false);
9928
+ if (done) setCheer(true);
9929
+ });
9930
+ };
9931
+ const phase = update.phase ?? "idle";
9932
+ const dsh = update.dsh;
9933
+ const dshPhase = dsh?.phase ?? "idle";
9934
+ (0, import_react25.useEffect)(() => {
9935
+ if (phase === "installed" || dshPhase === "installed") setCheer(true);
9936
+ }, [phase, dshPhase]);
9937
+ const markState = checking ? "searching" : phase === "installing" || dshPhase === "installing" ? "working" : cheer ? "done" : "idle";
9938
+ const dshNew = showsDshRow(dsh, update.app);
9939
+ const dshShown = dshNew && dsh.available;
9940
+ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react25.createElement)(
9941
+ "button",
9942
+ { type: "button", className: "bt-fb-primary", disabled: phase === "installing", onClick: () => run(actions.installUpdate()) },
9943
+ phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")
9944
+ ) : (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: checking || update.enabled === false, onClick: check }, checking ? t("Checking…") : t("Check for updates"));
9945
+ const dshControl = !dshNew || dshPhase === "installed" ? null : dsh.kind === "app" ? dsh.available ? (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.openAppUpdates(dsh.verified)) }, t("Open updates")) : null : dsh.available || dshPhase !== "idle" ? (0, import_react25.createElement)(
9946
+ "button",
9947
+ { type: "button", className: "bt-fb-primary", disabled: dshPhase === "installing", onClick: () => run(actions.installDshUpdate()) },
9948
+ dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")
9949
+ ) : null;
9950
+ const repo = update.repoUrl;
9951
+ return [
9952
+ (0, import_react25.createElement)(
9953
+ "div",
9954
+ { key: "hero", className: "bt-about-hero" },
9955
+ (0, import_react25.createElement)("span", { className: "bt-about-mark" }, (0, import_react25.createElement)(BotMark, { look: MAIN_LOOK, bot: { id: "about" }, size: 52, state: markState, live: true, gaze: true, pokeable: true })),
9956
+ (0, import_react25.createElement)(
9957
+ "div",
9958
+ { className: "bt-about-copy" },
9959
+ (0, import_react25.createElement)("span", { className: "bt-about-name" }, "DS Bot"),
9960
+ update.current ? (0, import_react25.createElement)("span", { className: "bt-about-version" }, t("Version {version}", { version: update.current })) : null
9961
+ ),
9962
+ repo ? (0, import_react25.createElement)(
9963
+ "a",
9964
+ {
9965
+ className: "bt-star",
9966
+ href: repo,
9967
+ target: "_blank",
9968
+ rel: "noopener noreferrer",
9969
+ title: t("Star DS Bot on GitHub"),
9970
+ "aria-label": stars === null ? t("Star DS Bot on GitHub") : `${t("Star DS Bot on GitHub")} · ${t("{count} stars", { count: stars })}`
9971
+ },
9972
+ (0, import_react25.createElement)("span", { className: "bt-star-main" }, (0, import_react25.createElement)("span", { className: "bt-star-icon" }, (0, import_react25.createElement)(StarLineIcon)), "Star"),
9973
+ stars === null ? null : (0, import_react25.createElement)("span", { className: "bt-star-count" }, starCount(stars))
9974
+ ) : null
9975
+ ),
9976
+ repo ? (0, import_react25.createElement)("p", { key: "ask", className: "bt-about-ask" }, t("If DS Bot helps you, a star on GitHub helps more people find it.")) : null,
9977
+ (0, import_react25.createElement)(
9978
+ Section,
9979
+ { key: "updates", title: t("Updates") },
9980
+ (0, import_react25.createElement)(Row3, { label: "DS Bot", hint: botHint(update) }, botControl, update.available && update.notesUrl ? (0, import_react25.createElement)("a", linkProps(update.notesUrl), t("What's new")) : null),
9981
+ dsh?.current ? (0, import_react25.createElement)(Row3, { label: "DeepSeek Harness", hint: dshHint(dsh, dshShown) }, dshControl) : null
9982
+ ),
9983
+ (0, import_react25.createElement)(
9984
+ Section,
9985
+ { key: "links", title: t("Links") },
9986
+ repo ? (0, import_react25.createElement)(Row3, { label: t("Source code"), hint: repo.replace(/^https:\/\//, "") }, (0, import_react25.createElement)("a", linkProps(repo), (0, import_react25.createElement)(GitHubMark), t("Open"))) : null,
9987
+ update.notesUrl ? (0, import_react25.createElement)(Row3, { label: t("Release notes") }, (0, import_react25.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
9988
+ (0, import_react25.createElement)(Row3, { label: t("Report a problem") }, (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("feedback") }, t("Open")))
9989
+ )
9990
+ ];
9991
+ }
9992
+ var ABOUT_CSS = `
9993
+ .bt-about-hero{display:flex;align-items:center;gap:14px;padding:16px;border:1px solid var(--bt-line);border-radius:12px;background:var(--bt-card)}
9994
+ .bt-about-mark{flex:none;display:inline-flex;position:relative}
9995
+ .bt-about-mark::before{content:'';position:absolute;inset:-20%;border-radius:50%}
9996
+ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-state=idle]:not(.bt-mark-poke) .bt-b{rotate:8deg;translate:5% -1%;animation:bt-nuzzle .78s ease-in-out .32s infinite}
9997
+ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-state=idle] .bt-eye{scale:1 0;animation:none}
9998
+ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-state=idle] .bt-eye-happy{opacity:1;transition-delay:.1s}
9999
+ .bt-about-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
10000
+ .bt-about-name{font-size:16px;line-height:22px;font-weight:600}
10001
+ .bt-about-version{font-size:12px;line-height:16px;color:var(--bt-ink-2);font-variant-numeric:tabular-nums}
10002
+ .bt-about-ask{margin:-10px 4px 0;font-size:12px;line-height:16px;color:var(--bt-ink-3)}
10003
+ .bt-star{flex:none;display:inline-flex;align-items:stretch;height:30px;border-radius:999px;border:1px solid var(--bt-line-2,var(--bt-line));overflow:hidden;color:var(--bt-ink);text-decoration:none;font-size:12px;line-height:28px;transition:transform .12s ease,border-color .15s ease}
10004
+ .bt-star:hover{border-color:color-mix(in srgb,#e3a008 55%,var(--bt-line))}
10005
+ .bt-star:active{transform:scale(.96)}
10006
+ .bt-star-main{display:inline-flex;align-items:center;gap:6px;padding:0 12px 0 10px;background:var(--bt-hover);font-weight:500}
10007
+ .bt-star-icon{display:inline-flex;color:var(--bt-ink-2);transition:color .15s ease}
10008
+ .bt-star-icon path{transition:fill .15s ease}
10009
+ .bt-star:hover .bt-star-icon{color:#e3a008;animation:bt-star-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
10010
+ .bt-star:hover .bt-star-icon path{fill:currentColor}
10011
+ .bt-star-count{display:inline-flex;align-items:center;padding:0 11px;border-left:1px solid var(--bt-line-2,var(--bt-line));color:var(--bt-ink-2);font-variant-numeric:tabular-nums;animation:bt-star-count-in ${SPRING_POP.duration}ms ${SPRING_POP.easing},bt-veil-in .2s ease-out}
10012
+ @keyframes bt-star-pop{from{scale:.6;rotate:-30deg}}
10013
+ @keyframes bt-star-count-in{from{translate:-6px 0}}
10014
+ .bt-about-link{display:inline-flex;align-items:center;gap:6px;text-decoration:none}
10015
+ .bt-about-link svg{width:14px;height:14px}
10016
+ .bt-fb-primary:disabled{opacity:.6;cursor:default;filter:none}
10017
+ @media (prefers-reduced-motion:reduce){.bt-star:hover .bt-star-icon,.bt-star-count{animation:none}}
10018
+ `;
10019
+
10020
+ // src/client/settings.js
10021
+ var import_react26 = require("react");
9507
10022
  var capitalize2 = (word) => word[0].toUpperCase() + word.slice(1);
9508
- var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["connectors", "Connectors"], ["schedules", "Scheduled tasks"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"]];
9509
- var PAGE_ICONS = { ...SETTINGS_ICONS, connectors: ConnectorIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon };
10023
+ var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["connectors", "Connectors"], ["schedules", "Scheduled tasks"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"], ["about", "About DS Bot"]];
10024
+ var PAGE_ICONS = { ...SETTINGS_ICONS, connectors: ConnectorIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon };
9510
10025
  function SetSection({ title, children }) {
9511
- return (0, import_react25.createElement)("section", { className: "bt-set-section" }, (0, import_react25.createElement)("h3", null, title), (0, import_react25.createElement)("div", { className: "bt-set-card" }, children));
10026
+ return (0, import_react26.createElement)("section", { className: "bt-set-section" }, (0, import_react26.createElement)("h3", null, title), (0, import_react26.createElement)("div", { className: "bt-set-card" }, children));
9512
10027
  }
9513
10028
  function SetRow({ label, hint, children }) {
9514
- return (0, import_react25.createElement)(
10029
+ return (0, import_react26.createElement)(
9515
10030
  "div",
9516
10031
  { className: "bt-set-row" },
9517
- (0, import_react25.createElement)("div", { className: "bt-set-copy" }, (0, import_react25.createElement)("span", null, label), hint ? (0, import_react25.createElement)("span", { className: "bt-set-hint" }, hint) : null),
9518
- children ? (0, import_react25.createElement)("div", { className: "bt-set-control" }, children) : null
10032
+ (0, import_react26.createElement)("div", { className: "bt-set-copy" }, (0, import_react26.createElement)("span", null, label), hint ? (0, import_react26.createElement)("span", { className: "bt-set-hint" }, hint) : null),
10033
+ children ? (0, import_react26.createElement)("div", { className: "bt-set-control" }, children) : null
9519
10034
  );
9520
10035
  }
9521
10036
  function GeneralPage({ roster, actions, run }) {
9522
10037
  const prefs = roster.prefs ?? {};
9523
10038
  const themes = allThemes();
9524
10039
  const current = themes[prefs.theme] ? prefs.theme : "deepseek";
9525
- const [custom, setCustom] = (0, import_react25.useState)(prefs.accent ?? "#4d6bfe");
9526
- const timer = (0, import_react25.useRef)(0);
9527
- (0, import_react25.useEffect)(() => () => clearTimeout(timer.current), []);
10040
+ const [custom, setCustom] = (0, import_react26.useState)(prefs.accent ?? "#4d6bfe");
10041
+ const timer = (0, import_react26.useRef)(0);
10042
+ (0, import_react26.useEffect)(() => () => clearTimeout(timer.current), []);
9528
10043
  const pickCustom = (value) => {
9529
10044
  setCustom(value);
9530
10045
  clearTimeout(timer.current);
9531
10046
  timer.current = setTimeout(() => run(actions.setPrefs({ accent: value })), 350);
9532
10047
  };
9533
10048
  return [
9534
- (0, import_react25.createElement)(
10049
+ (0, import_react26.createElement)(
9535
10050
  SetSection,
9536
10051
  { key: "look", title: t("Appearance") },
9537
- (0, import_react25.createElement)(
10052
+ (0, import_react26.createElement)(
9538
10053
  SetRow,
9539
10054
  { label: t("Theme"), hint: t("Colors for the whole team, on every device.") },
9540
- (0, import_react25.createElement)(
10055
+ (0, import_react26.createElement)(
9541
10056
  "select",
9542
10057
  { className: "bt-select", "aria-label": t("Theme"), value: current, onChange: (event) => run(actions.setPrefs({ theme: event.target.value })) },
9543
- Object.entries(themes).map(([id, theme]) => (0, import_react25.createElement)("option", { key: id, value: id }, t(theme.label)))
10058
+ Object.entries(themes).map(([id, theme]) => (0, import_react26.createElement)("option", { key: id, value: id }, t(theme.label)))
9544
10059
  )
9545
10060
  ),
9546
- (0, import_react25.createElement)(
10061
+ (0, import_react26.createElement)(
9547
10062
  SetRow,
9548
10063
  { label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") },
9549
- (0, import_react25.createElement)(
10064
+ (0, import_react26.createElement)(
9550
10065
  "div",
9551
10066
  { className: "bt-swatches" },
9552
- (0, import_react25.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }),
9553
- ACCENT_INKS.map((ink) => (0, import_react25.createElement)("button", {
10067
+ (0, import_react26.createElement)("button", { type: "button", className: "bt-swatch bt-swatch-default", title: t("Theme accent"), "aria-label": t("Theme accent"), "aria-pressed": !prefs.accent, onClick: () => run(actions.setPrefs({ accent: null })) }),
10068
+ ACCENT_INKS.map((ink) => (0, import_react26.createElement)("button", {
9554
10069
  key: ink,
9555
10070
  type: "button",
9556
10071
  className: "bt-swatch",
@@ -9560,21 +10075,21 @@ function GeneralPage({ roster, actions, run }) {
9560
10075
  style: { background: INKS[ink][0] },
9561
10076
  onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] }))
9562
10077
  })),
9563
- (0, import_react25.createElement)(
10078
+ (0, import_react26.createElement)(
9564
10079
  "label",
9565
10080
  { className: "bt-swatch bt-swatch-custom", title: t("Custom accent"), "data-on": Boolean(prefs.accent) && !ACCENT_INKS.some((ink) => INKS[ink][0] === prefs.accent) },
9566
- (0, import_react25.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) })
10081
+ (0, import_react26.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) })
9567
10082
  )
9568
10083
  )
9569
10084
  )
9570
10085
  ),
9571
- (0, import_react25.createElement)(
10086
+ (0, import_react26.createElement)(
9572
10087
  SetSection,
9573
10088
  { key: "mode", title: t("Mode") },
9574
- (0, import_react25.createElement)(
10089
+ (0, import_react26.createElement)(
9575
10090
  SetRow,
9576
10091
  { label: t("Classic Agent mode") },
9577
- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.setSurface("agent") }, t("Switch"))
10092
+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.setSurface("agent") }, t("Switch"))
9578
10093
  )
9579
10094
  )
9580
10095
  ];
@@ -9583,52 +10098,52 @@ function MainBotsSection({ roster, actions, run }) {
9583
10098
  const mains = (roster.mainBotIds ?? []).map((id) => roster.byId[id]).filter(Boolean);
9584
10099
  const [first, ...others] = mains;
9585
10100
  const candidates = roster.bots.filter((bot) => !isMainOf(roster, bot.id));
9586
- return (0, import_react25.createElement)(
10101
+ return (0, import_react26.createElement)(
9587
10102
  SetSection,
9588
10103
  { title: t("Main Bot") },
9589
- (0, import_react25.createElement)(
10104
+ (0, import_react26.createElement)(
9590
10105
  SetRow,
9591
10106
  {
9592
10107
  label: t("Main Bot"),
9593
10108
  hint: first ? t("First in the sidebar. Pick another Bot to hand it the role; {name} then stops being a Main Bot.", { name: first.name }) : void 0
9594
10109
  },
9595
- (0, import_react25.createElement)("select", {
10110
+ (0, import_react26.createElement)("select", {
9596
10111
  className: "bt-select",
9597
10112
  "aria-label": t("Main Bot"),
9598
10113
  value: first?.id ?? "",
9599
10114
  onChange: (event) => run(actions.setMain(event.target.value, true, first ? { replace: first.id } : {}))
9600
- }, first ? null : (0, import_react25.createElement)("option", { value: "" }, t("Choose a Bot…")), roster.bots.map((bot) => (0, import_react25.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
10115
+ }, first ? null : (0, import_react26.createElement)("option", { value: "" }, t("Choose a Bot…")), roster.bots.map((bot) => (0, import_react26.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
9601
10116
  ),
9602
- others.map((bot) => (0, import_react25.createElement)(
10117
+ others.map((bot) => (0, import_react26.createElement)(
9603
10118
  SetRow,
9604
10119
  { key: bot.id, label: bot.name, hint: t("Also a Main Bot.") },
9605
- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")),
9606
- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role"))
10120
+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, true, { primary: true })) }, t("Move to first")),
10121
+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.setMain(bot.id, false)) }, t("Remove Main Bot role"))
9607
10122
  )),
9608
- candidates.length > 0 ? (0, import_react25.createElement)(
10123
+ candidates.length > 0 ? (0, import_react26.createElement)(
9609
10124
  SetRow,
9610
10125
  { label: t("Add a Main Bot"), hint: t("Main Bots can create and change Bots, and run every group chat.") },
9611
- (0, import_react25.createElement)("select", {
10126
+ (0, import_react26.createElement)("select", {
9612
10127
  className: "bt-select",
9613
10128
  "aria-label": t("Add a Main Bot"),
9614
10129
  value: "",
9615
10130
  onChange: (event) => {
9616
10131
  if (event.target.value) void run(actions.setMain(event.target.value, true));
9617
10132
  }
9618
- }, (0, import_react25.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react25.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
10133
+ }, (0, import_react26.createElement)("option", { value: "" }, t("Choose a Bot…")), candidates.map((bot) => (0, import_react26.createElement)("option", { key: bot.id, value: bot.id }, bot.name)))
9619
10134
  ) : null
9620
10135
  );
9621
10136
  }
9622
10137
  var MOTIONS = [["quiet", "Quiet"], ["normal", "Normal"], ["lively", "Lively"]];
9623
10138
  function MotionSection({ roster, actions, run }) {
9624
10139
  const motion = roster.prefs?.motion === "quiet" || roster.prefs?.motion === "lively" ? roster.prefs.motion : "normal";
9625
- return (0, import_react25.createElement)(
10140
+ return (0, import_react26.createElement)(
9626
10141
  SetSection,
9627
10142
  { title: t("Animation") },
9628
- (0, import_react25.createElement)(
10143
+ (0, import_react26.createElement)(
9629
10144
  SetRow,
9630
10145
  { label: t("Bot animation") },
9631
- (0, import_react25.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react25.createElement)("button", {
10146
+ (0, import_react26.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react26.createElement)("button", {
9632
10147
  key: id,
9633
10148
  type: "button",
9634
10149
  "aria-pressed": motion === id,
@@ -9640,69 +10155,69 @@ function MotionSection({ roster, actions, run }) {
9640
10155
  );
9641
10156
  }
9642
10157
  function BotsPage({ roster, actions, run }) {
9643
- const [creating, setCreating] = (0, import_react25.useState)(false);
10158
+ const [creating, setCreating] = (0, import_react26.useState)(false);
9644
10159
  return [
9645
- (0, import_react25.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
9646
- (0, import_react25.createElement)(MotionSection, { key: "motion", roster, actions, run }),
9647
- creating ? (0, import_react25.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react25.createElement)(
10160
+ (0, import_react26.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
10161
+ (0, import_react26.createElement)(MotionSection, { key: "motion", roster, actions, run }),
10162
+ creating ? (0, import_react26.createElement)(CreateBot, { key: "create", roster, actions, inline: true, onBack: () => setCreating(false), onCreated: (bot) => actions.openSettings("bots", bot.id) }) : (0, import_react26.createElement)(
9648
10163
  "div",
9649
10164
  { key: "new", className: "bt-actions" },
9650
- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react25.createElement)(PlusIcon), t("New Bot"))
10165
+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setCreating(true) }, (0, import_react26.createElement)(PlusIcon), t("New Bot"))
9651
10166
  ),
9652
- (0, import_react25.createElement)(
10167
+ (0, import_react26.createElement)(
9653
10168
  SetSection,
9654
10169
  { key: "bots", title: t("Bots · {count}", { count: roster.bots.length }) },
9655
- roster.bots.map((bot) => (0, import_react25.createElement)(
10170
+ roster.bots.map((bot) => (0, import_react26.createElement)(
9656
10171
  "button",
9657
10172
  { key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) },
9658
- (0, import_react25.createElement)(
10173
+ (0, import_react26.createElement)(
9659
10174
  "span",
9660
10175
  { className: "bt-set-bot" },
9661
- (0, import_react25.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
9662
- (0, import_react25.createElement)("span", { className: "bt-row-title" }, bot.name),
9663
- bot.role ? (0, import_react25.createElement)("span", { className: "bt-tag" }, bot.role) : null,
9664
- isMainOf(roster, bot.id) ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
9665
- bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
10176
+ (0, import_react26.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
10177
+ (0, import_react26.createElement)("span", { className: "bt-row-title" }, bot.name),
10178
+ bot.role ? (0, import_react26.createElement)("span", { className: "bt-tag" }, bot.role) : null,
10179
+ isMainOf(roster, bot.id) ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
10180
+ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
9666
10181
  ),
9667
- (0, import_react25.createElement)("span", { className: "bt-chevron" }, (0, import_react25.createElement)(ChevronRightIcon))
10182
+ (0, import_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.createElement)(ChevronRightIcon))
9668
10183
  ))
9669
10184
  )
9670
10185
  ];
9671
10186
  }
9672
10187
  function GroupsPage({ roster, actions, run }) {
9673
- const [creating, setCreating] = (0, import_react25.useState)(false);
10188
+ const [creating, setCreating] = (0, import_react26.useState)(false);
9674
10189
  return [
9675
- creating ? (0, import_react25.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react25.createElement)(
10190
+ creating ? (0, import_react26.createElement)(NewGroupForm, { key: "create", roster, actions, run, onCancel: () => setCreating(false), onCreated: (room2) => actions.openSettings("groups", room2.id) }) : (0, import_react26.createElement)(
9676
10191
  "div",
9677
10192
  { key: "new", className: "bt-actions" },
9678
- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react25.createElement)(PlusIcon), t("Create group chat"))
10193
+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", disabled: roster.bots.length < 2, title: roster.bots.length < 2 ? t("A group chat needs at least two Bots") : void 0, onClick: () => setCreating(true) }, (0, import_react26.createElement)(PlusIcon), t("Create group chat"))
9679
10194
  ),
9680
- (0, import_react25.createElement)(
10195
+ (0, import_react26.createElement)(
9681
10196
  SetSection,
9682
10197
  { key: "groups", title: t("Group chats · {count}", { count: roster.rooms.length }) },
9683
- roster.rooms.length === 0 ? (0, import_react25.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => {
10198
+ roster.rooms.length === 0 ? (0, import_react26.createElement)(SetRow, { label: t("No group chats yet."), hint: t("A group chat lets several Bots work on one thing with you.") }) : roster.rooms.map((room2) => {
9684
10199
  const admin = roster.byId[room2.admin];
9685
- return (0, import_react25.createElement)(
10200
+ return (0, import_react26.createElement)(
9686
10201
  "button",
9687
10202
  { key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) },
9688
- (0, import_react25.createElement)(
10203
+ (0, import_react26.createElement)(
9689
10204
  "span",
9690
10205
  { className: "bt-set-bot" },
9691
- (0, import_react25.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
9692
- (0, import_react25.createElement)("span", { className: "bt-row-title" }, room2.name),
9693
- (0, import_react25.createElement)("span", { className: "bt-tag" }, t("{count} members", { count: room2.members.length })),
9694
- admin ? (0, import_react25.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react25.createElement)(AdminStar), admin.name) : null,
9695
- room2.hidden ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
10206
+ (0, import_react26.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
10207
+ (0, import_react26.createElement)("span", { className: "bt-row-title" }, room2.name),
10208
+ (0, import_react26.createElement)("span", { className: "bt-tag" }, t("{count} members", { count: room2.members.length })),
10209
+ admin ? (0, import_react26.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react26.createElement)(AdminStar), admin.name) : null,
10210
+ room2.hidden ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
9696
10211
  ),
9697
- (0, import_react25.createElement)("span", { className: "bt-chevron" }, (0, import_react25.createElement)(ChevronRightIcon))
10212
+ (0, import_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.createElement)(ChevronRightIcon))
9698
10213
  );
9699
10214
  })
9700
10215
  )
9701
10216
  ];
9702
10217
  }
9703
- function SettingsDialog({ page, id, roster, actions, leaving = false }) {
9704
- const [error, setError] = (0, import_react25.useState)("");
9705
- const close = (0, import_react25.useCallback)(() => actions.closeSettings(), []);
10218
+ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) {
10219
+ const [error, setError] = (0, import_react26.useState)("");
10220
+ const close = (0, import_react26.useCallback)(() => actions.closeSettings(), []);
9706
10221
  useEscape(close);
9707
10222
  const run = (promise) => Promise.resolve(promise).then(() => {
9708
10223
  setError("");
@@ -9720,32 +10235,32 @@ function SettingsDialog({ page, id, roster, actions, leaving = false }) {
9720
10235
  const nav = useGlide(shown, { axis: "y" });
9721
10236
  const pageKey = `${shown}:${bot?.id ?? room2?.id ?? ""}`;
9722
10237
  const depth = bot || room2 ? 1 : 0;
9723
- const last = (0, import_react25.useRef)({ key: pageKey, depth, move: "up" });
10238
+ const last = (0, import_react26.useRef)({ key: pageKey, depth, move: "up" });
9724
10239
  if (last.current.key !== pageKey) {
9725
10240
  last.current = { key: pageKey, depth, move: depth > last.current.depth ? "in" : depth < last.current.depth ? "out" : "up" };
9726
10241
  }
9727
- const [usageOpened, setUsageOpened] = (0, import_react25.useState)(shown === "usage");
10242
+ const [usageOpened, setUsageOpened] = (0, import_react26.useState)(shown === "usage");
9728
10243
  if (shown === "usage" && !usageOpened) setUsageOpened(true);
9729
- const errorLine = error ? (0, import_react25.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
9730
- return (0, import_react25.createElement)(
10244
+ const errorLine = error ? (0, import_react26.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
10245
+ return (0, import_react26.createElement)(
9731
10246
  "div",
9732
10247
  { className: "bt-settings-layer", "data-leaving": leaving || void 0 },
9733
- (0, import_react25.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
9734
- (0, import_react25.createElement)(
10248
+ (0, import_react26.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
10249
+ (0, import_react26.createElement)(
9735
10250
  "div",
9736
10251
  { className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("DS Bot settings"), "data-wide": shown === "usage" ? "" : void 0 },
9737
- (0, import_react25.createElement)(
10252
+ (0, import_react26.createElement)(
9738
10253
  "nav",
9739
10254
  { ref: nav.box, className: "bt-settings-nav", "aria-label": t("Settings pages") },
9740
- (0, import_react25.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
9741
- SETTINGS_PAGES.map(([key, text]) => (0, import_react25.createElement)("button", {
10255
+ (0, import_react26.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
10256
+ SETTINGS_PAGES.map(([key, text]) => (0, import_react26.createElement)("button", {
9742
10257
  key,
9743
10258
  type: "button",
9744
10259
  className: "bt-settings-item",
9745
10260
  "aria-current": shown === key ? "page" : void 0,
9746
10261
  onClick: () => actions.openSettings(key)
9747
- }, (0, import_react25.createElement)(PAGE_ICONS[key]), t(text))),
9748
- (0, import_react25.createElement)("button", {
10262
+ }, (0, import_react26.createElement)(PAGE_ICONS[key]), t(text))),
10263
+ (0, import_react26.createElement)("button", {
9749
10264
  type: "button",
9750
10265
  className: "bt-settings-item bt-settings-shell",
9751
10266
  title: t("Models, providers, and the rest of DeepSeek Harness"),
@@ -9753,62 +10268,63 @@ function SettingsDialog({ page, id, roster, actions, leaving = false }) {
9753
10268
  close();
9754
10269
  openHarnessSettings();
9755
10270
  }
9756
- }, (0, import_react25.createElement)(SlidersIcon), t("Harness settings"))
10271
+ }, (0, import_react26.createElement)(SlidersIcon), t("Harness settings"))
9757
10272
  ),
9758
- (0, import_react25.createElement)(
10273
+ (0, import_react26.createElement)(
9759
10274
  "div",
9760
10275
  { className: "bt-settings-main" },
9761
- (0, import_react25.createElement)(
10276
+ (0, import_react26.createElement)(
9762
10277
  "div",
9763
10278
  { className: "bt-settings-head" },
9764
- back ? (0, import_react25.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react25.createElement)(ChevronLeftIcon)) : null,
9765
- (0, import_react25.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label)
10279
+ back ? (0, import_react26.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": back, title: back, onClick: () => actions.openSettings(shown) }, (0, import_react26.createElement)(ChevronLeftIcon)) : null,
10280
+ (0, import_react26.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label)
9766
10281
  ),
9767
- shown === "usage" ? null : (0, import_react25.createElement)(
10282
+ shown === "usage" ? null : (0, import_react26.createElement)(
9768
10283
  "div",
9769
10284
  { key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move },
9770
10285
  errorLine,
9771
- shown === "general" ? (0, import_react25.createElement)(GeneralPage, { roster, actions, run }) : null,
9772
- bot ? (0, import_react25.createElement)("div", { className: "bt-settings-form" }, (0, import_react25.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
9773
- shown === "bots" && !bot ? (0, import_react25.createElement)(BotsPage, { roster, actions, run }) : null,
9774
- room2 ? (0, import_react25.createElement)("div", { className: "bt-settings-form" }, (0, import_react25.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
9775
- shown === "groups" && !room2 ? (0, import_react25.createElement)(GroupsPage, { roster, actions, run }) : null,
9776
- shown === "connectors" ? (0, import_react25.createElement)(ConnectorsPage, { roster, actions }) : null,
9777
- shown === "schedules" ? (0, import_react25.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
9778
- shown === "secrets" ? (0, import_react25.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
9779
- shown === "feedback" ? (0, import_react25.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null
10286
+ shown === "general" ? (0, import_react26.createElement)(GeneralPage, { roster, actions, run }) : null,
10287
+ bot ? (0, import_react26.createElement)("div", { className: "bt-settings-form" }, (0, import_react26.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
10288
+ shown === "bots" && !bot ? (0, import_react26.createElement)(BotsPage, { roster, actions, run }) : null,
10289
+ room2 ? (0, import_react26.createElement)("div", { className: "bt-settings-form" }, (0, import_react26.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
10290
+ shown === "groups" && !room2 ? (0, import_react26.createElement)(GroupsPage, { roster, actions, run }) : null,
10291
+ shown === "connectors" ? (0, import_react26.createElement)(ConnectorsPage, { roster, actions }) : null,
10292
+ shown === "schedules" ? (0, import_react26.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
10293
+ shown === "secrets" ? (0, import_react26.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
10294
+ shown === "feedback" ? (0, import_react26.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
10295
+ shown === "about" ? (0, import_react26.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null
9780
10296
  ),
9781
- usageOpened ? (0, import_react25.createElement)(
10297
+ usageOpened ? (0, import_react26.createElement)(
9782
10298
  "div",
9783
10299
  { key: "usage", className: "bt-settings-scroll bt-settings-usage", "data-off": shown === "usage" ? void 0 : "", "aria-hidden": shown === "usage" ? void 0 : true },
9784
10300
  shown === "usage" ? errorLine : null,
9785
- (0, import_react25.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
10301
+ (0, import_react26.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
9786
10302
  ) : null
9787
10303
  ),
9788
- (0, import_react25.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react25.createElement)(CloseIcon))
10304
+ (0, import_react26.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react26.createElement)(CloseIcon))
9789
10305
  )
9790
10306
  );
9791
10307
  }
9792
10308
 
9793
10309
  // src/client/context-menu.js
9794
- var import_react26 = require("react");
10310
+ var import_react27 = require("react");
9795
10311
  function ContextMenu({ menu, roster, actions, leaving = false }) {
9796
10312
  useEscape(() => actions.closeMenu());
9797
- (0, import_react26.useEffect)(() => {
10313
+ (0, import_react27.useEffect)(() => {
9798
10314
  const onDown = (event) => {
9799
10315
  if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu();
9800
10316
  };
9801
10317
  window.addEventListener("mousedown", onDown);
9802
10318
  return () => window.removeEventListener("mousedown", onDown);
9803
10319
  }, []);
9804
- const ref = (0, import_react26.useRef)(null);
9805
- const [place, setPlace] = (0, import_react26.useState)({ left: menu.x, top: menu.y });
9806
- (0, import_react26.useLayoutEffect)(() => {
10320
+ const ref = (0, import_react27.useRef)(null);
10321
+ const [place, setPlace] = (0, import_react27.useState)({ left: menu.x, top: menu.y });
10322
+ (0, import_react27.useLayoutEffect)(() => {
9807
10323
  const box = ref.current?.getBoundingClientRect();
9808
10324
  if (!box) return;
9809
10325
  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)) });
9810
10326
  }, [menu.x, menu.y]);
9811
- if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react26.createElement)(AgentMenu, { menu, roster, actions, leaving });
10327
+ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react27.createElement)(AgentMenu, { menu, roster, actions, leaving });
9812
10328
  const bot = roster.byId[menu.id];
9813
10329
  const room2 = roster.roomsById[menu.id];
9814
10330
  const entry = bot ?? room2;
@@ -9839,14 +10355,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
9839
10355
  isMainBot ? null : [room2 ? t("Delete group") : t("Delete"), TrashIcon, () => bot ? actions.deleteBot(bot.id) : actions.deleteRoom(menu.id), true]
9840
10356
  ]
9841
10357
  ].map((group) => group.filter(Boolean)).filter((group) => group.length > 0);
9842
- return (0, import_react26.createElement)(
10358
+ return (0, import_react27.createElement)(
9843
10359
  "div",
9844
10360
  { ref, className: "bt-menu", role: "menu", "aria-label": t("{name} actions", { name: bot?.name ?? room2.name }), style: place, "data-leaving": leaving || void 0 },
9845
- groups.map((group, index) => (0, import_react26.createElement)(
9846
- import_react26.Fragment,
10361
+ groups.map((group, index) => (0, import_react27.createElement)(
10362
+ import_react27.Fragment,
9847
10363
  { key: index },
9848
- index > 0 ? (0, import_react26.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
9849
- group.map(([label, Icon, run, danger]) => (0, import_react26.createElement)("button", {
10364
+ index > 0 ? (0, import_react27.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
10365
+ group.map(([label, Icon, run, danger]) => (0, import_react27.createElement)("button", {
9850
10366
  key: label,
9851
10367
  type: "button",
9852
10368
  role: "menuitem",
@@ -9855,14 +10371,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
9855
10371
  actions.closeMenu();
9856
10372
  void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
9857
10373
  }
9858
- }, (0, import_react26.createElement)(Icon), label))
10374
+ }, (0, import_react27.createElement)(Icon), label))
9859
10375
  ))
9860
10376
  );
9861
10377
  }
9862
10378
  function AgentMenu({ menu, roster, actions, leaving }) {
9863
- const ref = (0, import_react26.useRef)(null);
9864
- const [place, setPlace] = (0, import_react26.useState)({ left: menu.x, top: menu.y });
9865
- (0, import_react26.useLayoutEffect)(() => {
10379
+ const ref = (0, import_react27.useRef)(null);
10380
+ const [place, setPlace] = (0, import_react27.useState)({ left: menu.x, top: menu.y });
10381
+ (0, import_react27.useLayoutEffect)(() => {
9866
10382
  const box = ref.current?.getBoundingClientRect();
9867
10383
  if (!box) return;
9868
10384
  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)) });
@@ -9884,14 +10400,14 @@ function AgentMenu({ menu, roster, actions, leaving }) {
9884
10400
  [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true]
9885
10401
  ]
9886
10402
  ];
9887
- return (0, import_react26.createElement)(
10403
+ return (0, import_react27.createElement)(
9888
10404
  "div",
9889
10405
  { ref, className: "bt-menu", role: "menu", "aria-label": menu.kind === "agent" ? "DSH Agent" : t("Session actions"), style: place, "data-leaving": leaving || void 0 },
9890
- groups.map((group, index) => (0, import_react26.createElement)(
9891
- import_react26.Fragment,
10406
+ groups.map((group, index) => (0, import_react27.createElement)(
10407
+ import_react27.Fragment,
9892
10408
  { key: index },
9893
- index > 0 ? (0, import_react26.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
9894
- group.map(([label, Icon, run, danger]) => (0, import_react26.createElement)("button", {
10409
+ index > 0 ? (0, import_react27.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
10410
+ group.map(([label, Icon, run, danger]) => (0, import_react27.createElement)("button", {
9895
10411
  key: label,
9896
10412
  type: "button",
9897
10413
  role: "menuitem",
@@ -9900,7 +10416,7 @@ function AgentMenu({ menu, roster, actions, leaving }) {
9900
10416
  actions.closeMenu();
9901
10417
  void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error));
9902
10418
  }
9903
- }, (0, import_react26.createElement)(Icon), label))
10419
+ }, (0, import_react27.createElement)(Icon), label))
9904
10420
  ))
9905
10421
  );
9906
10422
  }
@@ -9970,8 +10486,142 @@ function migrateStorage(storage = globalThis.localStorage) {
9970
10486
  }
9971
10487
  }
9972
10488
 
10489
+ // src/client/member-status.js
10490
+ var import_react28 = require("react");
10491
+ var NO_WORK = { bots: {}, approvals: [] };
10492
+ function memberWork(room2, work = NO_WORK) {
10493
+ return room2.members.map((id) => {
10494
+ const approvals = (work.approvals ?? []).filter((entry) => entry.botId === id);
10495
+ const doing = work.bots?.[id];
10496
+ return { id, approvals, doing, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" };
10497
+ });
10498
+ }
10499
+ function busyMembers(room2, work, roster) {
10500
+ const busy = memberWork(room2, work).filter((entry) => entry.state !== "idle" && roster.byId[entry.id]);
10501
+ return [...busy.filter((entry) => entry.state === "ask"), ...busy.filter((entry) => entry.state === "work")].map((entry) => ({ ...entry, bot: roster.byId[entry.id] }));
10502
+ }
10503
+ function summaryText(busy) {
10504
+ const asks = busy.filter((entry) => entry.state === "ask");
10505
+ const workers = busy.filter((entry) => entry.state === "work");
10506
+ const names = asks.slice(0, 2).map((entry) => entry.bot.name);
10507
+ const waiting = asks.length === 0 ? "" : asks.length === 1 ? t("{name} is waiting for your approval", { name: names[0] }) : asks.length === 2 ? t("{first} and {second} are waiting for your approval", { first: names[0], second: names[1] }) : t("{first}, {second} and {count} more are waiting for your approval", { first: names[0], second: names[1], count: asks.length - 2, total: asks.length });
10508
+ const first = workers[0];
10509
+ const doing = first ? `${first.bot.name} · ${workPhrase(first.doing.tool)}…${workers.length > 1 ? ` +${workers.length - 1}` : ""}` : "";
10510
+ return { waiting, doing };
10511
+ }
10512
+ function Face({ entry, size }) {
10513
+ return (0, import_react28.createElement)(
10514
+ "span",
10515
+ { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 },
10516
+ (0, import_react28.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") })
10517
+ );
10518
+ }
10519
+ function MemberDock({ sessionId, useRoster, useWork, actions }) {
10520
+ const roster = useRoster((value) => value);
10521
+ const work = useWork((value) => value);
10522
+ const room2 = roster.roomsById[sessionId];
10523
+ if (!room2) return null;
10524
+ const busy = busyMembers(room2, work, roster);
10525
+ if (busy.length === 0) return null;
10526
+ const { waiting, doing } = summaryText(busy);
10527
+ return (0, import_react28.createElement)(
10528
+ "div",
10529
+ { className: "bt-event bt-mw-dock" },
10530
+ (0, import_react28.createElement)(
10531
+ "button",
10532
+ { type: "button", className: "bt-event-whole bt-mw-line", "aria-label": [waiting, doing].filter(Boolean).join(" · "), onClick: () => actions.showDetails(sessionId) },
10533
+ (0, import_react28.createElement)("span", { className: "bt-mw-stack", "aria-hidden": true }, busy.slice(0, 4).map((entry) => (0, import_react28.createElement)(Face, { key: entry.id, entry, size: 16 }))),
10534
+ waiting ? (0, import_react28.createElement)("span", { className: "bt-mw-ask" }, waiting) : null,
10535
+ waiting && doing ? (0, import_react28.createElement)("span", { "aria-hidden": true }, "·") : null,
10536
+ doing ? (0, import_react28.createElement)("span", { className: "bt-event-name" }, (0, import_react28.createElement)(Shimmer, null, doing)) : null
10537
+ )
10538
+ );
10539
+ }
10540
+ function useNow2(active) {
10541
+ const [now, setNow] = (0, import_react28.useState)(Date.now);
10542
+ (0, import_react28.useEffect)(() => {
10543
+ if (!active) return void 0;
10544
+ const timer = setInterval(() => setNow(Date.now()), 3e4);
10545
+ return () => clearInterval(timer);
10546
+ }, [active]);
10547
+ return now;
10548
+ }
10549
+ var since = (now, at) => typeof at === "number" && now - at >= 6e4 ? ` · ${elapsedLabel(now - at)}` : "";
10550
+ function ApprovalCard({ approval, bot, actions }) {
10551
+ const [busy, setBusy] = (0, import_react28.useState)(false);
10552
+ const answer = (choice) => {
10553
+ setBusy(true);
10554
+ void Promise.resolve(actions.answerApproval(approval.id, choice)).catch((error) => actions.reportError(error, "approval")).finally(() => setBusy(false));
10555
+ };
10556
+ return (0, import_react28.createElement)(
10557
+ "div",
10558
+ { className: "bt-mw-card", role: "group", "aria-label": t("{name} is waiting for your approval", { name: bot.name }) },
10559
+ (0, import_react28.createElement)("div", { className: "bt-mw-tool" }, approval.tool),
10560
+ approval.detail?.text ? (0, import_react28.createElement)("div", { className: "bt-mw-text" }, approval.detail.text) : null,
10561
+ approval.reason ? (0, import_react28.createElement)("div", { className: "bt-mw-reason" }, approval.reason) : null,
10562
+ approval.detail?.code ? (0, import_react28.createElement)("code", { className: "bt-mw-code", title: approval.detail.code }, approval.detail.code) : null,
10563
+ approval.answerable ? (0, import_react28.createElement)(
10564
+ "div",
10565
+ { className: "bt-mw-actions" },
10566
+ (0, import_react28.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => answer("reject") }, t("Reject")),
10567
+ (0, import_react28.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: () => answer("allow") }, t("Allow once"))
10568
+ ) : (0, import_react28.createElement)(
10569
+ "div",
10570
+ { className: "bt-mw-actions" },
10571
+ (0, import_react28.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name }))
10572
+ )
10573
+ );
10574
+ }
10575
+ function MemberWork({ entry, bot, room: room2, roster, actions, now }) {
10576
+ if (entry.state === "ask") {
10577
+ return (0, import_react28.createElement)(
10578
+ "div",
10579
+ { className: "bt-mw-member" },
10580
+ (0, import_react28.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`),
10581
+ entry.approvals.map((approval) => (0, import_react28.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions }))
10582
+ );
10583
+ }
10584
+ if (entry.state !== "work") return null;
10585
+ 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");
10586
+ return (0, import_react28.createElement)(
10587
+ "div",
10588
+ { className: "bt-mw-member" },
10589
+ (0, import_react28.createElement)("div", { className: "bt-mw-state" }, (0, import_react28.createElement)(Shimmer, null, `${workPhrase(entry.doing.tool)}…`), where ? ` ${where}` : "", since(now, entry.doing.since))
10590
+ );
10591
+ }
10592
+ function useMemberWork(room2, useWork) {
10593
+ const work = useWork?.((value) => value) ?? NO_WORK;
10594
+ const entries2 = room2 ? memberWork(room2, work) : [];
10595
+ const now = useNow2(entries2.some((entry) => entry.state !== "idle"));
10596
+ return { entries: entries2, now };
10597
+ }
10598
+ var MEMBER_CSS = `
10599
+ .bt-event.bt-mw-dock{margin:0 0 6px;pointer-events:none}
10600
+ .bt-mw-dock>button{pointer-events:auto;animation:bt-mw-in .32s cubic-bezier(.23,1,.32,1) backwards}
10601
+ .bt-mw-line{gap:6px!important}
10602
+ .bt-mw-stack{display:inline-flex;align-items:center;flex:none;line-height:0}
10603
+ .bt-mw-stack>*+*{margin-left:-4px}
10604
+ .bt-mw-face{position:relative;display:inline-flex;flex:none;border-radius:50%}
10605
+ .bt-mw-stack .bt-mw-face{box-shadow:0 0 0 1.5px var(--bt-main,#fff)}
10606
+ .bt-mw-face[data-ask]::after{content:"";position:absolute;right:-2px;bottom:-2px;width:7px;height:7px;border-radius:50%;background:#e3a008;box-shadow:0 0 0 1.5px var(--bt-main,#fff)}
10607
+ .bt-mw-ask{color:#b7791f;flex:none;white-space:nowrap}
10608
+ body[data-ds-dark-theme] .bt-mw-ask{color:#f0b84a}
10609
+ .bt-mw-member{padding:0 6px 8px 42px;display:flex;flex-direction:column;gap:6px}
10610
+ .bt-mw-state{font-size:12px;line-height:16px;color:var(--bt-ink-2);margin-top:-4px}
10611
+ .bt-mw-card{display:flex;flex-direction:column;gap:4px;padding:10px 12px;border-radius:12px;background:var(--bt-hover);font-size:12px;line-height:17px;color:var(--bt-ink-2)}
10612
+ .bt-mw-tool{font-weight:600;color:var(--bt-ink)}
10613
+ .bt-mw-text{color:var(--bt-ink);overflow-wrap:anywhere}
10614
+ .bt-mw-reason{overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
10615
+ .bt-mw-code{display:block;font:11px/16px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--bt-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
10616
+ .bt-mw-actions{display:flex;justify-content:flex-end;gap:6px;margin-top:4px}
10617
+ .bt-mw-actions>button{height:28px;padding:0 12px;font-size:12px}
10618
+ .bt-mw-actions>button:disabled{opacity:.5;cursor:default}
10619
+ @keyframes bt-mw-in{from{opacity:0;transform:translateY(4px)}}
10620
+ @media (prefers-reduced-motion:reduce){.bt-mw-dock>button{animation:none}}
10621
+ `;
10622
+
9973
10623
  // src/client/details-drawer.js
9974
- var import_react27 = require("react");
10624
+ var import_react29 = require("react");
9975
10625
  function transcriptOf(list, id, name, hiddenTurns = []) {
9976
10626
  const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline;
9977
10627
  if (!Array.isArray(outline)) return "";
@@ -9996,10 +10646,10 @@ var EMPTY_TABS = { ids: [], active: null };
9996
10646
  var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" };
9997
10647
  var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]);
9998
10648
  function PermissionRow({ bot, roster, actions, run }) {
9999
- const [pending, setPending] = (0, import_react27.useState)(null);
10649
+ const [pending, setPending] = (0, import_react29.useState)(null);
10000
10650
  const catalog = roster.permissions;
10001
10651
  const saved = bot.permission ?? catalog?.defaultPreset;
10002
- (0, import_react27.useEffect)(() => {
10652
+ (0, import_react29.useEffect)(() => {
10003
10653
  if (pending === saved) setPending(null);
10004
10654
  }, [pending, saved]);
10005
10655
  if (!catalog?.options?.length) return null;
@@ -10009,14 +10659,14 @@ function PermissionRow({ bot, roster, actions, run }) {
10009
10659
  setPending(value);
10010
10660
  void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null));
10011
10661
  };
10012
- return (0, import_react27.createElement)(
10662
+ return (0, import_react29.createElement)(
10013
10663
  "div",
10014
10664
  { className: "bt-drawer-permission" },
10015
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
10016
- (0, import_react27.createElement)(
10665
+ (0, import_react29.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
10666
+ (0, import_react29.createElement)(
10017
10667
  Segmented,
10018
10668
  { label: t("Permissions"), value: current },
10019
- catalog.options.map((option) => (0, import_react27.createElement)("button", {
10669
+ catalog.options.map((option) => (0, import_react29.createElement)("button", {
10020
10670
  key: option.value,
10021
10671
  type: "button",
10022
10672
  "aria-pressed": option.value === current,
@@ -10030,42 +10680,42 @@ function PermissionRow({ bot, roster, actions, run }) {
10030
10680
  function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) {
10031
10681
  const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS);
10032
10682
  const pages = useBrowserPages((map) => map);
10033
- (0, import_react27.useEffect)(() => {
10683
+ (0, import_react29.useEffect)(() => {
10034
10684
  actions.browserEnsure?.(bot.id);
10035
10685
  }, [bot.id]);
10036
10686
  if (roster.browser !== true || !actions.browserShow) {
10037
- return (0, import_react27.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app."));
10687
+ return (0, import_react29.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app."));
10038
10688
  }
10039
10689
  if (state.ids.length === 0) {
10040
- return (0, import_react27.createElement)(
10690
+ return (0, import_react29.createElement)(
10041
10691
  "div",
10042
10692
  { className: "bt-browser-empty-mark" },
10043
- (0, import_react27.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
10044
- (0, import_react27.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
10045
- (0, import_react27.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })),
10046
- (0, import_react27.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
10693
+ (0, import_react29.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
10694
+ (0, import_react29.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
10695
+ (0, import_react29.createElement)("div", { className: "bt-muted" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name })),
10696
+ (0, import_react29.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
10047
10697
  );
10048
10698
  }
10049
- return (0, import_react27.createElement)(
10699
+ return (0, import_react29.createElement)(
10050
10700
  "div",
10051
10701
  { className: "bt-bcards" },
10052
10702
  state.ids.map((tabId, index) => {
10053
10703
  const page = pages[tabId] ?? {};
10054
10704
  const label = page.title || shortUrl(page.url) || t("New tab");
10055
- return (0, import_react27.createElement)(
10705
+ return (0, import_react29.createElement)(
10056
10706
  "div",
10057
10707
  { key: tabId, className: "bt-bcard", "data-active": state.active === tabId || void 0, style: { animationDelay: `${index * 30}ms` } },
10058
- (0, import_react27.createElement)(
10708
+ (0, import_react29.createElement)(
10059
10709
  "button",
10060
10710
  { type: "button", className: "bt-bcard-shot", "aria-label": t("Open {title}", { title: label }), title: page.url, onClick: () => actions.browserPick(sessionId, tabId) },
10061
- page.excerpt ? (0, import_react27.createElement)(
10711
+ page.excerpt ? (0, import_react29.createElement)(
10062
10712
  "span",
10063
10713
  { className: "bt-bcard-preview" },
10064
- (0, import_react27.createElement)("span", { className: "bt-bcard-ptitle" }, label),
10065
- (0, import_react27.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
10066
- ) : (0, import_react27.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react27.createElement)(GlobeIcon), (0, import_react27.createElement)("span", null, label))
10714
+ (0, import_react29.createElement)("span", { className: "bt-bcard-ptitle" }, label),
10715
+ (0, import_react29.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
10716
+ ) : (0, import_react29.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react29.createElement)(GlobeIcon), (0, import_react29.createElement)("span", null, label))
10067
10717
  ),
10068
- (0, import_react27.createElement)("button", {
10718
+ (0, import_react29.createElement)("button", {
10069
10719
  type: "button",
10070
10720
  className: "bt-bcard-x",
10071
10721
  "aria-label": t("Close tab"),
@@ -10074,78 +10724,87 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow
10074
10724
  void actions.browserCloseTab(bot.id, tabId);
10075
10725
  }
10076
10726
  }, "×"),
10077
- (0, import_react27.createElement)(
10727
+ (0, import_react29.createElement)(
10078
10728
  "div",
10079
10729
  { className: "bt-bcard-row" },
10080
- (0, import_react27.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
10081
- (0, import_react27.createElement)("span", { className: "bt-bcard-title" }, label),
10082
- page.url ? (0, import_react27.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null
10730
+ (0, import_react29.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
10731
+ (0, import_react29.createElement)("span", { className: "bt-bcard-title" }, label),
10732
+ page.url ? (0, import_react29.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null
10083
10733
  )
10084
10734
  );
10085
10735
  }),
10086
- (0, import_react27.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react27.createElement)(PlusIcon), t("New tab"))
10736
+ (0, import_react29.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react29.createElement)(PlusIcon), t("New tab"))
10087
10737
  );
10088
10738
  }
10089
10739
  function About({ text, title = "About" }) {
10090
- const [open, setOpen] = (0, import_react27.useState)(false);
10740
+ const [open, setOpen] = (0, import_react29.useState)(false);
10091
10741
  const long = text.split("\n").length > 5 || text.length > 240;
10092
- const body = (0, import_react27.useRef)(null);
10742
+ const body = (0, import_react29.useRef)(null);
10093
10743
  useMorph(body, open);
10094
- return (0, import_react27.createElement)(
10744
+ return (0, import_react29.createElement)(
10095
10745
  "div",
10096
10746
  null,
10097
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t(title)),
10098
- (0, import_react27.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text),
10099
- long ? (0, import_react27.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null
10747
+ (0, import_react29.createElement)("div", { className: "bt-section-title" }, t(title)),
10748
+ (0, import_react29.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text),
10749
+ long ? (0, import_react29.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null
10100
10750
  );
10101
10751
  }
10102
- function GroupOverview({ room: room2, roster, actions, usage, openSettings }) {
10752
+ function GroupOverview({ room: room2, roster, actions, usage, useWork, openSettings }) {
10103
10753
  const members = room2.members.map((id) => roster.byId[id]).filter(Boolean);
10754
+ const { entries: entries2, now } = useMemberWork(room2, useWork);
10104
10755
  return [
10105
- room2.notice ? (0, import_react27.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react27.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
10106
- (0, import_react27.createElement)(
10756
+ room2.notice ? (0, import_react29.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react29.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
10757
+ (0, import_react29.createElement)(
10107
10758
  "div",
10108
10759
  { key: "members" },
10109
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length })),
10110
- (0, import_react27.createElement)("div", { className: "bt-members", role: "list" }, members.map((member) => (0, import_react27.createElement)(
10111
- "div",
10112
- { key: member.id, className: "bt-member", role: "listitem" },
10113
- (0, import_react27.createElement)(
10114
- "button",
10115
- { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) },
10116
- (0, import_react27.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
10117
- (0, import_react27.createElement)("span", null, member.name)
10118
- ),
10119
- room2.admin === member.id ? (0, import_react27.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
10120
- )))
10760
+ (0, import_react29.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length })),
10761
+ (0, import_react29.createElement)("div", { className: "bt-members", role: "list" }, members.map((member) => {
10762
+ const entry = entries2.find((item) => item.id === member.id);
10763
+ return (0, import_react29.createElement)(
10764
+ "div",
10765
+ { key: member.id, role: "listitem" },
10766
+ (0, import_react29.createElement)(
10767
+ "div",
10768
+ { className: "bt-member" },
10769
+ (0, import_react29.createElement)(
10770
+ "button",
10771
+ { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) },
10772
+ (0, import_react29.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
10773
+ (0, import_react29.createElement)("span", null, member.name)
10774
+ ),
10775
+ room2.admin === member.id ? (0, import_react29.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
10776
+ ),
10777
+ entry ? (0, import_react29.createElement)(MemberWork, { entry, bot: member, room: room2, roster, actions, now }) : null
10778
+ );
10779
+ }))
10121
10780
  ),
10122
- (0, import_react27.createElement)(
10781
+ (0, import_react29.createElement)(
10123
10782
  "div",
10124
10783
  { key: "mode" },
10125
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
10126
- (0, import_react27.createElement)("div", { className: "bt-kv" }, (0, import_react27.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone")))
10784
+ (0, import_react29.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
10785
+ (0, import_react29.createElement)("div", { className: "bt-kv" }, (0, import_react29.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone")))
10127
10786
  ),
10128
- usage ? (0, import_react27.createElement)(
10787
+ usage ? (0, import_react29.createElement)(
10129
10788
  "div",
10130
10789
  { key: "usage" },
10131
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Usage")),
10132
- (0, import_react27.createElement)("div", { className: "bt-kv" }, (0, import_react27.createElement)("span", null, room2.name), (0, import_react27.createElement)("span", null, usage))
10790
+ (0, import_react29.createElement)("div", { className: "bt-section-title" }, t("Usage")),
10791
+ (0, import_react29.createElement)("div", { className: "bt-kv" }, (0, import_react29.createElement)("span", null, room2.name), (0, import_react29.createElement)("span", null, usage))
10133
10792
  ) : null
10134
10793
  ];
10135
10794
  }
10136
- function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, actions, useSessions, useSessionStatus, useActivity, useBrowserTabs, useBrowserPages }) {
10795
+ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }) {
10137
10796
  const owner = roster.byId[sessionId];
10138
10797
  const part = owner ? currentPart(owner) : sessionId;
10139
10798
  const isMainBot = isMainOf(roster, owner?.id);
10140
- const [tab, setTab] = (0, import_react27.useState)("details");
10799
+ const [tab, setTab] = (0, import_react29.useState)("details");
10141
10800
  const room2 = roster.roomsById[sessionId];
10142
- const [view, setView] = (0, import_react27.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
10143
- const [editing, setEditing] = (0, import_react27.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
10144
- const [draftName, setDraftName] = (0, import_react27.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
10145
- const [copied, setCopied] = (0, import_react27.useState)(false);
10146
- const [error, setError] = (0, import_react27.useState)("");
10801
+ const [view, setView] = (0, import_react29.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
10802
+ const [editing, setEditing] = (0, import_react29.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
10803
+ const [draftName, setDraftName] = (0, import_react29.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
10804
+ const [copied, setCopied] = (0, import_react29.useState)(false);
10805
+ const [error, setError] = (0, import_react29.useState)("");
10147
10806
  const glide = useGlide(tab);
10148
- const last = (0, import_react27.useRef)({ view, tab, move: void 0 });
10807
+ const last = (0, import_react29.useRef)({ view, tab, move: void 0 });
10149
10808
  if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" };
10150
10809
  else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" };
10151
10810
  const move = last.current.move;
@@ -10170,36 +10829,36 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
10170
10829
  setTimeout(() => setCopied(false), 1600);
10171
10830
  });
10172
10831
  };
10173
- const errorLine = error ? (0, import_react27.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
10832
+ const errorLine = error ? (0, import_react29.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
10174
10833
  if ((bot || room2) && view === "settings") {
10175
10834
  return [
10176
- (0, import_react27.createElement)(
10835
+ (0, import_react29.createElement)(
10177
10836
  "div",
10178
10837
  { key: "head", className: "bt-subhead", "data-move": move },
10179
- (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react27.createElement)(ChevronLeftIcon)),
10180
- (0, import_react27.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
10181
- hasStrip ? null : (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react27.createElement)(CloseIcon))
10838
+ (0, import_react29.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react29.createElement)(ChevronLeftIcon)),
10839
+ (0, import_react29.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
10840
+ hasStrip ? null : (0, import_react29.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react29.createElement)(CloseIcon))
10182
10841
  ),
10183
- (0, import_react27.createElement)(
10842
+ (0, import_react29.createElement)(
10184
10843
  "div",
10185
10844
  { key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move },
10186
10845
  errorLine,
10187
- bot ? (0, import_react27.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react27.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
10846
+ bot ? (0, import_react29.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react29.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
10188
10847
  )
10189
10848
  ];
10190
10849
  }
10191
- const top = (0, import_react27.createElement)(
10850
+ const top = (0, import_react29.createElement)(
10192
10851
  "div",
10193
10852
  { className: "bt-drawer-top", "data-move": side },
10194
- bot || room2 ? (0, import_react27.createElement)("button", { type: "button", className: "bt-round", "aria-label": bot ? t("Edit Bot") : t("Group settings"), title: bot ? t("Edit Bot") : t("Group settings"), "data-info-row": "settings", onClick: () => setView("settings") }, (0, import_react27.createElement)(GearIcon)) : null,
10195
- bot && roster.browser === true && actions.openBrowser ? (0, import_react27.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react27.createElement)(GlobeIcon)) : null,
10196
- bot || room2 ? (0, import_react27.createElement)("button", { type: "button", className: "bt-round", "aria-label": copied ? t("Copied conversation") : t("Share conversation"), title: copied ? t("Copied") : t("Copy conversation as Markdown"), onClick: share }, copied ? (0, import_react27.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react27.createElement)(ShareIcon)) : null,
10197
- hasStrip ? null : (0, import_react27.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react27.createElement)(CloseIcon))
10853
+ bot || room2 ? (0, import_react29.createElement)("button", { type: "button", className: "bt-round", "aria-label": bot ? t("Edit Bot") : t("Group settings"), title: bot ? t("Edit Bot") : t("Group settings"), "data-info-row": "settings", onClick: () => setView("settings") }, (0, import_react29.createElement)(GearIcon)) : null,
10854
+ bot && roster.browser === true && actions.openBrowser ? (0, import_react29.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Open browser"), title: t("Open this Bot's browser"), onClick: () => actions.openBrowser(sessionId) }, (0, import_react29.createElement)(GlobeIcon)) : null,
10855
+ bot || room2 ? (0, import_react29.createElement)("button", { type: "button", className: "bt-round", "aria-label": copied ? t("Copied conversation") : t("Share conversation"), title: copied ? t("Copied") : t("Copy conversation as Markdown"), onClick: share }, copied ? (0, import_react29.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react29.createElement)(ShareIcon)) : null,
10856
+ hasStrip ? null : (0, import_react29.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react29.createElement)(CloseIcon))
10198
10857
  );
10199
10858
  if (!bot && !room2) {
10200
10859
  return [
10201
- (0, import_react27.createElement)(import_react27.Fragment, { key: "top" }, top),
10202
- (0, import_react27.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react27.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team.")))
10860
+ (0, import_react29.createElement)(import_react29.Fragment, { key: "top" }, top),
10861
+ (0, import_react29.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react29.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team.")))
10203
10862
  ];
10204
10863
  }
10205
10864
  const current = bot?.name ?? room2.name;
@@ -10213,12 +10872,12 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
10213
10872
  const subtitle = bot ? bot.role : roomSubtitle(room2, roster);
10214
10873
  const tabs = bot ? ["details", "browser"] : ["details"];
10215
10874
  return [
10216
- (0, import_react27.createElement)(import_react27.Fragment, { key: "top" }, top),
10217
- (0, import_react27.createElement)(
10875
+ (0, import_react29.createElement)(import_react29.Fragment, { key: "top" }, top),
10876
+ (0, import_react29.createElement)(
10218
10877
  "div",
10219
10878
  { key: "id", className: "bt-drawer-id", "data-move": side },
10220
- bot ? (0, import_react27.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react27.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
10221
- editing ? (0, import_react27.createElement)("input", {
10879
+ bot ? (0, import_react29.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react29.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
10880
+ editing ? (0, import_react29.createElement)("input", {
10222
10881
  className: "bt-drawer-name-input",
10223
10882
  autoFocus: true,
10224
10883
  value: draftName,
@@ -10233,49 +10892,49 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
10233
10892
  setEditing(false);
10234
10893
  }
10235
10894
  }
10236
- }) : (0, import_react27.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => {
10895
+ }) : (0, import_react29.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => {
10237
10896
  setDraftName(room2 && !room2.named ? "" : current);
10238
10897
  setEditing(true);
10239
10898
  } }, current),
10240
- subtitle ? (0, import_react27.createElement)("span", { className: "bt-drawer-role" }, subtitle) : null
10899
+ subtitle ? (0, import_react29.createElement)("span", { className: "bt-drawer-role" }, subtitle) : null
10241
10900
  ),
10242
- (0, import_react27.createElement)(
10901
+ (0, import_react29.createElement)(
10243
10902
  "div",
10244
10903
  { key: "tabs", ref: glide.box, className: "bt-tabs", role: "tablist", "data-move": side },
10245
- (0, import_react27.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
10246
- tabs.map((key) => (0, import_react27.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key])))
10904
+ (0, import_react29.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
10905
+ tabs.map((key) => (0, import_react29.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key])))
10247
10906
  ),
10248
- (0, import_react27.createElement)(
10907
+ (0, import_react29.createElement)(
10249
10908
  "div",
10250
10909
  { key: `body:${tab}`, className: "bt-drawer-body", "data-move": move },
10251
10910
  errorLine,
10252
- room2 && tab === "details" ? (0, import_react27.createElement)(GroupOverview, { room: room2, roster, actions, usage, openSettings: () => setView("settings") }) : null,
10911
+ room2 && tab === "details" ? (0, import_react29.createElement)(GroupOverview, { room: room2, roster, actions, usage, useWork, openSettings: () => setView("settings") }) : null,
10253
10912
  bot && tab === "details" ? [
10254
- (0, import_react27.createElement)(
10913
+ (0, import_react29.createElement)(
10255
10914
  "div",
10256
10915
  { key: "model", className: "bt-drawer-model" },
10257
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Model")),
10258
- (0, import_react27.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
10259
- modelOf(bot, roster) ? null : (0, import_react27.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name }))
10916
+ (0, import_react29.createElement)("div", { className: "bt-section-title" }, t("Model")),
10917
+ (0, import_react29.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
10918
+ modelOf(bot, roster) ? null : (0, import_react29.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name }))
10260
10919
  ),
10261
- (0, import_react27.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
10262
- bot.instructions ? (0, import_react27.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react27.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
10263
- roster.memory === false ? null : (0, import_react27.createElement)(MemoryRow, { key: "memory", bot, actions }),
10264
- usage ? (0, import_react27.createElement)(
10920
+ (0, import_react29.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
10921
+ bot.instructions ? (0, import_react29.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react29.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
10922
+ roster.memory === false ? null : (0, import_react29.createElement)(MemoryRow, { key: "memory", bot, actions }),
10923
+ usage ? (0, import_react29.createElement)(
10265
10924
  "div",
10266
10925
  { key: "usage" },
10267
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Usage")),
10268
- (0, import_react27.createElement)("div", { className: "bt-kv" }, (0, import_react27.createElement)("span", null, bot.name), (0, import_react27.createElement)("span", null, usage))
10926
+ (0, import_react29.createElement)("div", { className: "bt-section-title" }, t("Usage")),
10927
+ (0, import_react29.createElement)("div", { className: "bt-kv" }, (0, import_react29.createElement)("span", null, bot.name), (0, import_react29.createElement)("span", null, usage))
10269
10928
  ) : null,
10270
- (0, import_react27.createElement)(ScheduleSummary, { key: "routines", bot, actions })
10929
+ (0, import_react29.createElement)(ScheduleSummary, { key: "routines", bot, actions })
10271
10930
  ] : null,
10272
- bot && tab === "browser" ? (0, import_react27.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
10931
+ bot && tab === "browser" ? (0, import_react29.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
10273
10932
  )
10274
10933
  ];
10275
10934
  }
10276
10935
 
10277
10936
  // src/client/browser-pane.js
10278
- var import_react28 = require("react");
10937
+ var import_react30 = require("react");
10279
10938
  var import_react_dom7 = require("react-dom");
10280
10939
  var PANEL_LINGER_MS = 300;
10281
10940
  var WIDTH_KEY = STORAGE.browserWidth;
@@ -10290,10 +10949,10 @@ var EMPTY_TABS2 = { ids: [], active: null };
10290
10949
  var SPRING_LEAD2 = springEasing(576, 39.4);
10291
10950
  var SPRING_TRAIL2 = springEasing(144, 19.7);
10292
10951
  var icon = (paths) => function BrowserIcon() {
10293
- return (0, import_react28.createElement)(
10952
+ return (0, import_react30.createElement)(
10294
10953
  "svg",
10295
10954
  { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
10296
- paths.map((d, index) => (0, import_react28.createElement)("path", { key: index, d }))
10955
+ paths.map((d, index) => (0, import_react30.createElement)("path", { key: index, d }))
10297
10956
  );
10298
10957
  };
10299
10958
  var GlobeIcon = icon(["M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M3 10h14", "M10 3c2 2.2 2.9 4.5 2.9 7S12 14.8 10 17c-2-2.2-2.9-4.5-2.9-7S8 5.2 10 3z"]);
@@ -10305,9 +10964,9 @@ var CloseIcon2 = icon(["M5.5 5.5l9 9M14.5 5.5l-9 9"]);
10305
10964
  var PlusTabIcon = icon(["M10 5v10M5 10h10"]);
10306
10965
  var LockIcon2 = icon(["M6.5 9V7a3.5 3.5 0 0 1 7 0v2", "M5.5 9h9v7h-9z"]);
10307
10966
  function Favicon({ src, className }) {
10308
- const [failed, setFailed] = (0, import_react28.useState)(null);
10309
- if (!src || failed === src) return (0, import_react28.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react28.createElement)(GlobeIcon));
10310
- return (0, import_react28.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
10967
+ const [failed, setFailed] = (0, import_react30.useState)(null);
10968
+ if (!src || failed === src) return (0, import_react30.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react30.createElement)(GlobeIcon));
10969
+ return (0, import_react30.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
10311
10970
  }
10312
10971
  var savedWidth = () => {
10313
10972
  try {
@@ -10366,11 +11025,11 @@ var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(pref
10366
11025
  var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab");
10367
11026
  function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) {
10368
11027
  const pages = useBrowserPages((map) => map);
10369
- const wrap = (0, import_react28.useRef)(null);
10370
- const ind = (0, import_react28.useRef)(null);
10371
- const spot = (0, import_react28.useRef)(null);
10372
- const under = (0, import_react28.useRef)(null);
10373
- (0, import_react28.useLayoutEffect)(() => {
11028
+ const wrap = (0, import_react30.useRef)(null);
11029
+ const ind = (0, import_react30.useRef)(null);
11030
+ const spot = (0, import_react30.useRef)(null);
11031
+ const under = (0, import_react30.useRef)(null);
11032
+ (0, import_react30.useLayoutEffect)(() => {
10374
11033
  const box = wrap.current;
10375
11034
  const capsule = ind.current;
10376
11035
  if (!box || !capsule) return void 0;
@@ -10399,7 +11058,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
10399
11058
  trail.cancel();
10400
11059
  };
10401
11060
  });
10402
- (0, import_react28.useEffect)(() => {
11061
+ (0, import_react30.useEffect)(() => {
10403
11062
  const box = wrap.current;
10404
11063
  if (!box) return void 0;
10405
11064
  const observer = new ResizeObserver(() => {
@@ -10413,13 +11072,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
10413
11072
  observer.observe(box);
10414
11073
  return () => observer.disconnect();
10415
11074
  }, []);
10416
- return (0, import_react28.createElement)(
11075
+ return (0, import_react30.createElement)(
10417
11076
  "div",
10418
11077
  { className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") },
10419
- (0, import_react28.createElement)(
11078
+ (0, import_react30.createElement)(
10420
11079
  "div",
10421
11080
  { className: "bt-ptabs-in", ref: wrap },
10422
- (0, import_react28.createElement)(
11081
+ (0, import_react30.createElement)(
10423
11082
  "button",
10424
11083
  {
10425
11084
  type: "button",
@@ -10429,13 +11088,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
10429
11088
  title: bot.name,
10430
11089
  onClick: () => onView("details")
10431
11090
  },
10432
- (0, import_react28.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
10433
- view === "details" ? (0, import_react28.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null
11091
+ (0, import_react30.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
11092
+ view === "details" ? (0, import_react30.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null
10434
11093
  ),
10435
11094
  ids.map((tabId) => {
10436
11095
  const page = pages[tabId] ?? EMPTY_PAGE;
10437
11096
  const on = view === "browser" && tabId === active;
10438
- return (0, import_react28.createElement)(
11097
+ return (0, import_react30.createElement)(
10439
11098
  "button",
10440
11099
  {
10441
11100
  key: tabId,
@@ -10456,9 +11115,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
10456
11115
  }
10457
11116
  }
10458
11117
  },
10459
- (0, import_react28.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
10460
- (0, import_react28.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
10461
- (0, import_react28.createElement)("span", {
11118
+ (0, import_react30.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
11119
+ (0, import_react30.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
11120
+ (0, import_react30.createElement)("span", {
10462
11121
  className: "bt-ptab-x",
10463
11122
  role: "button",
10464
11123
  "aria-label": t("Close tab"),
@@ -10470,20 +11129,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
10470
11129
  }, "×")
10471
11130
  );
10472
11131
  }),
10473
- (0, import_react28.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true })
11132
+ (0, import_react30.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true })
10474
11133
  ),
10475
11134
  // New tab and Close stay outside the scrolling tabs, so a narrow panel keeps both.
10476
- (0, import_react28.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react28.createElement)(PlusTabIcon)),
10477
- (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react28.createElement)(CloseIcon2))
11135
+ (0, import_react30.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react30.createElement)(PlusTabIcon)),
11136
+ (0, import_react30.createElement)("button", { type: "button", className: "bt-icon-btn bt-ptab-close", "aria-label": t("Close panel"), title: t("Close panel"), onClick: () => actions.closePanel() }, (0, import_react30.createElement)(CloseIcon2))
10478
11137
  );
10479
11138
  }
10480
11139
  function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) {
10481
11140
  const page = useBrowserPages((pages) => pages[tabId ?? ""]) ?? EMPTY_PAGE;
10482
- const frame = (0, import_react28.useRef)(null);
10483
- const input = (0, import_react28.useRef)(null);
10484
- const [draft, setDraft] = (0, import_react28.useState)(null);
11141
+ const frame = (0, import_react30.useRef)(null);
11142
+ const input = (0, import_react30.useRef)(null);
11143
+ const [draft, setDraft] = (0, import_react30.useState)(null);
10485
11144
  const showGuest = landed && !leaving && Boolean(page.url) && !page.error;
10486
- (0, import_react28.useLayoutEffect)(() => {
11145
+ (0, import_react30.useLayoutEffect)(() => {
10487
11146
  const element = frame.current;
10488
11147
  if (!element || leaving || !tabId) {
10489
11148
  void actions.browserShow(null);
@@ -10509,7 +11168,7 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
10509
11168
  window.removeEventListener("resize", schedule);
10510
11169
  };
10511
11170
  }, [tabId, showGuest, leaving]);
10512
- (0, import_react28.useEffect)(() => {
11171
+ (0, import_react30.useEffect)(() => {
10513
11172
  setDraft(null);
10514
11173
  if (landed && !page.url) input.current?.focus();
10515
11174
  }, [tabId, landed]);
@@ -10525,27 +11184,27 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
10525
11184
  };
10526
11185
  const editing = draft !== null;
10527
11186
  const secure = /^https:/i.test(page.url);
10528
- return (0, import_react28.createElement)(
11187
+ return (0, import_react30.createElement)(
10529
11188
  "div",
10530
11189
  { className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) },
10531
- (0, import_react28.createElement)(
11190
+ (0, import_react30.createElement)(
10532
11191
  "div",
10533
11192
  { className: "bt-browser-bar" },
10534
- (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react28.createElement)(BackIcon)),
10535
- (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react28.createElement)(ForwardIcon)),
10536
- (0, import_react28.createElement)("button", {
11193
+ (0, import_react30.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back"), title: t("Back"), disabled: !page.canGoBack, onClick: () => actions.browserCommand(tabId, "back") }, (0, import_react30.createElement)(BackIcon)),
11194
+ (0, import_react30.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Forward"), title: t("Forward"), disabled: !page.canGoForward, onClick: () => actions.browserCommand(tabId, "forward") }, (0, import_react30.createElement)(ForwardIcon)),
11195
+ (0, import_react30.createElement)("button", {
10537
11196
  type: "button",
10538
11197
  className: "bt-icon-btn",
10539
11198
  disabled: !page.url,
10540
11199
  "aria-label": page.loading ? t("Stop") : t("Reload"),
10541
11200
  title: page.loading ? t("Stop") : t("Reload"),
10542
11201
  onClick: () => actions.browserCommand(tabId, page.loading ? "stop" : "reload")
10543
- }, (0, import_react28.createElement)(page.loading ? StopIcon : ReloadIcon)),
10544
- (0, import_react28.createElement)(
11202
+ }, (0, import_react30.createElement)(page.loading ? StopIcon : ReloadIcon)),
11203
+ (0, import_react30.createElement)(
10545
11204
  "form",
10546
11205
  { className: "bt-browser-address", onSubmit: go, "data-editing": editing || void 0 },
10547
- (0, import_react28.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react28.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
10548
- (0, import_react28.createElement)("input", {
11206
+ (0, import_react30.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react30.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
11207
+ (0, import_react30.createElement)("input", {
10549
11208
  ref: input,
10550
11209
  type: "text",
10551
11210
  spellCheck: false,
@@ -10569,71 +11228,71 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
10569
11228
  }
10570
11229
  })
10571
11230
  ),
10572
- page.loading ? (0, import_react28.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
11231
+ page.loading ? (0, import_react30.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
10573
11232
  ),
10574
- (0, import_react28.createElement)(
11233
+ (0, import_react30.createElement)(
10575
11234
  "div",
10576
11235
  { ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 },
10577
- page.error ? (0, import_react28.createElement)(
11236
+ page.error ? (0, import_react30.createElement)(
10578
11237
  "div",
10579
11238
  { className: "bt-browser-empty" },
10580
- (0, import_react28.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
10581
- (0, import_react28.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
10582
- page.url ? (0, import_react28.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => {
11239
+ (0, import_react30.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
11240
+ (0, import_react30.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
11241
+ page.url ? (0, import_react30.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => {
10583
11242
  void actions.browserNavigate(tabId, page.url);
10584
11243
  } }, t("Try again")) : null
10585
- ) : !page.url ? (0, import_react28.createElement)(
11244
+ ) : !page.url ? (0, import_react30.createElement)(
10586
11245
  "div",
10587
11246
  { className: "bt-browser-empty" },
10588
- (0, import_react28.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }),
10589
- (0, import_react28.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
10590
- (0, import_react28.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name }))
11247
+ (0, import_react30.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }),
11248
+ (0, import_react30.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
11249
+ (0, import_react30.createElement)("div", { className: "bt-browser-empty-text" }, t("Open a page here and {name} can read it with you. Logins and cookies stay with {name}; other Bots never see them.", { name: bot.name }))
10591
11250
  ) : null
10592
11251
  )
10593
11252
  );
10594
11253
  }
10595
- function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatus, useActivity, useBrowserTabs, useBrowserPages }) {
11254
+ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }) {
10596
11255
  const bot = roster.byId[pane.id];
10597
11256
  const canBrowse = Boolean(bot && roster.browser === true && actions.browserShow);
10598
11257
  const view = pane.view === "browser" && canBrowse ? "browser" : "details";
10599
11258
  const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2);
10600
11259
  const active = tabState.active;
10601
11260
  const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE);
10602
- const root = (0, import_react28.useRef)(null);
10603
- const fly = (0, import_react28.useRef)(null);
10604
- const [landed, setLanded] = (0, import_react28.useState)(false);
10605
- const [settling, setSettling] = (0, import_react28.useState)(false);
10606
- const [width, setWidth] = (0, import_react28.useState)(savedWidth);
10607
- const [viewport, setViewport] = (0, import_react28.useState)(() => window.innerWidth);
10608
- const [dragging, setDragging] = (0, import_react28.useState)(false);
10609
- const [resizing, setResizing] = (0, import_react28.useState)(false);
11261
+ const root = (0, import_react30.useRef)(null);
11262
+ const fly = (0, import_react30.useRef)(null);
11263
+ const [landed, setLanded] = (0, import_react30.useState)(false);
11264
+ const [settling, setSettling] = (0, import_react30.useState)(false);
11265
+ const [width, setWidth] = (0, import_react30.useState)(savedWidth);
11266
+ const [viewport, setViewport] = (0, import_react30.useState)(() => window.innerWidth);
11267
+ const [dragging, setDragging] = (0, import_react30.useState)(false);
11268
+ const [resizing, setResizing] = (0, import_react30.useState)(false);
10610
11269
  const instant = dragging || resizing;
10611
- const [reading, setReading] = (0, import_react28.useState)(false);
11270
+ const [reading, setReading] = (0, import_react30.useState)(false);
10612
11271
  const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport));
10613
- (0, import_react28.useLayoutEffect)(() => {
11272
+ (0, import_react30.useLayoutEffect)(() => {
10614
11273
  if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH);
10615
11274
  }, [view, width]);
10616
- (0, import_react28.useLayoutEffect)(() => {
11275
+ (0, import_react30.useLayoutEffect)(() => {
10617
11276
  chatRoom.hold(instant);
10618
11277
  }, [instant]);
10619
- const [last, setLast] = (0, import_react28.useState)({ view, width: panelWidth });
11278
+ const [last, setLast] = (0, import_react30.useState)({ view, width: panelWidth });
10620
11279
  if (last.view !== view || last.width !== panelWidth) {
10621
11280
  setLast({ view, width: panelWidth });
10622
11281
  if (last.view !== view) setSettling(last.width !== panelWidth && !instant);
10623
11282
  }
10624
- const shownView = (0, import_react28.useRef)(view);
10625
- (0, import_react28.useLayoutEffect)(() => {
11283
+ const shownView = (0, import_react30.useRef)(view);
11284
+ (0, import_react30.useLayoutEffect)(() => {
10626
11285
  const switched = settling && shownView.current !== view;
10627
11286
  shownView.current = view;
10628
11287
  if (switched) chatRoom.hold(true);
10629
11288
  chatRoom.set(leaving ? null : panelWidth);
10630
11289
  if (switched) chatRoom.hold(false);
10631
11290
  }, [view, panelWidth, leaving]);
10632
- (0, import_react28.useEffect)(() => () => {
11291
+ (0, import_react30.useEffect)(() => () => {
10633
11292
  chatRoom.set(null);
10634
11293
  chatRoom.hold(false);
10635
11294
  }, []);
10636
- (0, import_react28.useEffect)(() => {
11295
+ (0, import_react30.useEffect)(() => {
10637
11296
  let timer;
10638
11297
  const onResize = () => {
10639
11298
  setResizing(true);
@@ -10647,28 +11306,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
10647
11306
  window.removeEventListener("resize", onResize);
10648
11307
  };
10649
11308
  }, []);
10650
- (0, import_react28.useEffect)(() => {
11309
+ (0, import_react30.useEffect)(() => {
10651
11310
  if (!settling) return void 0;
10652
11311
  const timer = setTimeout(() => setSettling(false), SPRING_SHAPE.duration + 40);
10653
11312
  return () => clearTimeout(timer);
10654
11313
  }, [settling, view]);
10655
- (0, import_react28.useEffect)(() => {
11314
+ (0, import_react30.useEffect)(() => {
10656
11315
  const timer = setTimeout(() => setLanded(true), 360);
10657
11316
  return () => clearTimeout(timer);
10658
11317
  }, []);
10659
- (0, import_react28.useEffect)(() => {
11318
+ (0, import_react30.useEffect)(() => {
10660
11319
  if (canBrowse) actions.browserEnsure(bot.id);
10661
11320
  }, [bot?.id, canBrowse]);
10662
- (0, import_react28.useEffect)(() => {
11321
+ (0, import_react30.useEffect)(() => {
10663
11322
  if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details");
10664
11323
  }, [view, active, tabState.ids.length]);
10665
- (0, import_react28.useEffect)(() => {
11324
+ (0, import_react30.useEffect)(() => {
10666
11325
  if (view !== "browser" || leaving) void actions.browserShow?.(null);
10667
11326
  }, [view, leaving]);
10668
- (0, import_react28.useEffect)(() => () => {
11327
+ (0, import_react30.useEffect)(() => () => {
10669
11328
  void actions.browserShow?.(null);
10670
11329
  }, []);
10671
- (0, import_react28.useEffect)(() => {
11330
+ (0, import_react30.useEffect)(() => {
10672
11331
  if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0;
10673
11332
  setReading(true);
10674
11333
  const timer = setTimeout(() => setReading(false), READ_FLASH_MS);
@@ -10705,7 +11364,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
10705
11364
  return actions[name](...args);
10706
11365
  };
10707
11366
  }
10708
- (0, import_react28.useLayoutEffect)(() => {
11367
+ (0, import_react30.useLayoutEffect)(() => {
10709
11368
  const move = fly.current;
10710
11369
  if (!move) return;
10711
11370
  fly.current = null;
@@ -10788,7 +11447,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
10788
11447
  for (const name of ends) grip.addEventListener(name, onEnd);
10789
11448
  };
10790
11449
  const hasStrip = canBrowse && tabState.ids.length > 0;
10791
- return (0, import_react28.createElement)(
11450
+ return (0, import_react30.createElement)(
10792
11451
  "aside",
10793
11452
  {
10794
11453
  ref: root,
@@ -10805,15 +11464,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
10805
11464
  if (event.target === event.currentTarget) setLanded(true);
10806
11465
  }
10807
11466
  },
10808
- leaving ? null : (0, import_react28.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }),
11467
+ leaving ? null : (0, import_react30.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }),
10809
11468
  // Over the whole window while dragging: the cursor stays a resize cursor and the
10810
11469
  // pages underneath (the chat, the guest) see no pointer.
10811
- dragging ? (0, import_react_dom7.createPortal)((0, import_react28.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
10812
- hasStrip ? (0, import_react28.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
10813
- view === "browser" && bot ? (0, import_react28.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react28.createElement)(
11470
+ dragging ? (0, import_react_dom7.createPortal)((0, import_react30.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
11471
+ hasStrip ? (0, import_react30.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
11472
+ view === "browser" && bot ? (0, import_react30.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react30.createElement)(
10814
11473
  "div",
10815
11474
  { key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" },
10816
- (0, import_react28.createElement)(DetailsDrawer, {
11475
+ (0, import_react30.createElement)(DetailsDrawer, {
10817
11476
  sessionId: pane.id,
10818
11477
  startRenaming: pane.renaming,
10819
11478
  startTab: pane.tab,
@@ -10823,6 +11482,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
10823
11482
  useSessions,
10824
11483
  useSessionStatus,
10825
11484
  useActivity,
11485
+ useWork,
10826
11486
  useBrowserTabs,
10827
11487
  useBrowserPages
10828
11488
  })
@@ -10909,46 +11569,46 @@ var BROWSER_CSS = `
10909
11569
  `;
10910
11570
 
10911
11571
  // src/client/overlays.js
10912
- var import_react29 = require("react");
11572
+ var import_react31 = require("react");
10913
11573
  var import_react_dom8 = require("react-dom");
10914
11574
  var DIALOG_EXIT_MS = 160;
10915
11575
  var MENU_EXIT_MS3 = 120;
10916
- function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useRegistry, useBrowserPages, useBrowserTabs, actions }) {
11576
+ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useWork, useRegistry, useBrowserPages, useBrowserTabs, useUpdate, actions }) {
10917
11577
  const roster = useRoster((value) => value);
10918
- const ui = useUi((value) => value);
11578
+ const ui2 = useUi((value) => value);
10919
11579
  useRegistry((value) => value);
10920
11580
  const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null);
10921
- const previous = (0, import_react29.useRef)(current);
10922
- (0, import_react29.useEffect)(() => {
11581
+ const previous = (0, import_react31.useRef)(current);
11582
+ (0, import_react31.useEffect)(() => {
10923
11583
  if (previous.current === current) return;
10924
11584
  previous.current = current;
10925
11585
  if (current) actions.followPanel?.(current);
10926
11586
  }, [current]);
10927
- const open = ui.details ? { id: ui.details, view: ui.view ?? "details", renaming: ui.renaming, tab: ui.tab } : null;
10928
- const [kept, setKept] = (0, import_react29.useState)(open);
10929
- (0, import_react29.useEffect)(() => {
11587
+ const open = ui2.details ? { id: ui2.details, view: ui2.view ?? "details", renaming: ui2.renaming, tab: ui2.tab } : null;
11588
+ const [kept, setKept] = (0, import_react31.useState)(open);
11589
+ (0, import_react31.useEffect)(() => {
10930
11590
  if (open) {
10931
11591
  setKept(open);
10932
11592
  return void 0;
10933
11593
  }
10934
11594
  const timer = setTimeout(() => setKept(null), PANEL_LINGER_MS);
10935
11595
  return () => clearTimeout(timer);
10936
- }, [ui.details, ui.view, ui.renaming, ui.tab]);
11596
+ }, [ui2.details, ui2.view, ui2.renaming, ui2.tab]);
10937
11597
  const pane = open ?? kept;
10938
- const [settings, settingsLeaving] = useLinger(ui.settings, DIALOG_EXIT_MS);
10939
- const [memory, memoryLeaving] = useLinger(ui.memory && roster.byId[ui.memory] ? ui.memory : null, DIALOG_EXIT_MS);
10940
- const [menu, menuLeaving] = useLinger(ui.menu, MENU_EXIT_MS3);
10941
- return (0, import_react29.createElement)(
10942
- import_react29.Fragment,
11598
+ const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS);
11599
+ const [memory, memoryLeaving] = useLinger(ui2.memory && roster.byId[ui2.memory] ? ui2.memory : null, DIALOG_EXIT_MS);
11600
+ const [menu, menuLeaving] = useLinger(ui2.menu, MENU_EXIT_MS3);
11601
+ return (0, import_react31.createElement)(
11602
+ import_react31.Fragment,
10943
11603
  null,
10944
- pane ? (0, import_react_dom8.createPortal)((0, import_react29.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useBrowserTabs, useBrowserPages }), document.body) : null,
10945
- ui.newChat ? (0, import_react_dom8.createPortal)((0, import_react29.createElement)(NewChat, { key: ui.newChat, mode: ui.newChat, roster, actions }), document.body) : null,
10946
- ui.palette ? (0, import_react_dom8.createPortal)((0, import_react29.createElement)(CommandPalette, { key: ui.palette.token, start: ui.palette.query, token: ui.palette.token, roster, actions, useSessions }), document.body) : null,
10947
- ui.exchange ? (0, import_react_dom8.createPortal)((0, import_react29.createElement)(ExchangeDialog, { pair: ui.exchange, roster, actions }), document.body) : null,
10948
- ui.voice ? (0, import_react_dom8.createPortal)((0, import_react29.createElement)(VoiceMode, { key: ui.voice, sessionId: ui.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
10949
- menu ? (0, import_react_dom8.createPortal)((0, import_react29.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null,
10950
- settings ? (0, import_react_dom8.createPortal)((0, import_react29.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, leaving: settingsLeaving }), document.body) : null,
10951
- memory && roster.byId[memory] ? (0, import_react_dom8.createPortal)((0, import_react29.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null
11604
+ pane ? (0, import_react_dom8.createPortal)((0, import_react31.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }), document.body) : null,
11605
+ ui2.newChat ? (0, import_react_dom8.createPortal)((0, import_react31.createElement)(NewChat, { key: ui2.newChat, mode: ui2.newChat, roster, actions }), document.body) : null,
11606
+ ui2.palette ? (0, import_react_dom8.createPortal)((0, import_react31.createElement)(CommandPalette, { key: ui2.palette.token, start: ui2.palette.query, token: ui2.palette.token, roster, actions, useSessions }), document.body) : null,
11607
+ ui2.exchange ? (0, import_react_dom8.createPortal)((0, import_react31.createElement)(ExchangeDialog, { pair: ui2.exchange, roster, actions }), document.body) : null,
11608
+ ui2.voice ? (0, import_react_dom8.createPortal)((0, import_react31.createElement)(VoiceMode, { key: ui2.voice, sessionId: ui2.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
11609
+ menu ? (0, import_react_dom8.createPortal)((0, import_react31.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null,
11610
+ settings ? (0, import_react_dom8.createPortal)((0, import_react31.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
11611
+ memory && roster.byId[memory] ? (0, import_react_dom8.createPortal)((0, import_react31.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null
10952
11612
  );
10953
11613
  }
10954
11614
 
@@ -11778,6 +12438,117 @@ function writeMode(mode, storage = globalThis.localStorage) {
11778
12438
  }
11779
12439
  }
11780
12440
 
12441
+ // src/client/user-message.js
12442
+ var import_react32 = require("react");
12443
+ var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
12444
+ var canDrawUserMessages = () => typeof ui.projectUserText === "function";
12445
+ function contentParts(content) {
12446
+ const texts2 = [];
12447
+ const attachments = [];
12448
+ const rest = [];
12449
+ for (const block of Array.isArray(content) ? content : []) {
12450
+ if (block?.type === "text" && typeof block.text === "string") texts2.push(block.text);
12451
+ else if ((block?.type === "image" || block?.type === "file") && block.attachment !== void 0) attachments.push(block);
12452
+ else rest.push(block);
12453
+ }
12454
+ return { text: texts2.join("\n"), attachments, rest };
12455
+ }
12456
+ function mentionableBots(roster, sessionId) {
12457
+ const room2 = roster.roomsById[sessionId];
12458
+ if (room2) return room2.members.map((id) => roster.byId[id]).filter(Boolean);
12459
+ return roster.byId[sessionId] ? roster.bots : [];
12460
+ }
12461
+ function userTextParts(text, bots, referenceLabels = []) {
12462
+ const parts = [];
12463
+ let cursor = 0;
12464
+ for (const span of mentionSpans(text, bots, { references: referenceLabels })) {
12465
+ if (!span.bot) continue;
12466
+ if (span.start > cursor) parts.push({ text: text.slice(cursor, span.start) });
12467
+ parts.push({ bot: span.bot, label: text.slice(span.start + 1, span.end) });
12468
+ cursor = span.end;
12469
+ }
12470
+ if (cursor < text.length) parts.push({ text: text.slice(cursor) });
12471
+ return parts;
12472
+ }
12473
+ function UserText({ text, bots, referenceLabels, skillNames, references, actions }) {
12474
+ return userTextParts(text, bots, referenceLabels).map((part, index) => part.bot ? (0, import_react32.createElement)(
12475
+ "button",
12476
+ { key: index, type: "button", className: "bt-umention", title: `@${part.bot.name}`, onClick: () => actions.openSession(part.bot.id) },
12477
+ (0, import_react32.createElement)(BotMark, { bot: part.bot, size: 16 }),
12478
+ part.label
12479
+ ) : (0, import_react32.createElement)(import_react32.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references)));
12480
+ }
12481
+ function Attachments({ attachments, renderMessageImages }) {
12482
+ const compact2 = attachments.length > 1;
12483
+ return (0, import_react32.createElement)("div", { className: "bt-uattach" }, attachments.map((block, index) => {
12484
+ if (block.type === "image") {
12485
+ return renderMessageImages ? (0, import_react32.createElement)(import_react32.Fragment, { key: index }, renderMessageImages({ images: [{ attachment: block.attachment }], align: "end", compact: compact2 })) : null;
12486
+ }
12487
+ const file = block.attachment;
12488
+ const meta = [ui.fileExtension?.(file.name)?.toUpperCase().slice(0, 8), ui.fileSizeText?.(file.bytes)].filter(Boolean).join(" · ");
12489
+ return (0, import_react32.createElement)(
12490
+ "span",
12491
+ { key: index, className: "bt-ufile", title: file.name },
12492
+ ui.FileTypeIcon ? (0, import_react32.createElement)(ui.FileTypeIcon, { path: file.name, className: "bt-ufile-icon" }) : null,
12493
+ (0, import_react32.createElement)("span", { className: "bt-ufile-text" }, (0, import_react32.createElement)("span", { className: "bt-ufile-name" }, file.name), meta ? (0, import_react32.createElement)("span", { className: "bt-ufile-meta" }, meta) : null)
12494
+ );
12495
+ }));
12496
+ }
12497
+ function CopyButton({ text }) {
12498
+ const [copied, setCopied] = (0, import_react32.useState)(false);
12499
+ const copy2 = () => {
12500
+ void navigator.clipboard?.writeText(text).then(() => {
12501
+ setCopied(true);
12502
+ setTimeout(() => setCopied(false), 1e3);
12503
+ });
12504
+ };
12505
+ return (0, import_react32.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_react32.createElement)(CheckIcon) : (0, import_react32.createElement)(CopyIcon));
12506
+ }
12507
+ var clock3 = (time) => typeof time === "number" ? new Date(time).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }) : void 0;
12508
+ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageImages, openFile, openSkill }) {
12509
+ const roster = useRoster((value) => value);
12510
+ const data = node.data ?? {};
12511
+ const { text, attachments, rest } = contentParts(data.content);
12512
+ const referenceLabels = data.referenceLabels ?? [];
12513
+ const references = openFile && openSkill ? { openFile, openSkill } : void 0;
12514
+ const steering = node.kind === "steering";
12515
+ return (0, import_react32.createElement)(
12516
+ "div",
12517
+ { className: "bt-umsg", "data-steering": steering || void 0 },
12518
+ (0, import_react32.createElement)(
12519
+ "div",
12520
+ { className: "bt-ustack" },
12521
+ attachments.length > 0 ? (0, import_react32.createElement)(Attachments, { attachments, renderMessageImages }) : null,
12522
+ text !== "" || rest.length > 0 ? (0, import_react32.createElement)(
12523
+ "div",
12524
+ { className: "bt-ubody", title: clock3(data.time) },
12525
+ (0, import_react32.createElement)(UserText, { text, bots: mentionableBots(roster, sessionId), referenceLabels, skillNames: data.skillNames ?? [], references, actions }),
12526
+ rest.map((block, index) => ui.JsonBlock ? (0, import_react32.createElement)(ui.JsonBlock, { key: index, label: t("Content"), payload: block }) : null)
12527
+ ) : null,
12528
+ referenceLabels.length > 0 ? (0, import_react32.createElement)("div", { className: "bt-urefs" }, t("Referenced {labels}", { labels: referenceLabels.join(t(", ")) })) : null
12529
+ ),
12530
+ text === "" ? null : (0, import_react32.createElement)("div", { className: "bt-utools" }, (0, import_react32.createElement)(CopyButton, { text }))
12531
+ );
12532
+ }
12533
+ var USER_MESSAGE_CSS = `
12534
+ .bt-umsg{display:flex;flex-direction:row-reverse;align-items:center;justify-content:flex-start;gap:4px}
12535
+ .bt-ustack{min-width:0;max-width:min(calc(var(--dsh-chat-content-width,748px) * .702),82%);display:flex;flex-direction:column;align-items:flex-end;gap:8px}
12536
+ .bt-ubody{max-width:100%;box-sizing:border-box;background:var(--dsw-specific-bubble,var(--bt-user));color:var(--bt-user-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);white-space:pre-wrap;word-break:break-word}
12537
+ .bt-ubody a{color:inherit}
12538
+ .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}
12539
+ .bt-umention:hover{text-decoration:underline;text-underline-offset:2px}
12540
+ .bt-umention>:first-child{flex:none}
12541
+ .bt-uattach{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;max-width:100%}
12542
+ .bt-ufile{display:inline-flex;align-items:center;gap:10px;box-sizing:border-box;width:240px;min-height:64px;padding:8px 12px;border-radius:18px;border:.5px solid var(--bt-line-2);background:var(--bt-card)}
12543
+ .bt-ufile-icon{flex:none;width:28px;height:28px}
12544
+ .bt-ufile-text{display:flex;flex-direction:column;min-width:0}
12545
+ .bt-ufile-name{font-size:14px;line-height:22px;font-weight:500;color:var(--bt-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
12546
+ .bt-ufile-meta{font-size:12px;line-height:15px;color:var(--bt-ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
12547
+ .bt-urefs{font-size:13px;line-height:18px;color:var(--bt-ink-3)}
12548
+ .bt-utools{display:flex;flex:none;gap:2px;opacity:0;visibility:hidden;transition:opacity .12s ease,visibility .12s}
12549
+ .bt-umsg:hover .bt-utools,.bt-umsg:focus-within .bt-utools{opacity:1;visibility:visible}
12550
+ `;
12551
+
11781
12552
  // src/client/index.js
11782
12553
  var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout", "configForms"];
11783
12554
  function apply(ctx) {
@@ -11805,7 +12576,7 @@ function apply(ctx) {
11805
12576
  } catch {
11806
12577
  seenAt = {};
11807
12578
  }
11808
- const ui = createSource({ details: null, view: "details", renaming: false, tab: null, newChat: null, exchange: null, menu: null, voice: null, palette: null, quote: null, settings: null, memory: null, toast: null, signIn: null, renameAgent: null, unread: savedUnread, reactions: savedReactions, visits: {} });
12579
+ const ui2 = createSource({ details: null, view: "details", renaming: false, tab: null, newChat: null, exchange: null, menu: null, voice: null, palette: null, quote: null, settings: null, memory: null, toast: null, signIn: null, renameAgent: null, unread: savedUnread, reactions: savedReactions, visits: {} });
11809
12580
  const surface = createSource({ mode: readMode(), plain: false });
11810
12581
  let agentSessionCache = /* @__PURE__ */ new Set();
11811
12582
  try {
@@ -11823,7 +12594,7 @@ function apply(ctx) {
11823
12594
  };
11824
12595
  let expected = null;
11825
12596
  const setPlain = (plain) => surface.set((state) => state.plain === plain ? state : { ...state, plain });
11826
- const dropPanels = () => ui.set((state) => state.details || state.renaming ? { ...state, details: null, view: "details", renaming: false, tab: null } : state);
12597
+ const dropPanels = () => ui2.set((state) => state.details || state.renaming ? { ...state, details: null, view: "details", renaming: false, tab: null } : state);
11827
12598
  const seenKey = (id) => roster.getSnapshot().byId?.[id]?.id ?? id;
11828
12599
  const leaveVisits = (ids) => {
11829
12600
  const now = Date.now();
@@ -11834,11 +12605,13 @@ function apply(ctx) {
11834
12605
  }
11835
12606
  };
11836
12607
  ctx.effect(() => {
11837
- const onPageHide = () => leaveVisits(Object.keys(ui.getSnapshot().visits ?? {}));
12608
+ const onPageHide = () => leaveVisits(Object.keys(ui2.getSnapshot().visits ?? {}));
11838
12609
  window.addEventListener("pagehide", onPageHide);
11839
12610
  return () => window.removeEventListener("pagehide", onPageHide);
11840
12611
  }, "ds-bot: remember visits on page hide");
11841
12612
  const activity = createSource({});
12613
+ const work = createSource({ bots: {}, approvals: [] });
12614
+ let workKey = "";
11842
12615
  const live = createSource({});
11843
12616
  const turnClock = createSource({});
11844
12617
  let activityKey = "{}";
@@ -11861,7 +12634,7 @@ function apply(ctx) {
11861
12634
  } catch {
11862
12635
  savedPrefs = {};
11863
12636
  }
11864
- const setUnread = (id, value) => ui.set((state) => {
12637
+ const setUnread = (id, value) => ui2.set((state) => {
11865
12638
  if (Boolean(state.unread[id]) === value) return state;
11866
12639
  const unread = { ...state.unread };
11867
12640
  if (value) unread[id] = true;
@@ -11886,6 +12659,11 @@ function apply(ctx) {
11886
12659
  activityKey = nextActivity;
11887
12660
  activity.set(value.activity ?? {});
11888
12661
  }
12662
+ const nextWork = JSON.stringify(value.work ?? null);
12663
+ if (nextWork !== workKey) {
12664
+ workKey = nextWork;
12665
+ work.set(value.work ?? { bots: {}, approvals: [] });
12666
+ }
11889
12667
  noteMemoryNews(value.memoryNews ?? {});
11890
12668
  const current = roster.getSnapshot();
11891
12669
  if (value.connectors !== void 0 && current.ready && JSON.stringify(value.connectors) !== JSON.stringify(current.connectors ?? [])) {
@@ -11949,7 +12727,7 @@ function apply(ctx) {
11949
12727
  if (stopped) return;
11950
12728
  const value = await call("update-check", { fresh: false }).catch(() => null);
11951
12729
  if (stopped) return;
11952
- if (value) update.set(value);
12730
+ if (value) update.set((previous) => ({ ...value, app: previous.app }));
11953
12731
  timer = setTimeout(loop, value?.phase === "installing" || value?.dsh?.phase === "installing" ? 2e3 : 60 * 6e4);
11954
12732
  };
11955
12733
  wakeUpdate = loop;
@@ -11961,6 +12739,22 @@ function apply(ctx) {
11961
12739
  };
11962
12740
  };
11963
12741
  }, "ds-bot: update check");
12742
+ const appUpdates = window.dshDesktop?.protocolVersion === 1 ? window.dshDesktop.updates ?? null : null;
12743
+ if (appUpdates?.status && appUpdates.subscribe) {
12744
+ ctx.effect(() => {
12745
+ let stopped = false;
12746
+ const take = (app) => {
12747
+ if (!stopped && app) update.set((previous) => ({ ...previous, app }));
12748
+ };
12749
+ void appUpdates.status().then(take, () => {
12750
+ });
12751
+ const unsubscribe = appUpdates.subscribe(take);
12752
+ return () => {
12753
+ stopped = true;
12754
+ unsubscribe?.();
12755
+ };
12756
+ }, "ds-bot: app update status");
12757
+ }
11964
12758
  const bridge = window.dshDesktop?.protocolVersion === 1 ? window.dshDesktop.browser ?? null : null;
11965
12759
  const browsers = bridge ? createBotBrowsers({ bridge, createSource }) : null;
11966
12760
  if (browsers) {
@@ -11976,7 +12770,7 @@ function apply(ctx) {
11976
12770
  stopReads = null;
11977
12771
  }
11978
12772
  void browsers.retain(snapshot.bots.map((bot) => bot.id));
11979
- ui.set((state) => state.details && state.view === "browser" && (!on || !snapshot.byId[state.details]) ? { ...state, view: "details" } : state);
12773
+ ui2.set((state) => state.details && state.view === "browser" && (!on || !snapshot.byId[state.details]) ? { ...state, view: "details" } : state);
11980
12774
  };
11981
12775
  sync();
11982
12776
  const offRoster = roster.subscribe(sync);
@@ -11996,9 +12790,12 @@ function apply(ctx) {
11996
12790
  const render = () => {
11997
12791
  const next = roster.getSnapshot().bots.map((bot) => {
11998
12792
  const anchor = `.bt-bubble a[href="${MENTION_ORIGIN}${bot.id}"]`;
12793
+ const chip = /\s/.test(bot.name) ? "" : `[class*="_userRow"] [data-ref-chip="file"][title="@${bot.name.replace(/["\\]/g, "\\$&")}" i]`;
11999
12794
  const look = lookOf(bot);
12000
- const mark = look.image ? `${anchor}::before{background:center/cover url("${look.image}");-webkit-mask:none;mask:none;border-radius:50%}` : `${anchor}::before{background:${paintCss(look.color)};-webkit-mask-image:${maskOf(look.shape)};mask-image:${maskOf(look.shape)}}`;
12001
- return `${anchor}{color:${inkText(look.color)}}${mark}`;
12795
+ const marks = chip ? `${anchor}::before,${chip}::before` : `${anchor}::before`;
12796
+ const mark = look.image ? `${marks}{background:center/cover url("${look.image}");-webkit-mask:none;mask:none;border-radius:50%}` : `${marks}{background:${paintCss(look.color)};-webkit-mask-image:${maskOf(look.shape)};mask-image:${maskOf(look.shape)}}`;
12797
+ const chipBase = chip ? `${chip}{color:inherit;font-weight:600}${chip}>svg{display:none}${chip}::before{content:"";display:inline-block;flex:none;width:16px;height:16px;margin-right:3px;vertical-align:-3px;-webkit-mask:${DEFAULT_MASK} center/contain no-repeat;mask:${DEFAULT_MASK} center/contain no-repeat}` : "";
12798
+ return `${anchor}{color:${inkText(look.color)}}${chipBase}${mark}`;
12002
12799
  }).join("\n");
12003
12800
  if (next !== last) style.textContent = last = next;
12004
12801
  };
@@ -12113,7 +12910,7 @@ function apply(ctx) {
12113
12910
  isAgentSession: (id) => isAgentSessionId(id),
12114
12911
  openSession: (id) => {
12115
12912
  if (isAgentSessionId(id)) return actions.openAgentSession(id);
12116
- ui.set((state) => state.newChat || state.exchange || state.menu || state.palette ? { ...state, newChat: null, exchange: null, menu: null, palette: null } : state);
12913
+ ui2.set((state) => state.newChat || state.exchange || state.menu || state.palette ? { ...state, newChat: null, exchange: null, menu: null, palette: null } : state);
12117
12914
  setPlain(false);
12118
12915
  const target = partFor(id);
12119
12916
  expected = target;
@@ -12122,7 +12919,7 @@ function apply(ctx) {
12122
12919
  // The conversation replacements withdraw for a plain Session; set the surface
12123
12920
  // before the navigation so the Bot skin never flashes.
12124
12921
  openAgentSession: (sessionId) => {
12125
- ui.set((state) => ({ ...state, newChat: null, exchange: null, menu: null, palette: null, details: null, view: "details", renaming: false, tab: null }));
12922
+ ui2.set((state) => ({ ...state, newChat: null, exchange: null, menu: null, palette: null, details: null, view: "details", renaming: false, tab: null }));
12126
12923
  setPlain(true);
12127
12924
  expected = sessionId;
12128
12925
  ctx.uiWorkspace.openSession(sessionId);
@@ -12163,8 +12960,8 @@ function apply(ctx) {
12163
12960
  else if (roster.getSnapshot().mainBotId) actions.openSession(roster.getSnapshot().mainBotId);
12164
12961
  },
12165
12962
  renameAgentSession: (agentId, sessionId, title) => after(call("agent-rename", { agentId, sessionId, title })),
12166
- beginAgentRename: (sessionId) => ui.set((state) => ({ ...state, renameAgent: sessionId })),
12167
- endAgentRename: () => ui.set((state) => state.renameAgent ? { ...state, renameAgent: null } : state),
12963
+ beginAgentRename: (sessionId) => ui2.set((state) => ({ ...state, renameAgent: sessionId })),
12964
+ endAgentRename: () => ui2.set((state) => state.renameAgent ? { ...state, renameAgent: null } : state),
12168
12965
  // Removing the Agent archives every Session it owns; one of them may fill the
12169
12966
  // main view, and it must not stay there wearing the Bot surface afterwards.
12170
12967
  removeAgent: async (agentId) => {
@@ -12190,26 +12987,26 @@ function apply(ctx) {
12190
12987
  ctx.layout.selectPanel(id);
12191
12988
  },
12192
12989
  openNewChat: (mode) => {
12193
- ui.set((state) => ({ ...state, newChat: mode, menu: null, palette: null }));
12990
+ ui2.set((state) => ({ ...state, newChat: mode, menu: null, palette: null }));
12194
12991
  },
12195
12992
  openPalette: (query = "") => {
12196
- ui.set((state) => ({ ...state, palette: { query, token: Date.now() }, newChat: null, menu: null }));
12993
+ ui2.set((state) => ({ ...state, palette: { query, token: Date.now() }, newChat: null, menu: null }));
12197
12994
  },
12198
12995
  // The token keeps a late exit timer from closing a palette opened again since.
12199
12996
  closePalette: (token) => {
12200
- ui.set((state) => state.palette && (token === void 0 || state.palette.token === token) ? { ...state, palette: null } : state);
12997
+ ui2.set((state) => state.palette && (token === void 0 || state.palette.token === token) ? { ...state, palette: null } : state);
12201
12998
  },
12202
12999
  openExchange: (a, b) => {
12203
- if (a && b) ui.set((state) => ({ ...state, exchange: [a, b] }));
13000
+ if (a && b) ui2.set((state) => ({ ...state, exchange: [a, b] }));
12204
13001
  },
12205
13002
  openVoice: (id) => {
12206
- if (id) ui.set((state) => ({ ...state, voice: id, menu: null }));
13003
+ if (id) ui2.set((state) => ({ ...state, voice: id, menu: null }));
12207
13004
  },
12208
13005
  closeVoice: () => {
12209
- ui.set((state) => state.voice ? { ...state, voice: null } : state);
13006
+ ui2.set((state) => state.voice ? { ...state, voice: null } : state);
12210
13007
  },
12211
13008
  toggleDetails: (id, force) => {
12212
- ui.set((state) => {
13009
+ ui2.set((state) => {
12213
13010
  const details = force || state.details !== id || state.view !== "details" ? id : null;
12214
13011
  return { ...state, renaming: false, tab: null, details, view: "details" };
12215
13012
  });
@@ -12217,34 +13014,34 @@ function apply(ctx) {
12217
13014
  // The open panel follows the main view; a subject with no browser (a group
12218
13015
  // chat, an outside Session) settles on the details view.
12219
13016
  followPanel: (id) => {
12220
- ui.set((state) => {
13017
+ ui2.set((state) => {
12221
13018
  if (!state.details || state.details === id) return state;
12222
13019
  const view = state.view === "browser" && roster.getSnapshot().byId[id] === void 0 ? "details" : state.view;
12223
13020
  return { ...state, details: id, view, renaming: false, tab: null };
12224
13021
  });
12225
13022
  },
12226
13023
  showDetails: (id, tab = null) => {
12227
- ui.set((state) => ({ ...state, details: id, view: "details", renaming: false, tab }));
13024
+ ui2.set((state) => ({ ...state, details: id, view: "details", renaming: false, tab }));
12228
13025
  },
12229
13026
  // Opening the conversation would move focus to its composer and blur the name field.
12230
13027
  renameBot: (id) => {
12231
- ui.set((state) => ({ ...state, details: id, view: "details", renaming: true, tab: null }));
13028
+ ui2.set((state) => ({ ...state, details: id, view: "details", renaming: true, tab: null }));
12232
13029
  },
12233
13030
  // `id` opens one Bot (page 'bots') or one group chat (page 'groups').
12234
13031
  openSettings: (page = "general", id = null) => {
12235
- ui.set((state) => ({ ...state, settings: { page, id }, menu: null, palette: null, newChat: null }));
13032
+ ui2.set((state) => ({ ...state, settings: { page, id }, menu: null, palette: null, newChat: null }));
12236
13033
  },
12237
13034
  closeSettings: () => {
12238
- ui.set((state) => state.settings ? { ...state, settings: null } : state);
13035
+ ui2.set((state) => state.settings ? { ...state, settings: null } : state);
12239
13036
  },
12240
13037
  openMemory: (botId) => {
12241
- ui.set((state) => ({ ...state, memory: botId, menu: null, palette: null }));
13038
+ ui2.set((state) => ({ ...state, memory: botId, menu: null, palette: null }));
12242
13039
  },
12243
13040
  closeMemory: () => {
12244
- ui.set((state) => state.memory ? { ...state, memory: null } : state);
13041
+ ui2.set((state) => state.memory ? { ...state, memory: null } : state);
12245
13042
  },
12246
13043
  openConnectors: () => {
12247
- ui.set((state) => ({ ...state, settings: { page: "connectors", id: null }, menu: null, palette: null, newChat: null }));
13044
+ ui2.set((state) => ({ ...state, settings: { page: "connectors", id: null }, menu: null, palette: null, newChat: null }));
12248
13045
  },
12249
13046
  // Agent mode hands the surface back to the shell; everything open on the Bot
12250
13047
  // surface closes first so nothing stale reopens on the way back.
@@ -12252,7 +13049,7 @@ function apply(ctx) {
12252
13049
  if (mode !== "bot" && mode !== "agent") return;
12253
13050
  if (surface.getSnapshot().mode === mode) return;
12254
13051
  if (mode === "agent") {
12255
- ui.set((state) => ({ ...state, details: null, view: "details", settings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, toast: null }));
13052
+ ui2.set((state) => ({ ...state, details: null, view: "details", settings: null, palette: null, newChat: null, menu: null, voice: null, exchange: null, memory: null, toast: null }));
12256
13053
  }
12257
13054
  writeMode(mode);
12258
13055
  surface.set((state) => ({ ...state, mode }));
@@ -12279,18 +13076,18 @@ function apply(ctx) {
12279
13076
  beginVisit: (id) => {
12280
13077
  const made = roster.getSnapshot().byId[id] ?? roster.getSnapshot().roomsById[id];
12281
13078
  const seen = seenAt[seenKey(id)] ?? (made?.createdBy && Number.isFinite(made.createdAt) ? made.createdAt : null);
12282
- ui.set((state) => ({ ...state, visits: { ...state.visits, [id]: { seen, entered: Date.now() } } }));
13079
+ ui2.set((state) => ({ ...state, visits: { ...state.visits, [id]: { seen, entered: Date.now() } } }));
12283
13080
  },
12284
13081
  endVisit: (id) => {
12285
13082
  leaveVisits([id]);
12286
- ui.set((state) => {
13083
+ ui2.set((state) => {
12287
13084
  if (!(id in (state.visits ?? {}))) return state;
12288
13085
  const visits = { ...state.visits };
12289
13086
  delete visits[id];
12290
13087
  return { ...state, visits };
12291
13088
  });
12292
13089
  },
12293
- toggleReaction: (key, index, emoji) => ui.set((state) => {
13090
+ toggleReaction: (key, index, emoji) => ui2.set((state) => {
12294
13091
  const entry = { ...state.reactions?.[key] ?? {} };
12295
13092
  const list = entry[index] ?? [];
12296
13093
  entry[index] = list.includes(emoji) ? list.filter((item) => item !== emoji) : [...list, emoji];
@@ -12304,19 +13101,19 @@ function apply(ctx) {
12304
13101
  }
12305
13102
  return { ...state, reactions };
12306
13103
  }),
12307
- quote: (sessionId, text) => ui.set((state) => ({ ...state, quote: { sessionId, text, token: Date.now() } })),
12308
- clearQuote: (token) => ui.set((state) => state.quote?.token === token ? { ...state, quote: null } : state),
13104
+ quote: (sessionId, text) => ui2.set((state) => ({ ...state, quote: { sessionId, text, token: Date.now() } })),
13105
+ clearQuote: (token) => ui2.set((state) => state.quote?.token === token ? { ...state, quote: null } : state),
12309
13106
  clearUnread: (id) => setUnread(id, false),
12310
13107
  setFlags: (id, flags) => after(call("set-flags", { id, ...flags })),
12311
13108
  deleteRoom: (id) => after(call("delete-room", { id })),
12312
13109
  openMenu: (menu) => {
12313
- ui.set((state) => ({ ...state, menu }));
13110
+ ui2.set((state) => ({ ...state, menu }));
12314
13111
  },
12315
13112
  closeMenu: () => {
12316
- ui.set((state) => state.menu ? { ...state, menu: null } : state);
13113
+ ui2.set((state) => state.menu ? { ...state, menu: null } : state);
12317
13114
  },
12318
13115
  closeOverlay: () => {
12319
- ui.set((state) => ({ ...state, newChat: null, exchange: null, details: state.newChat || state.exchange ? state.details : null }));
13116
+ ui2.set((state) => ({ ...state, newChat: null, exchange: null, details: state.newChat || state.exchange ? state.details : null }));
12320
13117
  },
12321
13118
  createBot: (payload) => after(call("create-bot", payload)),
12322
13119
  createRoom: (members, options = {}) => after(call("create-room", { members, ...options })),
@@ -12332,6 +13129,7 @@ function apply(ctx) {
12332
13129
  send: (sessionId, text) => after(call("send", { sessionId: partFor(sessionId), text })),
12333
13130
  answerQuestion: (sessionId, questionId, answer) => after(call("answer-question", { sessionId: partFor(sessionId), questionId, answer })),
12334
13131
  dismissQuestion: (sessionId) => after(call("dismiss-question", { sessionId })),
13132
+ answerApproval: (id, choice) => after(call("approval-answer", { id, choice })),
12335
13133
  // Values go out in these requests and never come back in a response.
12336
13134
  secretSet: (payload) => after(call("secret-set", payload)),
12337
13135
  secretAllow: (requestId) => after(call("secret-allow", { requestId })),
@@ -12359,9 +13157,9 @@ function apply(ctx) {
12359
13157
  // A failed action away from any form (a menu, the palette) shows as a toast.
12360
13158
  reportError: (error, where = "action") => {
12361
13159
  noteClientError(where, error);
12362
- ui.set((state) => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } }));
13160
+ ui2.set((state) => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } }));
12363
13161
  },
12364
- dismissToast: (token) => ui.set((state) => state.toast && (token === void 0 || state.toast.token === token) ? { ...state, toast: null } : state)
13162
+ dismissToast: (token) => ui2.set((state) => state.toast && (token === void 0 || state.toast.token === token) ? { ...state, toast: null } : state)
12365
13163
  };
12366
13164
  if (browsers) {
12367
13165
  Object.assign(actions, {
@@ -12371,7 +13169,7 @@ function apply(ctx) {
12371
13169
  if (!bot) return;
12372
13170
  browsers.ensureTabs(bot.id);
12373
13171
  if (browsers.tabs.getSnapshot()[bot.id]?.active == null) browsers.openTab(bot.id);
12374
- ui.set((state) => ({ ...state, details: id, view: "browser", renaming: false, tab: null }));
13172
+ ui2.set((state) => ({ ...state, details: id, view: "browser", renaming: false, tab: null }));
12375
13173
  },
12376
13174
  // '+' on the strip or the drawer's new-tab card: a blank tab in the browser view.
12377
13175
  browserNewTab: (id) => {
@@ -12379,7 +13177,7 @@ function apply(ctx) {
12379
13177
  if (!bot) return null;
12380
13178
  browsers.ensureTabs(bot.id);
12381
13179
  const tabId = browsers.openTab(bot.id);
12382
- ui.set((state) => ({ ...state, details: id, view: "browser", renaming: false, tab: null }));
13180
+ ui2.set((state) => ({ ...state, details: id, view: "browser", renaming: false, tab: null }));
12383
13181
  return tabId;
12384
13182
  },
12385
13183
  // A drawer card: its tab active, the panel on the browser view.
@@ -12387,7 +13185,7 @@ function apply(ctx) {
12387
13185
  const bot = roster.getSnapshot().byId[id];
12388
13186
  if (!bot) return;
12389
13187
  browsers.activate(bot.id, tabId);
12390
- ui.set((state) => ({ ...state, details: id, view: "browser" }));
13188
+ ui2.set((state) => ({ ...state, details: id, view: "browser" }));
12391
13189
  },
12392
13190
  browserEnsure: (botId) => browsers.ensureTabs(botId),
12393
13191
  browserCloseTab: (botId, tabId) => browsers.closeTab(botId, tabId),
@@ -12400,22 +13198,22 @@ function apply(ctx) {
12400
13198
  }
12401
13199
  actions.setPanelView = (view) => {
12402
13200
  if (view !== "details" && view !== "browser") return;
12403
- ui.set((state) => state.details && state.view !== view ? { ...state, view } : state);
13201
+ ui2.set((state) => state.details && state.view !== view ? { ...state, view } : state);
12404
13202
  };
12405
13203
  actions.closePanel = () => {
12406
- ui.set((state) => state.details ? { ...state, details: null, view: "details", renaming: false, tab: null } : state);
13204
+ ui2.set((state) => state.details ? { ...state, details: null, view: "details", renaming: false, tab: null } : state);
12407
13205
  };
12408
13206
  actions.signInEntry = () => ctx.slots.entries?.("settings.models.sign-in")?.[0] ?? null;
12409
13207
  actions.canSignIn = () => Boolean(actions.signInEntry());
12410
- actions.signIn = () => ui.set((state) => ({ ...state, signIn: (state.signIn ?? 0) + 1 }));
12411
- actions.closeSignIn = () => ui.set((state) => ({ ...state, signIn: null }));
13208
+ actions.signIn = () => ui2.set((state) => ({ ...state, signIn: (state.signIn ?? 0) + 1 }));
13209
+ actions.closeSignIn = () => ui2.set((state) => ({ ...state, signIn: null }));
12412
13210
  actions.accountT = () => ctx.get("locale").bind("settings.account");
12413
13211
  actions.speech = () => ctx.get("remote.speech") ?? void 0;
12414
13212
  actions.voiceSetup = () => {
12415
13213
  const navigation = ctx.get("pluginNavigation");
12416
13214
  return navigation ? () => navigation.openBundle(VOICE_BUNDLE) : null;
12417
13215
  };
12418
- actions.seatAccount = (on) => ui.set((state) => state.accountSeated === on ? state : { ...state, accountSeated: on });
13216
+ actions.seatAccount = (on) => ui2.set((state) => state.accountSeated === on ? state : { ...state, accountSeated: on });
12419
13217
  actions.installUpdate = async () => {
12420
13218
  const value = await call("update-install");
12421
13219
  update.set((previous) => ({ ...previous, ...value }));
@@ -12426,7 +13224,17 @@ function apply(ctx) {
12426
13224
  update.set((previous) => ({ ...previous, dsh }));
12427
13225
  wakeUpdate();
12428
13226
  };
12429
- actions.openAppUpdates = () => window.dshDesktop?.updates?.open?.();
13227
+ actions.openAppUpdates = async (verified) => {
13228
+ const app = await appUpdates?.status?.();
13229
+ if (app) update.set((previous) => ({ ...previous, app }));
13230
+ if (sameVersion(app?.version, verified)) await appUpdates.open();
13231
+ };
13232
+ actions.checkUpdate = async () => {
13233
+ const value = await call("update-check", { fresh: true });
13234
+ update.set((previous) => ({ ...value, app: previous.app }));
13235
+ wakeUpdate();
13236
+ };
13237
+ actions.repoStars = () => call("repo-stars");
12430
13238
  actions.noteTurnTime = (sessionId, turn, key, time) => {
12431
13239
  if (sessionId === void 0 || !Number.isFinite(turn) || !Number.isFinite(time)) return;
12432
13240
  turnClock.set((state) => {
@@ -12466,7 +13274,7 @@ function apply(ctx) {
12466
13274
  });
12467
13275
  const browserPages = browsers?.pages ?? createSource({});
12468
13276
  const browserTabs = browsers?.tabs ?? createSource({});
12469
- const face = () => ({ actions, hooks: { roster, ui, activity, memoryNews, registry, turnClock, live, browserPages, browserTabs, surface, update } });
13277
+ const face = () => ({ actions, hooks: { roster, ui: ui2, activity, work, memoryNews, registry, turnClock, live, browserPages, browserTabs, surface, update } });
12470
13278
  const assistantFace = (sessionId) => {
12471
13279
  const binding = ctx.sessions.binding(sessionId);
12472
13280
  if (binding === void 0) throw new Error(`ds-bot: unknown session "${sessionId}"`);
@@ -12476,10 +13284,41 @@ function apply(ctx) {
12476
13284
  const snapshot = chat.getSnapshot();
12477
13285
  if (snapshot === void 0) throw new Error("ds-bot: Chat target is unavailable");
12478
13286
  return snapshot.nodes.turnDataSource(Number(turn), "assistant-step");
13287
+ },
13288
+ turnContext: (turn) => {
13289
+ const snapshot = chat.getSnapshot();
13290
+ if (snapshot === void 0) throw new Error("ds-bot: Chat target is unavailable");
13291
+ return snapshot.nodes.turnDataSource(Number(turn), "context");
12479
13292
  }
12480
13293
  } };
12481
13294
  };
12482
13295
  const shadow = (name, options, component) => ctx.slots.inject(name, () => ctx.slots.register({ name, ...options }, component));
13296
+ let atMenu = null;
13297
+ const installAtMenu = () => {
13298
+ const fiber = ctx.inject(["inputTriggers"], (scope) => {
13299
+ scope.effect(() => {
13300
+ const triggers = scope.inputTriggers;
13301
+ const controllerOf = (sessionId) => {
13302
+ const binding = ctx.sessions.binding(sessionId);
13303
+ return binding === void 0 ? void 0 : triggers.sessionOf(binding.ctx);
13304
+ };
13305
+ const launched = (sessionId) => controllerOf(sessionId)?.launcher.getSnapshot() === CHATS_SOURCE;
13306
+ const offs = atSources({ roster, sessions: ctx.sessions, launched, iconFor: menuIcons(roster) }).map((source) => triggers.registerSource(source));
13307
+ atMenu = { controllerOf };
13308
+ return () => {
13309
+ atMenu = null;
13310
+ for (const off of offs) off();
13311
+ };
13312
+ }, "ds-bot: @ menu");
13313
+ });
13314
+ return () => fiber.dispose();
13315
+ };
13316
+ actions.canReferenceChat = () => atMenu !== null;
13317
+ actions.referenceChat = (sessionId, span) => {
13318
+ const controller = atMenu?.controllerOf(sessionId);
13319
+ if (controller === void 0 || span === void 0) return;
13320
+ controller.toggleSource(CHATS_SOURCE, { trigger: "@", query: "", quoted: false, position: "inline", span });
13321
+ };
12483
13322
  const installShell = () => [
12484
13323
  installCompiledStyles(SHELL_CSS, "shell"),
12485
13324
  installCompiledStyles(SIDEBAR_CSS, "sidebar"),
@@ -12488,6 +13327,9 @@ function apply(ctx) {
12488
13327
  installCompiledStyles(USAGE_CSS, "usage"),
12489
13328
  installCompiledStyles(SCHEDULE_CSS, "schedules"),
12490
13329
  installCompiledStyles(FEEDBACK_CSS, "feedback"),
13330
+ installCompiledStyles(ABOUT_CSS, "about"),
13331
+ installCompiledStyles(MEMBER_CSS, "members"),
13332
+ installCompiledStyles(USER_MESSAGE_CSS, "user-messages"),
12491
13333
  browsers ? installCompiledStyles(BROWSER_CSS, "browser") : null,
12492
13334
  installTheme(),
12493
13335
  shadow("sidebar.workspaces", { priority: -10, inject: face }, TeamSidebar),
@@ -12509,6 +13351,7 @@ function apply(ctx) {
12509
13351
  installCompiledStyles(CONVERSATION_CSS, "conversation"),
12510
13352
  installBubbleTokens(),
12511
13353
  installMentions(),
13354
+ installAtMenu(),
12512
13355
  // Bot conversations are always flat, so the work-details mode is not a choice:
12513
13356
  // the mode is held at `verbose` while this group is mounted.
12514
13357
  holdFlatTranscript(ctx.configForms.get("ui-chat")),
@@ -12522,6 +13365,7 @@ function apply(ctx) {
12522
13365
  // every message.
12523
13366
  shadow("conversation.chat.node", { key: "question-reply", priority: -10, locale: "chat" }, AnsweredCell),
12524
13367
  shadow("conversation.chat.node", { key: "compaction", priority: -10, locale: "chat" }, Nothing),
13368
+ ...canDrawUserMessages() ? ["user", "steering"].map((key) => shadow("conversation.chat.node", { key, priority: -10, locale: "chat", inject: face }, UserMessageCell)) : [],
12525
13369
  shadow("conversation.input.permission", { priority: -10 }, Nothing),
12526
13370
  shadow("conversation.input.plan", { priority: -10 }, Nothing),
12527
13371
  // The composer carries no model picker or run statistics; a Bot's model lives
@@ -12535,6 +13379,7 @@ function apply(ctx) {
12535
13379
  priority: -10,
12536
13380
  select: (owner) => owner.pendingInteraction?.kind === "question" ? owner.pendingInteraction : null
12537
13381
  }, QuestionCard),
13382
+ shadow("conversation.input.dock", { id: "bot-members", order: -120, inject: face }, MemberDock),
12538
13383
  shadow("conversation.input.dock", { id: "bot-model", order: -110, inject: face }, ModelChoiceDock),
12539
13384
  shadow("conversation.input.dock", { id: "bot-question", order: -100, inject: face }, QuestionDock),
12540
13385
  shadow("conversation.input.dock", { id: "bot-secret", order: -90, inject: face }, SecretDock)