ds-bot 0.1.0 → 0.1.2

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
@@ -217,6 +217,10 @@ var ZH = {
217
217
  "Click again to delete. Its chats are archived.": "再点一次删除,它的对话会归档。",
218
218
  "Removes the Bot from the team.": "把这个 Bot 从团队里移除。",
219
219
  "Pick a model and {name} can start.": "选好模型,{name} 就能开始工作。",
220
+ "Permissions": "权限",
221
+ "Read only": "仅可查看",
222
+ "Workspace": "工作区内修改",
223
+ "Full access": "完全权限",
220
224
  "The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.": "这是唯一的主 Bot。要把主 Bot 交给别的 Bot,请到「Bot」页的主 Bot 里换。",
221
225
  Change: "更换",
222
226
  "Can create and change Bots, and runs every group chat.": "可以创建和修改 Bot,并管理所有群聊。",
@@ -245,6 +249,7 @@ var ZH = {
245
249
  Create: "创建",
246
250
  // Model menu
247
251
  Default: "默认",
252
+ "Thinking level": "思考强度",
248
253
  "Model: {model}": "模型:{model}",
249
254
  none: "无",
250
255
  "Choose a model": "选择模型",
@@ -368,6 +373,13 @@ var ZH = {
368
373
  "The update did not finish: {reason}": "更新没有完成:{reason}",
369
374
  "What's new": "更新说明",
370
375
  "Updating…": "正在更新…",
376
+ "DSH {version} is available": "DSH {version} 可以更新",
377
+ "Updating DSH…": "正在更新 DSH…",
378
+ "DSH update": "DSH 更新",
379
+ "DSH {version}": "DSH {version}",
380
+ "Restart DSH to start using DSH {version}.": "重启 DSH 后开始使用 DSH {version}。",
381
+ "Update DS Bot {version} first.": "先更新 DS Bot {version}。",
382
+ "Open updates": "打开更新",
371
383
  // Command palette
372
384
  Messages: "消息",
373
385
  Commands: "命令",
@@ -601,6 +613,10 @@ var ZH = {
601
613
  "Resume listening": "继续听",
602
614
  Pause: "暂停",
603
615
  "End voice chat": "结束语音对话",
616
+ "Recognizing…": "识别中…",
617
+ "Voice input needs its speech model": "语音输入还没有语音模型",
618
+ "Set up voice input": "设置语音输入",
619
+ "That was too long to recognize": "这段太长了,识别不了",
604
620
  // Usage page
605
621
  "24 hours": "24 小时",
606
622
  "7 days": "7 天",
@@ -667,6 +683,25 @@ var ZH = {
667
683
  "Compared with the day before": "与前一天相比",
668
684
  "Compared with the {count} days before": "与再往前 {count} 天相比",
669
685
  "{share}% of all usage from {span}": "占 {span} 全部用量的 {share}%",
686
+ // About
687
+ "About DS Bot": "关于 DS Bot",
688
+ "Version {version}": "版本 {version}",
689
+ "Star DS Bot on GitHub": "在 GitHub 上给 DS Bot 点 Star",
690
+ "{count} stars": "{count} 个 Star",
691
+ "If DS Bot helps you, a star on GitHub helps more people find it.": "如果 DS Bot 对你有用,欢迎在 GitHub 上点个 Star,让更多人看到它。",
692
+ Updates: "更新",
693
+ "Check for updates": "检查更新",
694
+ "Checking…": "正在检查…",
695
+ "Up to date": "已是最新版本",
696
+ "Checked {time}": "检查于 {time}",
697
+ "Could not check for updates.": "没能检查更新。",
698
+ "Update checks are off.": "已关闭更新检查。",
699
+ "Installed from a local folder; update it there.": "从本地文件夹安装,请在那里更新。",
700
+ "Updates are not offered for this install.": "这种安装方式不提供更新。",
701
+ Links: "链接",
702
+ "Source code": "源代码",
703
+ "Release notes": "发布说明",
704
+ Open: "打开",
670
705
  // Feedback and errors
671
706
  Feedback: "反馈",
672
707
  "Report a problem": "报告问题",
@@ -802,6 +837,7 @@ var HOST_ERRORS = [
802
837
  ];
803
838
  var HOST_ZH = {
804
839
  "Request failed": "请求失败",
840
+ "Permissions are not available in this DeepSeek Harness": "这个 DeepSeek Harness 不支持权限设置",
805
841
  "Avatar images must be PNG, JPEG, WebP, or GIF": "头像图片必须是 PNG、JPEG、WebP 或 GIF",
806
842
  "Avatar image is too large": "头像图片太大了",
807
843
  "A Bot needs a name": "Bot 需要一个名字",
@@ -1077,8 +1113,8 @@ function bodyOf(def, paint, flat = false) {
1077
1113
  return "";
1078
1114
  }
1079
1115
  }
1080
- 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>`;
1081
- 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>`;
1116
+ var eyeMarkup = ([cx, cy, w, h30, tilt]) => `<g transform="translate(${cx} ${cy}) rotate(${tilt})"><rect class="bt-eye" x="${-w / 2}" y="${-h30 / 2}" width="${w}" height="${h30}" rx="${w / 2}"/></g>`;
1117
+ var happyEyeMarkup = ([cx, cy, w, h30, tilt]) => `<g transform="translate(${cx} ${cy}) rotate(${tilt})"><path class="bt-eye-happy" opacity="0" d="M${-w * 0.62} ${h30 * 0.14}Q0 ${-h30 * 0.42} ${w * 0.62} ${h30 * 0.14}" fill="none" stroke="#000" stroke-width="${w * 0.5}" stroke-linecap="round"/></g>`;
1082
1118
  var maskBox = (size) => `maskUnits="userSpaceOnUse" x="${-size}" y="${-size}" width="${size * 3}" height="${size * 3}"`;
1083
1119
  var holes = (def, eyes) => `<rect x="${-def.size}" y="${-def.size}" width="${def.size * 3}" height="${def.size * 3}" fill="#fff"/>${eyes}`;
1084
1120
  var MASK_ID = "%MASK%";
@@ -1453,12 +1489,12 @@ function liveSprings(apply2) {
1453
1489
  const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3));
1454
1490
  last = now;
1455
1491
  const steps = Math.ceil(dt * 240);
1456
- const h29 = dt / steps;
1492
+ const h30 = dt / steps;
1457
1493
  let moving = false;
1458
1494
  for (const value of values.values()) {
1459
1495
  for (let index = 0; index < steps; index += 1) {
1460
- value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h29;
1461
- value.x += value.v * h29;
1496
+ value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h30;
1497
+ value.x += value.v * h30;
1462
1498
  }
1463
1499
  if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) {
1464
1500
  value.x = value.to;
@@ -2014,6 +2050,8 @@ button.bt-q-opt:active{background:var(--bt-active)}
2014
2050
  .bt-set-body{gap:20px}
2015
2051
  .bt-drawer-model{display:flex;flex-direction:column}
2016
2052
  .bt-drawer-model .bt-note{margin-top:6px}
2053
+ .bt-drawer-permission{display:flex;flex-direction:column}
2054
+ .bt-seg button.bt-perm-danger,.bt-seg button.bt-perm-danger[aria-pressed=true]{color:#e02135}
2017
2055
 
2018
2056
  /* Settings dialog: a 198px page list beside the page. */
2019
2057
  .bt-settings-layer{position:fixed;inset:var(--dsh-frame-chrome-top,0px) 0 0;z-index:60;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
@@ -2113,6 +2151,8 @@ button.bt-set-row:hover{background:var(--bt-hover)}
2113
2151
  .bt-voice-status{font-size:13px;line-height:18px;color:var(--bt-ink-2);min-height:18px}
2114
2152
  .bt-voice-caption{min-height:44px;font-size:15px;line-height:22px;color:var(--bt-ink);animation:bt-blur-in .25s ease-out;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}
2115
2153
  .bt-voice-note{font-size:12px;color:#e02135}
2154
+ .bt-voice-setup{height:32px;padding:0 14px;border-radius:16px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;cursor:pointer;transition:background-color .15s ease}
2155
+ .bt-voice-setup:hover{background:var(--bt-active)}
2116
2156
  .bt-voice-bar{display:flex;gap:16px;padding:0 0 32px}
2117
2157
  .bt-voice-btn{position:relative;width:52px;height:52px;border-radius:50%;border:0;background:var(--bt-hover);color:var(--bt-ink);display:grid;place-items:center;cursor:pointer;transition:background-color .15s ease,transform .12s ease}
2118
2158
  .bt-voice-btn:hover{background:var(--bt-active)}
@@ -2871,6 +2911,7 @@ function PinMarkIcon() {
2871
2911
  }
2872
2912
  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"]);
2873
2913
  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"]);
2914
+ 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"]);
2874
2915
  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"]);
2875
2916
  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"]);
2876
2917
  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"]);
@@ -3513,12 +3554,22 @@ var SECRET_CSS = `
3513
3554
  @media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}}
3514
3555
  `;
3515
3556
 
3557
+ // src/client/turn-reasoning.js
3558
+ function turnReasoning(steps, current) {
3559
+ const members = steps.length ? steps : [current];
3560
+ if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return "";
3561
+ return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n");
3562
+ }
3563
+ var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "");
3564
+ var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning");
3565
+ var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim();
3566
+
3516
3567
  // src/client/cells.js
3517
3568
  var import_react6 = require("react");
3518
3569
  var import_react_dom = require("react-dom");
3519
3570
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
3520
3571
  var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") });
3521
- function Bubbles({ texts, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead }) {
3572
+ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead }) {
3522
3573
  const onClick = (event) => {
3523
3574
  const target = mentionTarget(event);
3524
3575
  if (target === null) return;
@@ -3530,10 +3581,10 @@ function Bubbles({ texts, roster, selfId, actions, streaming, sessionId, msgKey,
3530
3581
  return (0, import_react6.createElement)(
3531
3582
  "div",
3532
3583
  { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick },
3533
- texts.map((text, index) => {
3584
+ texts2.map((text, index) => {
3534
3585
  const key = msgKey ? `${msgKey}:${index}` : void 0;
3535
3586
  const picked = key ? reactions?.[index] ?? [] : [];
3536
- const last = index === texts.length - 1;
3587
+ const last = index === texts2.length - 1;
3537
3588
  const live = streaming && last;
3538
3589
  return (0, import_react6.createElement)(
3539
3590
  "div",
@@ -3638,8 +3689,7 @@ function MessageTools({ text, name, sessionId, react, actions }) {
3638
3689
  ), document.body) : null
3639
3690
  );
3640
3691
  }
3641
- function ThinkingRow({ text, running }) {
3642
- if (running || !text) return null;
3692
+ function ThinkingRow({ text }) {
3643
3693
  return (0, import_react6.createElement)(
3644
3694
  "details",
3645
3695
  { className: "bt-think" },
@@ -3647,22 +3697,22 @@ function ThinkingRow({ text, running }) {
3647
3697
  (0, import_react6.createElement)("div", { className: "bt-think-text" }, text)
3648
3698
  );
3649
3699
  }
3650
- function AssistantCell({ node, groupPart, sessionId, useRoster, useUi, actions }) {
3700
+ function AssistantCell({ node, groupPart, sessionId, useRoster, useUi, useTurnSteps, actions }) {
3651
3701
  const roster = useRoster((value) => value);
3652
3702
  const data = node.data;
3703
+ const steps = useTurnSteps(String(data.turn));
3653
3704
  const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn);
3654
- const texts = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text);
3705
+ const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text);
3655
3706
  const running = data.status === "running";
3656
- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts.length > 0 ? { text: true } : null);
3707
+ useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null);
3657
3708
  if (hidden) return null;
3658
- const reasoning = groupPart === "response" ? "" : data.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim();
3659
- const thinkingLive = running && texts.length === 0 && data.blocks.some((block) => block.kind === "reasoning");
3660
- if (texts.length === 0 && !reasoning) return null;
3709
+ const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data);
3710
+ if (texts2.length === 0 && !reasoning) return null;
3661
3711
  return (0, import_react6.createElement)(
3662
3712
  import_react6.Fragment,
3663
3713
  null,
3664
- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning, running: thinkingLive }) : null,
3665
- texts.length ? (0, import_react6.createElement)(Bubbles, { texts, roster, selfId: roster.byId[sessionId]?.id ?? sessionId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : null
3714
+ reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null,
3715
+ 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
3666
3716
  );
3667
3717
  }
3668
3718
  function EventLine({ prefix, bots, joiner = t("and"), onOpen }) {
@@ -4339,6 +4389,16 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, ac
4339
4389
  ), host);
4340
4390
  }
4341
4391
 
4392
+ // src/client/dsh-offer.js
4393
+ var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]);
4394
+ var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0];
4395
+ var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b);
4396
+ function showsDshRow(dsh, app) {
4397
+ if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false;
4398
+ if (dsh.kind === "npm") return true;
4399
+ return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified);
4400
+ }
4401
+
4342
4402
  // src/client/sidebar.js
4343
4403
  var import_react8 = require("react");
4344
4404
  var import_react_dom3 = require("react-dom");
@@ -4667,6 +4727,7 @@ function UpdateButton({ useUpdate, actions }) {
4667
4727
  const [open, setOpen] = (0, import_react8.useState)(false);
4668
4728
  const [box, setBox] = (0, import_react8.useState)(null);
4669
4729
  const [error, setError] = (0, import_react8.useState)("");
4730
+ const [dshError, setDshError] = (0, import_react8.useState)("");
4670
4731
  const button = (0, import_react8.useRef)(null);
4671
4732
  (0, import_react8.useEffect)(() => {
4672
4733
  if (!open) return void 0;
@@ -4689,7 +4750,11 @@ function UpdateButton({ useUpdate, actions }) {
4689
4750
  };
4690
4751
  }, [open]);
4691
4752
  const phase = update?.phase ?? "idle";
4692
- if (!update?.available && phase === "idle") return null;
4753
+ const dsh = update?.dsh;
4754
+ const dshPhase = dsh?.phase ?? "idle";
4755
+ const dshRow = showsDshRow(dsh, update?.app);
4756
+ if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null;
4757
+ const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase;
4693
4758
  const toggle = () => {
4694
4759
  const rect = button.current?.getBoundingClientRect();
4695
4760
  if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 });
@@ -4699,8 +4764,14 @@ function UpdateButton({ useUpdate, actions }) {
4699
4764
  setError("");
4700
4765
  void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure))));
4701
4766
  };
4702
- const label = phase === "installing" ? t("Updating DS Bot…") : phase === "installed" ? t("DS Bot {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : t("DS Bot {version} is available", { version: update.latest });
4767
+ const startDsh = () => {
4768
+ setDshError("");
4769
+ void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure))));
4770
+ };
4771
+ 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 });
4703
4772
  const message = error || (phase === "failed" ? update.error : "");
4773
+ const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : "");
4774
+ 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 });
4704
4775
  return (0, import_react8.createElement)(
4705
4776
  import_react8.Fragment,
4706
4777
  null,
@@ -4708,30 +4779,52 @@ function UpdateButton({ useUpdate, actions }) {
4708
4779
  ref: button,
4709
4780
  type: "button",
4710
4781
  className: "bt-round bt-update",
4711
- "data-phase": phase,
4782
+ "data-phase": cloudPhase,
4712
4783
  "aria-label": label,
4713
4784
  title: label,
4714
4785
  "aria-haspopup": "dialog",
4715
4786
  "aria-expanded": open,
4716
4787
  onClick: toggle
4717
- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
4788
+ }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null),
4718
4789
  open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)(
4719
4790
  "div",
4720
- { className: "bt-update-pop", role: "dialog", "aria-label": t("DS Bot update"), style: box },
4721
- (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })),
4722
- (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })),
4723
- message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
4724
- (0, import_react8.createElement)(
4791
+ { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box },
4792
+ update?.available || phase !== "idle" ? (0, import_react8.createElement)(
4793
+ import_react8.Fragment,
4794
+ null,
4795
+ (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })),
4796
+ (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })),
4797
+ message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
4798
+ (0, import_react8.createElement)(
4799
+ "div",
4800
+ { className: "bt-update-actions" },
4801
+ update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
4802
+ phase === "installed" ? null : (0, import_react8.createElement)("button", {
4803
+ type: "button",
4804
+ className: "bt-update-go",
4805
+ disabled: phase === "installing",
4806
+ onClick: start
4807
+ }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update"))
4808
+ )
4809
+ ) : null,
4810
+ dshRow ? (0, import_react8.createElement)(
4725
4811
  "div",
4726
- { className: "bt-update-actions" },
4727
- update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
4728
- phase === "installed" ? null : (0, import_react8.createElement)("button", {
4729
- type: "button",
4730
- className: "bt-update-go",
4731
- disabled: phase === "installing",
4732
- onClick: start
4733
- }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update"))
4734
- )
4812
+ { className: "bt-update-dsh" },
4813
+ (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
4814
+ (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText),
4815
+ dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
4816
+ (0, import_react8.createElement)(
4817
+ "div",
4818
+ { className: "bt-update-actions" },
4819
+ dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => {
4820
+ }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", {
4821
+ type: "button",
4822
+ className: "bt-update-go",
4823
+ disabled: dshPhase === "installing",
4824
+ onClick: startDsh
4825
+ }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update"))
4826
+ )
4827
+ ) : null
4735
4828
  ), document.body) : null
4736
4829
  );
4737
4830
  }
@@ -5027,6 +5120,7 @@ var SIDEBAR_CSS = `
5027
5120
  .bt-update-go:hover:not(:disabled){opacity:.9}
5028
5121
  .bt-update-go:active:not(:disabled){transform:scale(.96)}
5029
5122
  .bt-update-go:disabled{opacity:.6;cursor:default}
5123
+ .bt-update-dsh:not(:first-child){margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)}
5030
5124
  @media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}}
5031
5125
 
5032
5126
  /* The DSH Agent row and its expanding Session list. The black whale is the
@@ -5354,10 +5448,11 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) {
5354
5448
  }
5355
5449
  }, [quote?.token]);
5356
5450
  if (sessionId === void 0) return null;
5451
+ const pluginMic = actions.speech() !== void 0;
5357
5452
  return (0, import_react10.createElement)(
5358
5453
  import_react10.Fragment,
5359
5454
  null,
5360
- (0, import_react10.createElement)("button", {
5455
+ pluginMic ? null : (0, import_react10.createElement)("button", {
5361
5456
  type: "button",
5362
5457
  className: "bt-mic",
5363
5458
  "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"),
@@ -5449,6 +5544,132 @@ function ComposerPlus({ sessionId, useRoster, actions }) {
5449
5544
  );
5450
5545
  }
5451
5546
 
5547
+ // src/client/speech.js
5548
+ var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle";
5549
+ var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]);
5550
+ async function speechCatalog(speech) {
5551
+ const result = await speech.catalog();
5552
+ if (!result?.ok) throw new Error(result?.error?.message ?? "Speech catalog failed");
5553
+ const catalog = result.value;
5554
+ const provider = catalog.providers.find((item) => item.id === catalog.selection.providerId);
5555
+ return { catalog, usable: USABLE.has(provider?.preparation?.phase) };
5556
+ }
5557
+ function waveBytes(samples, rate = 16e3) {
5558
+ const bytes = new Uint8Array(44 + samples.length * 2);
5559
+ const view = new DataView(bytes.buffer);
5560
+ const ascii = (at, text) => {
5561
+ for (let index = 0; index < text.length; index += 1) bytes[at + index] = text.charCodeAt(index);
5562
+ };
5563
+ ascii(0, "RIFF");
5564
+ view.setUint32(4, 36 + samples.length * 2, true);
5565
+ ascii(8, "WAVEfmt ");
5566
+ view.setUint32(16, 16, true);
5567
+ view.setUint16(20, 1, true);
5568
+ view.setUint16(22, 1, true);
5569
+ view.setUint32(24, rate, true);
5570
+ view.setUint32(28, rate * 2, true);
5571
+ view.setUint16(32, 2, true);
5572
+ view.setUint16(34, 16, true);
5573
+ ascii(36, "data");
5574
+ view.setUint32(40, samples.length * 2, true);
5575
+ samples.forEach((sample, index) => {
5576
+ const value = Math.max(-1, Math.min(1, sample));
5577
+ view.setInt16(44 + index * 2, value < 0 ? Math.round(value * 32768) : Math.round(value * 32767), true);
5578
+ });
5579
+ return bytes;
5580
+ }
5581
+ function base64Of(bytes) {
5582
+ let text = "";
5583
+ for (let index = 0; index < bytes.length; index += 32768) text += String.fromCharCode(...bytes.subarray(index, index + 32768));
5584
+ return btoa(text);
5585
+ }
5586
+ var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause });
5587
+ async function openCapture() {
5588
+ const devices = navigator.mediaDevices;
5589
+ if (!devices?.getUserMedia || typeof MediaRecorder === "undefined") throw captureError("unavailable");
5590
+ let stream;
5591
+ try {
5592
+ stream = await devices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false });
5593
+ } catch (error) {
5594
+ throw captureError(error?.name === "NotAllowedError" ? "blocked" : "failed", error);
5595
+ }
5596
+ const audio = new AudioContext();
5597
+ const analyser = audio.createAnalyser();
5598
+ analyser.fftSize = 512;
5599
+ audio.createMediaStreamSource(stream).connect(analyser);
5600
+ const frame = new Float32Array(analyser.fftSize);
5601
+ const recorder = new MediaRecorder(stream);
5602
+ const chunks = [];
5603
+ recorder.ondataavailable = (event) => {
5604
+ if (event.data.size > 0) chunks.push(event.data);
5605
+ };
5606
+ recorder.start();
5607
+ let closed = false;
5608
+ const close = () => {
5609
+ if (closed) return;
5610
+ closed = true;
5611
+ if (recorder.state !== "inactive") recorder.stop();
5612
+ stream.getTracks().forEach((track) => track.stop());
5613
+ void audio.close().catch(() => {
5614
+ });
5615
+ };
5616
+ return {
5617
+ level() {
5618
+ if (closed) return 0;
5619
+ analyser.getFloatTimeDomainData(frame);
5620
+ let sum = 0;
5621
+ for (const sample of frame) sum += sample * sample;
5622
+ return Math.min(1, Math.sqrt(sum / frame.length) * 4);
5623
+ },
5624
+ cancel: close,
5625
+ async finish(maxSeconds) {
5626
+ if (closed || recorder.state === "inactive") {
5627
+ close();
5628
+ return null;
5629
+ }
5630
+ try {
5631
+ await new Promise((resolve) => {
5632
+ recorder.onstop = resolve;
5633
+ recorder.stop();
5634
+ });
5635
+ stream.getTracks().forEach((track) => track.stop());
5636
+ const blob = new Blob(chunks, { type: recorder.mimeType });
5637
+ if (blob.size === 0) return null;
5638
+ const decoded = await audio.decodeAudioData(await blob.arrayBuffer());
5639
+ const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3));
5640
+ const offline = new OfflineAudioContext(1, length, 16e3);
5641
+ const source = offline.createBufferSource();
5642
+ source.buffer = decoded;
5643
+ source.connect(offline.destination);
5644
+ source.start();
5645
+ return waveBytes((await offline.startRendering()).getChannelData(0));
5646
+ } finally {
5647
+ close();
5648
+ }
5649
+ }
5650
+ };
5651
+ }
5652
+ async function transcribeWave(speech, catalog, wave, signal) {
5653
+ if (wave.byteLength > catalog.maxAudioBytes) throw captureError("too-long");
5654
+ const result = await speech.transcribe({ audioBase64: base64Of(wave), ...catalog.selection }, signal);
5655
+ if (!result?.ok) throw Object.assign(new Error(result?.error?.message ?? "Transcription failed"), { kind: "service" });
5656
+ return String(result.value?.text ?? "").trim();
5657
+ }
5658
+ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) {
5659
+ let spokeAt = 0;
5660
+ let quietSince = 0;
5661
+ return (level, now) => {
5662
+ if (level >= loud) {
5663
+ spokeAt ||= now;
5664
+ quietSince = 0;
5665
+ return "speech";
5666
+ }
5667
+ if (!spokeAt) return "waiting";
5668
+ if (level < quiet) quietSince ||= now;
5669
+ return quietSince && now - quietSince >= quietMs ? "done" : "speech";
5670
+ };
5671
+ }
5672
+
5452
5673
  // src/client/overlay-hooks.js
5453
5674
  var import_react11 = require("react");
5454
5675
  var escapeLayers = [];
@@ -5680,10 +5901,19 @@ var MENU_EXIT_MS = 120;
5680
5901
  function groupModels(models) {
5681
5902
  const groups = /* @__PURE__ */ new Map();
5682
5903
  for (const model of models ?? []) {
5683
- if (!groups.has(model.providerName)) groups.set(model.providerName, []);
5684
- groups.get(model.providerName).push(model);
5904
+ const provider = model.provider ?? model.providerName ?? model.ref.split("/")[0];
5905
+ if (!groups.has(provider)) groups.set(provider, []);
5906
+ groups.get(provider).push(model);
5907
+ }
5908
+ const names = /* @__PURE__ */ new Map();
5909
+ for (const [provider, list] of groups) {
5910
+ const name = list[0].providerName ?? provider;
5911
+ names.set(name, (names.get(name) ?? 0) + 1);
5685
5912
  }
5686
- return [...groups];
5913
+ return [...groups].map(([provider, list]) => {
5914
+ const name = list[0].providerName ?? provider;
5915
+ return [names.get(name) > 1 ? `${name} (${provider})` : name, list];
5916
+ });
5687
5917
  }
5688
5918
  var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref);
5689
5919
  function modelOf(bot, roster) {
@@ -5693,10 +5923,57 @@ function modelOf(bot, roster) {
5693
5923
  const [provider, ...rest] = ref.split("/");
5694
5924
  return { ref, name: served?.name ?? rest.join("/"), provider: served?.providerName ?? provider, served: served !== void 0 };
5695
5925
  }
5926
+ var effortCache = /* @__PURE__ */ new Map();
5927
+ function useEfforts(served, ref, actions) {
5928
+ const [fetched, setFetched] = (0, import_react13.useState)(void 0);
5929
+ (0, import_react13.useEffect)(() => {
5930
+ setFetched(void 0);
5931
+ if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0;
5932
+ let live = true;
5933
+ if (effortCache.has(ref)) setFetched(effortCache.get(ref));
5934
+ else void Promise.resolve(actions.efforts(ref)).then((value) => {
5935
+ const reasoning = value?.efforts ? value : null;
5936
+ effortCache.set(ref, reasoning);
5937
+ if (live) setFetched(reasoning);
5938
+ }).catch(() => {
5939
+ });
5940
+ return () => {
5941
+ live = false;
5942
+ };
5943
+ }, [ref, served?.reasoning !== void 0, actions]);
5944
+ return served?.reasoning ?? fetched;
5945
+ }
5946
+ function effortRows(reasoning) {
5947
+ if (reasoning?.efforts === void 0 || reasoning.efforts.length === 0) return [];
5948
+ return [
5949
+ { effort: void 0, label: t("Default") },
5950
+ ...reasoning.efforts.map((effort) => ({ effort: effort.id, label: effort.name }))
5951
+ ];
5952
+ }
5953
+ function EffortRows({ reasoning, current, onPick }) {
5954
+ return (0, import_react13.createElement)(
5955
+ import_react13.Fragment,
5956
+ null,
5957
+ (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
5958
+ (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
5959
+ effortRows(reasoning).map((row) => (0, import_react13.createElement)(
5960
+ "button",
5961
+ {
5962
+ key: row.effort ?? "default",
5963
+ type: "button",
5964
+ role: "menuitemradio",
5965
+ "aria-checked": row.effort === current,
5966
+ onClick: () => onPick(row.effort)
5967
+ },
5968
+ (0, import_react13.createElement)("span", { className: "bt-menu-label" }, row.label),
5969
+ (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react13.createElement)(CheckIcon) : null)
5970
+ ))
5971
+ );
5972
+ }
5696
5973
  function ModelRows({ roster, current, onPick }) {
5697
5974
  return groupModels(roster.models).map(([provider, list]) => (0, import_react13.createElement)(
5698
5975
  import_react13.Fragment,
5699
- { key: provider },
5976
+ { key: list[0].provider ?? provider },
5700
5977
  (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
5701
5978
  list.map((model) => (0, import_react13.createElement)(
5702
5979
  "button",
@@ -5714,13 +5991,18 @@ function ModelRows({ roster, current, onPick }) {
5714
5991
  ))
5715
5992
  ));
5716
5993
  }
5717
- function ModelMenu({ bot, roster, actions, run, wide = false, onPick }) {
5994
+ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) {
5718
5995
  const [place, setPlace] = (0, import_react13.useState)(null);
5719
5996
  const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS);
5720
5997
  const button = (0, import_react13.useRef)(null);
5721
5998
  const menu = (0, import_react13.useRef)(null);
5722
5999
  const shown = modelOf(bot, roster);
5723
6000
  const current = bot.model ?? bot.appliedModel;
6001
+ const served = servedOf(roster, current);
6002
+ const reasoning = useEfforts(served, current, actions);
6003
+ const picked = reasoning?.efforts?.some((effort) => effort.id === bot.effort) ? bot.effort : void 0;
6004
+ const currentEffort = picked ?? reasoning?.defaultEffort;
6005
+ const effortLabel = currentEffort === void 0 ? reasoning?.efforts?.length > 0 ? t("Default") : void 0 : reasoning.efforts.find((effort) => effort.id === currentEffort)?.name ?? currentEffort;
5724
6006
  const close = () => setPlace(null);
5725
6007
  (0, import_react13.useEffect)(() => {
5726
6008
  if (place === null) return void 0;
@@ -5764,6 +6046,11 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick }) {
5764
6046
  if (onPick) onPick(ref);
5765
6047
  else if (ref !== current || !bot.model) void run(actions.updateBot(bot.id, { model: ref }));
5766
6048
  };
6049
+ const pickEffort = (effort) => {
6050
+ close();
6051
+ if (onPickEffort) onPickEffort(effort);
6052
+ else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null }));
6053
+ };
5767
6054
  const models = roster.models ?? [];
5768
6055
  return (0, import_react13.createElement)(
5769
6056
  import_react13.Fragment,
@@ -5785,7 +6072,7 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick }) {
5785
6072
  "span",
5786
6073
  { className: "bt-model-copy" },
5787
6074
  (0, import_react13.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")),
5788
- shown ? (0, import_react13.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? shown.provider : t("{provider} · not available", { provider: shown.provider })) : null
6075
+ shown ? (0, import_react13.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null
5789
6076
  ),
5790
6077
  (0, import_react13.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react13.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" }))
5791
6078
  ),
@@ -5793,6 +6080,7 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick }) {
5793
6080
  "div",
5794
6081
  { ref: menu, className: `bt-menu bt-model-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Model for {name}", { name: bot.name }), style: shownPlace, "data-leaving": leaving || void 0 },
5795
6082
  models.length > 0 ? (0, import_react13.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react13.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")),
6083
+ effortRows(reasoning).length > 0 ? (0, import_react13.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
5796
6084
  (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
5797
6085
  (0, import_react13.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
5798
6086
  close();
@@ -5824,15 +6112,21 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) {
5824
6112
  };
5825
6113
  const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name });
5826
6114
  const choices = models.length === 0 ? setupChoices(actions).map(([label, run]) => (0, import_react13.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)) : [
5827
- ...models.map((model) => (0, import_react13.createElement)("button", {
5828
- key: model.ref,
5829
- type: "button",
5830
- className: "bt-model-chip",
5831
- disabled: busy !== "",
5832
- "aria-busy": busy === model.ref || void 0,
5833
- title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref,
5834
- onClick: () => pick2(model.ref)
5835
- }, model.name, busy === model.ref ? (0, import_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)),
6115
+ ...groupModels(models).map(([provider, list]) => (0, import_react13.createElement)(
6116
+ "div",
6117
+ { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider },
6118
+ (0, import_react13.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
6119
+ (0, import_react13.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react13.createElement)("button", {
6120
+ key: model.ref,
6121
+ type: "button",
6122
+ className: "bt-model-chip",
6123
+ disabled: busy !== "",
6124
+ "aria-busy": busy === model.ref || void 0,
6125
+ "aria-label": `${model.name} · ${provider}`,
6126
+ title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref,
6127
+ onClick: () => pick2(model.ref)
6128
+ }, model.name, busy === model.ref ? (0, import_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)))
6129
+ )),
5836
6130
  (0, import_react13.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessSettings }, (0, import_react13.createElement)(SlidersIcon), t("Manage models"))
5837
6131
  ];
5838
6132
  return (0, import_react13.createElement)(
@@ -5905,6 +6199,9 @@ var MODEL_CSS = `
5905
6199
  .bt-model-none{padding:8px;font-size:13px;line-height:18px;color:var(--bt-ink-2)}
5906
6200
  .bt-model-dock{width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:0 0 10px;box-sizing:border-box;animation:bt-model-in ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards}
5907
6201
  .bt-model-chips{display:flex;flex-wrap:wrap;gap:6px;margin-left:30px;max-width:min(560px,calc(100% - 30px));max-height:min(240px,36vh);overflow-y:auto;scrollbar-width:thin}
6202
+ .bt-model-choice-group{display:flex;flex-direction:column;gap:6px;width:100%;min-width:0}
6203
+ .bt-model-choice-provider{padding-top:4px;font-size:12px;line-height:16px;font-weight:500;color:var(--bt-ink-2);overflow-wrap:anywhere}
6204
+ .bt-model-choice-chips{display:flex;flex-wrap:wrap;gap:6px}
5908
6205
  .bt-model-chip{display:inline-flex;align-items:center;gap:6px;min-height:30px;padding:5px 12px;border:.5px solid var(--bt-line-2);border-radius:999px;background:var(--bt-surface,transparent);color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}
5909
6206
  .bt-model-chip:hover:not(:disabled){background:var(--bt-hover)}
5910
6207
  .bt-model-chip:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px}
@@ -7031,9 +7328,10 @@ function startModel(roster) {
7031
7328
  const models = roster.models ?? [];
7032
7329
  return (models.find((model) => model.ref === roster.defaultModel) ?? models[0])?.ref ?? "";
7033
7330
  }
7034
- function createPayload({ name, look, image, model, brief }) {
7331
+ function createPayload({ name, look, image, model, effort, brief }) {
7035
7332
  const payload = { name, color: look.color, avatar: image ? { shape: look.shape, image } : { shape: look.shape } };
7036
7333
  if (model) payload.model = model;
7334
+ if (effort) payload.effort = effort;
7037
7335
  if (brief) payload.brief = brief;
7038
7336
  return payload;
7039
7337
  }
@@ -7051,6 +7349,7 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
7051
7349
  const [image, setImage] = (0, import_react17.useState)(null);
7052
7350
  const [name, setName] = (0, import_react17.useState)(start?.name ?? "");
7053
7351
  const [model, setModel] = (0, import_react17.useState)(() => startModel(roster));
7352
+ const [effort, setEffort] = (0, import_react17.useState)(void 0);
7054
7353
  const [busy, setBusy] = (0, import_react17.useState)(false);
7055
7354
  const [error, setError] = (0, import_react17.useState)("");
7056
7355
  const fileRef = (0, import_react17.useRef)(null);
@@ -7067,7 +7366,7 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
7067
7366
  setError("");
7068
7367
  setBusy(true);
7069
7368
  try {
7070
- const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, brief }));
7369
+ const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort, brief }));
7071
7370
  onCreated(bot);
7072
7371
  } catch (failure) {
7073
7372
  setError(failure?.message ?? String(failure));
@@ -7151,7 +7450,10 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
7151
7450
  "div",
7152
7451
  { className: "bt-field" },
7153
7452
  (0, import_react17.createElement)("span", { className: "bt-field-label" }, t("Model")),
7154
- (0, import_react17.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0 }, roster, actions, wide: true, onPick: setModel }),
7453
+ (0, import_react17.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => {
7454
+ setModel(ref);
7455
+ setEffort(void 0);
7456
+ }, onPickEffort: setEffort }),
7155
7457
  (0, import_react17.createElement)("span", { className: "bt-note" }, (roster.models ?? []).length > 0 ? t("The Bot runs on this model. You can change it later in its details.") : t("Add a provider and a model in DeepSeek Harness first, or pick one in the Bot's chat later."))
7156
7458
  ),
7157
7459
  error ? (0, import_react17.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
@@ -7513,7 +7815,15 @@ function ExchangeDialog({ pair, roster, actions }) {
7513
7815
  // src/client/voice.js
7514
7816
  var import_react20 = require("react");
7515
7817
  var plainSpeech = (text) => stripHeader(text).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim();
7516
- var VOICE_LABELS = { listening: "Listening", thinking: "Thinking", speaking: "Speaking", paused: "Paused", unsupported: "Voice chat is not available in this browser" };
7818
+ var VOICE_LABELS = {
7819
+ listening: "Listening",
7820
+ thinking: "Thinking",
7821
+ speaking: "Speaking",
7822
+ paused: "Paused",
7823
+ setup: "Voice input needs its speech model",
7824
+ unsupported: "Voice chat is not available in this browser"
7825
+ };
7826
+ var CAPTURE_NOTES = { blocked: "Microphone access is blocked", unavailable: "Voice chat is not available in this browser", "too-long": "That was too long to recognize" };
7517
7827
  function useMicLevel(enabled) {
7518
7828
  const [level, setLevel] = (0, import_react20.useState)(0);
7519
7829
  (0, import_react20.useEffect)(() => {
@@ -7566,27 +7876,59 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
7566
7876
  const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0;
7567
7877
  return entry ? `${outline.length}|${entry.response ?? ""}` : "0|";
7568
7878
  });
7569
- const [phase, setPhase] = (0, import_react20.useState)(speechApi() ? "listening" : "unsupported");
7879
+ const [speech] = (0, import_react20.useState)(() => actions.speech());
7880
+ const [phase, setPhase] = (0, import_react20.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
7881
+ const [catalog, setCatalog] = (0, import_react20.useState)(null);
7570
7882
  const [heard, setHeard] = (0, import_react20.useState)("");
7571
7883
  const [said, setSaid] = (0, import_react20.useState)("");
7884
+ const [note, setNote] = (0, import_react20.useState)("");
7885
+ const [serviceLevel, setServiceLevel] = (0, import_react20.useState)(0);
7886
+ const [cycle, setCycle] = (0, import_react20.useState)(0);
7572
7887
  const waiting = (0, import_react20.useRef)(null);
7888
+ const reply = (0, import_react20.useRef)(lastReply);
7889
+ reply.current = lastReply;
7890
+ const turn = (0, import_react20.useRef)(0);
7573
7891
  const paneLeft = usePaneLeft();
7574
7892
  const close = () => {
7893
+ turn.current += 1;
7575
7894
  window.speechSynthesis?.cancel();
7576
7895
  actions.closeVoice();
7577
7896
  };
7578
7897
  useEscape(close);
7898
+ const submit = (text) => {
7899
+ setHeard(text);
7900
+ setPhase("thinking");
7901
+ waiting.current = reply.current.split("|")[0];
7902
+ void actions.send(sessionId, text.trim()).catch(() => setPhase("listening"));
7903
+ };
7579
7904
  const dictation = useDictation((text, final) => {
7580
7905
  setHeard(text);
7581
7906
  if (!final) return;
7582
7907
  dictation.stop();
7583
- setPhase("thinking");
7584
- waiting.current = lastReply.split("|")[0];
7585
- void actions.send(sessionId, text.trim()).catch(() => setPhase("listening"));
7908
+ submit(text);
7586
7909
  });
7910
+ (0, import_react20.useEffect)(() => {
7911
+ if (!note) return void 0;
7912
+ const timer = setTimeout(() => setNote(""), 2600);
7913
+ return () => clearTimeout(timer);
7914
+ }, [note]);
7915
+ (0, import_react20.useEffect)(() => {
7916
+ if (!speech) return void 0;
7917
+ let live = true;
7918
+ speechCatalog(speech).then(({ catalog: next, usable }) => {
7919
+ if (!live) return;
7920
+ setCatalog(next);
7921
+ setPhase(usable ? "listening" : "setup");
7922
+ }, () => {
7923
+ if (live) setPhase("setup");
7924
+ });
7925
+ return () => {
7926
+ live = false;
7927
+ };
7928
+ }, [speech]);
7587
7929
  const listen = phase === "listening";
7588
7930
  (0, import_react20.useEffect)(() => {
7589
- if (!listen || dictation.listening) return void 0;
7931
+ if (speech || !listen || dictation.listening) return void 0;
7590
7932
  const timer = setTimeout(() => dictation.start({ continuous: false, onEnd: () => {
7591
7933
  } }), 250);
7592
7934
  return () => clearTimeout(timer);
@@ -7594,20 +7936,82 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
7594
7936
  (0, import_react20.useEffect)(() => {
7595
7937
  if (phase !== "listening") dictation.stop();
7596
7938
  }, [phase]);
7939
+ (0, import_react20.useEffect)(() => {
7940
+ if (!speech || !catalog || !listen) return void 0;
7941
+ const own = turn.current;
7942
+ let capture = null;
7943
+ let frame = 0;
7944
+ let stopped = false;
7945
+ let smooth = 0;
7946
+ const detect = turnDetector();
7947
+ const limit = Math.max(1, catalog.maxDurationSeconds - 1) * 1e3;
7948
+ const finish = async (current) => {
7949
+ setPhase("transcribing");
7950
+ try {
7951
+ const wave = await current.finish(catalog.maxDurationSeconds);
7952
+ const text = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : "";
7953
+ if (turn.current !== own) return;
7954
+ if (text) submit(text);
7955
+ else setPhase("listening");
7956
+ } catch (error) {
7957
+ if (turn.current !== own) return;
7958
+ setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that"));
7959
+ setPhase("listening");
7960
+ }
7961
+ };
7962
+ openCapture().then((opened) => {
7963
+ if (stopped) {
7964
+ opened.cancel();
7965
+ return;
7966
+ }
7967
+ capture = opened;
7968
+ const began = performance.now();
7969
+ const tick = () => {
7970
+ const now = performance.now();
7971
+ const level2 = capture.level();
7972
+ smooth = smooth * 0.8 + level2 * 0.2;
7973
+ setServiceLevel(Math.round(smooth * 100) / 100);
7974
+ const state = detect(level2, now);
7975
+ if (state === "done" || state === "speech" && now - began >= limit) {
7976
+ const current = capture;
7977
+ capture = null;
7978
+ setServiceLevel(0);
7979
+ void finish(current);
7980
+ return;
7981
+ }
7982
+ if (state === "waiting" && now - began >= limit) {
7983
+ setCycle((value) => value + 1);
7984
+ return;
7985
+ }
7986
+ frame = requestAnimationFrame(tick);
7987
+ };
7988
+ tick();
7989
+ }, (error) => {
7990
+ if (stopped) return;
7991
+ setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that"));
7992
+ setPhase("paused");
7993
+ });
7994
+ return () => {
7995
+ stopped = true;
7996
+ cancelAnimationFrame(frame);
7997
+ capture?.cancel();
7998
+ setServiceLevel(0);
7999
+ };
8000
+ }, [speech, catalog, listen, cycle]);
7597
8001
  (0, import_react20.useEffect)(() => {
7598
8002
  if (phase !== "thinking" || running || waiting.current === null) return;
7599
8003
  const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)];
7600
8004
  if (count === waiting.current || !text.trim()) return;
7601
8005
  waiting.current = null;
7602
- const speech = plainSpeech(text);
7603
- setSaid(speech);
8006
+ const spoken = plainSpeech(text);
8007
+ setSaid(spoken);
7604
8008
  setHeard("");
7605
- if (!window.speechSynthesis || !speech) {
8009
+ if (!window.speechSynthesis || !spoken) {
7606
8010
  setPhase("listening");
7607
8011
  return;
7608
8012
  }
7609
- const utterance = new SpeechSynthesisUtterance(speech);
7610
- utterance.lang = /[\u3400-\u9fff]/.test(speech) ? "zh-CN" : "en-US";
8013
+ const utterance = new SpeechSynthesisUtterance(spoken);
8014
+ utterance.lang = /[\u3400-\u9fff]/.test(spoken) ? "zh-CN" : "en-US";
7611
8015
  utterance.onend = () => setPhase((current) => current === "speaking" ? "listening" : current);
7612
8016
  utterance.onerror = utterance.onend;
7613
8017
  setPhase("speaking");
@@ -7615,23 +8019,28 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
7615
8019
  window.speechSynthesis.speak(utterance);
7616
8020
  }, [phase, running, lastReply]);
7617
8021
  (0, import_react20.useEffect)(() => () => window.speechSynthesis?.cancel(), []);
7618
- const level = useMicLevel(phase === "listening" && dictation.listening);
7619
- const markState = phase === "thinking" ? "thinking" : phase === "speaking" ? "sending" : "idle";
8022
+ const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening);
8023
+ const level = speech ? serviceLevel : micLevel;
8024
+ const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle";
8025
+ const blocked = phase === "unsupported" || phase === "setup" || phase === "starting";
7620
8026
  const togglePause = () => {
7621
8027
  if (phase === "paused") {
7622
8028
  setPhase("listening");
7623
8029
  return;
7624
8030
  }
8031
+ turn.current += 1;
7625
8032
  window.speechSynthesis?.cancel();
7626
8033
  setPhase("paused");
7627
8034
  };
8035
+ const openSetup = phase === "setup" ? actions.voiceSetup() : null;
7628
8036
  const caption = phase === "speaking" ? said : heard;
8037
+ const shownNote = note || dictation.note;
7629
8038
  return (0, import_react20.createElement)(
7630
8039
  "div",
7631
8040
  { className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
7632
8041
  (0, import_react20.createElement)(
7633
8042
  "div",
7634
- { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase },
8043
+ { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase },
7635
8044
  (0, import_react20.createElement)(
7636
8045
  "div",
7637
8046
  { className: "bt-voice-orb", style: { "--bt-level": level } },
@@ -7643,10 +8052,14 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
7643
8052
  (0, import_react20.createElement)(
7644
8053
  "div",
7645
8054
  { className: "bt-voice-status", role: "status" },
7646
- phase === "thinking" ? (0, import_react20.createElement)(Shimmer, null, t("Thinking…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
8055
+ phase === "thinking" ? (0, import_react20.createElement)(Shimmer, null, t("Thinking…")) : phase === "transcribing" ? (0, import_react20.createElement)(Shimmer, null, t("Recognizing…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
7647
8056
  ),
7648
8057
  (0, import_react20.createElement)("div", { className: "bt-voice-caption", key: `${phase}:${caption.slice(0, 12)}` }, caption),
7649
- dictation.note ? (0, import_react20.createElement)("div", { className: "bt-voice-note" }, dictation.note) : null
8058
+ openSetup ? (0, import_react20.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
8059
+ close();
8060
+ openSetup();
8061
+ } }, t("Set up voice input")) : null,
8062
+ shownNote ? (0, import_react20.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
7650
8063
  ),
7651
8064
  (0, import_react20.createElement)(
7652
8065
  "div",
@@ -7655,7 +8068,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
7655
8068
  type: "button",
7656
8069
  className: "bt-voice-btn",
7657
8070
  "aria-pressed": phase === "paused",
7658
- disabled: phase === "unsupported",
8071
+ disabled: blocked,
7659
8072
  "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"),
7660
8073
  onClick: togglePause
7661
8074
  }, (0, import_react20.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react20.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
@@ -9120,55 +9533,196 @@ var FEEDBACK_CSS = `
9120
9533
  @media (prefers-reduced-motion:reduce){.bt-toast{animation:none}.bt-toast[data-leaving]{transition:none}}
9121
9534
  `;
9122
9535
 
9123
- // src/client/settings.js
9536
+ // src/client/about.js
9124
9537
  var import_react25 = require("react");
9538
+ var CHEER_MS = 1800;
9539
+ var starCount = (count) => new Intl.NumberFormat(dateLocale(), { notation: "compact", maximumFractionDigits: 1 }).format(count);
9540
+ function botHint(update) {
9541
+ if (update.phase === "installing") return t("Updating DS Bot…");
9542
+ if (update.phase === "installed") return t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest });
9543
+ if (update.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(update.error ?? "") });
9544
+ if (update.available) return t("DS Bot {version} is available", { version: update.latest });
9545
+ if (update.enabled === false) return t("Update checks are off.");
9546
+ if (update.source === "path") return t("Installed from a local folder; update it there.");
9547
+ if (!update.checkedAt) return null;
9548
+ if (!update.latest && update.source !== "registry") return t("Updates are not offered for this install.");
9549
+ const checked = t("Checked {time}", { time: whenText(update.checkedAt) });
9550
+ return `${update.latest ? t("Up to date") : t("Could not check for updates.")} · ${checked}`;
9551
+ }
9552
+ function dshHint(dsh, shown) {
9553
+ if (dsh.phase === "installing") return t("Updating DSH…");
9554
+ if (dsh.phase === "installed") return t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified });
9555
+ if (dsh.phase === "failed") return t("The update did not finish: {reason}", { reason: hostText(dsh.error ?? "") });
9556
+ if (dsh.available === false && dsh.blockedBy) return t("Update DS Bot {version} first.", { version: dsh.blockedBy });
9557
+ if (shown) return t("DSH {version} is available", { version: dsh.verified });
9558
+ return t("Version {version}", { version: dsh.current });
9559
+ }
9560
+ var linkProps = (href) => ({ className: "bt-soft bt-soft-sm bt-about-link", href, target: "_blank", rel: "noopener noreferrer" });
9561
+ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) {
9562
+ const update = useUpdate((value) => value) ?? {};
9563
+ const [checking, setChecking] = (0, import_react25.useState)(false);
9564
+ const [stars, setStars] = (0, import_react25.useState)(null);
9565
+ (0, import_react25.useEffect)(() => {
9566
+ let live = true;
9567
+ actions.repoStars().then((count) => {
9568
+ if (live && Number.isInteger(count)) setStars(count);
9569
+ }, () => {
9570
+ });
9571
+ return () => {
9572
+ live = false;
9573
+ };
9574
+ }, []);
9575
+ const [cheer, setCheer] = (0, import_react25.useState)(true);
9576
+ (0, import_react25.useEffect)(() => {
9577
+ if (!cheer) return void 0;
9578
+ const timer = setTimeout(() => setCheer(false), CHEER_MS);
9579
+ return () => clearTimeout(timer);
9580
+ }, [cheer]);
9581
+ const check = () => {
9582
+ setChecking(true);
9583
+ void run(actions.checkUpdate()).then((done) => {
9584
+ setChecking(false);
9585
+ if (done) setCheer(true);
9586
+ });
9587
+ };
9588
+ const phase = update.phase ?? "idle";
9589
+ const dsh = update.dsh;
9590
+ const dshPhase = dsh?.phase ?? "idle";
9591
+ (0, import_react25.useEffect)(() => {
9592
+ if (phase === "installed" || dshPhase === "installed") setCheer(true);
9593
+ }, [phase, dshPhase]);
9594
+ const markState = checking ? "searching" : phase === "installing" || dshPhase === "installing" ? "working" : cheer ? "done" : "idle";
9595
+ const dshNew = showsDshRow(dsh, update.app);
9596
+ const dshShown = dshNew && dsh.available;
9597
+ const botControl = phase === "installed" ? null : update.available || phase === "installing" ? (0, import_react25.createElement)(
9598
+ "button",
9599
+ { type: "button", className: "bt-fb-primary", disabled: phase === "installing", onClick: () => run(actions.installUpdate()) },
9600
+ phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")
9601
+ ) : (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"));
9602
+ 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)(
9603
+ "button",
9604
+ { type: "button", className: "bt-fb-primary", disabled: dshPhase === "installing", onClick: () => run(actions.installDshUpdate()) },
9605
+ dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")
9606
+ ) : null;
9607
+ const repo = update.repoUrl;
9608
+ return [
9609
+ (0, import_react25.createElement)(
9610
+ "div",
9611
+ { key: "hero", className: "bt-about-hero" },
9612
+ (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 })),
9613
+ (0, import_react25.createElement)(
9614
+ "div",
9615
+ { className: "bt-about-copy" },
9616
+ (0, import_react25.createElement)("span", { className: "bt-about-name" }, "DS Bot"),
9617
+ update.current ? (0, import_react25.createElement)("span", { className: "bt-about-version" }, t("Version {version}", { version: update.current })) : null
9618
+ ),
9619
+ repo ? (0, import_react25.createElement)(
9620
+ "a",
9621
+ {
9622
+ className: "bt-star",
9623
+ href: repo,
9624
+ target: "_blank",
9625
+ rel: "noopener noreferrer",
9626
+ title: t("Star DS Bot on GitHub"),
9627
+ "aria-label": stars === null ? t("Star DS Bot on GitHub") : `${t("Star DS Bot on GitHub")} · ${t("{count} stars", { count: stars })}`
9628
+ },
9629
+ (0, import_react25.createElement)("span", { className: "bt-star-main" }, (0, import_react25.createElement)("span", { className: "bt-star-icon" }, (0, import_react25.createElement)(StarLineIcon)), "Star"),
9630
+ stars === null ? null : (0, import_react25.createElement)("span", { className: "bt-star-count" }, starCount(stars))
9631
+ ) : null
9632
+ ),
9633
+ 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,
9634
+ (0, import_react25.createElement)(
9635
+ Section,
9636
+ { key: "updates", title: t("Updates") },
9637
+ (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),
9638
+ dsh?.current ? (0, import_react25.createElement)(Row3, { label: "DeepSeek Harness", hint: dshHint(dsh, dshShown) }, dshControl) : null
9639
+ ),
9640
+ (0, import_react25.createElement)(
9641
+ Section,
9642
+ { key: "links", title: t("Links") },
9643
+ 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,
9644
+ update.notesUrl ? (0, import_react25.createElement)(Row3, { label: t("Release notes") }, (0, import_react25.createElement)("a", linkProps(update.notesUrl), t("Open"))) : null,
9645
+ (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")))
9646
+ )
9647
+ ];
9648
+ }
9649
+ var ABOUT_CSS = `
9650
+ .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)}
9651
+ .bt-about-mark{flex:none;display:inline-flex;position:relative}
9652
+ .bt-about-mark::before{content:'';position:absolute;inset:-20%;border-radius:50%}
9653
+ 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}
9654
+ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-state=idle] .bt-eye{scale:1 0;animation:none}
9655
+ 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}
9656
+ .bt-about-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
9657
+ .bt-about-name{font-size:16px;line-height:22px;font-weight:600}
9658
+ .bt-about-version{font-size:12px;line-height:16px;color:var(--bt-ink-2);font-variant-numeric:tabular-nums}
9659
+ .bt-about-ask{margin:-10px 4px 0;font-size:12px;line-height:16px;color:var(--bt-ink-3)}
9660
+ .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}
9661
+ .bt-star:hover{border-color:color-mix(in srgb,#e3a008 55%,var(--bt-line))}
9662
+ .bt-star:active{transform:scale(.96)}
9663
+ .bt-star-main{display:inline-flex;align-items:center;gap:6px;padding:0 12px 0 10px;background:var(--bt-hover);font-weight:500}
9664
+ .bt-star-icon{display:inline-flex;color:var(--bt-ink-2);transition:color .15s ease}
9665
+ .bt-star-icon path{transition:fill .15s ease}
9666
+ .bt-star:hover .bt-star-icon{color:#e3a008;animation:bt-star-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing}}
9667
+ .bt-star:hover .bt-star-icon path{fill:currentColor}
9668
+ .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}
9669
+ @keyframes bt-star-pop{from{scale:.6;rotate:-30deg}}
9670
+ @keyframes bt-star-count-in{from{translate:-6px 0}}
9671
+ .bt-about-link{display:inline-flex;align-items:center;gap:6px;text-decoration:none}
9672
+ .bt-about-link svg{width:14px;height:14px}
9673
+ .bt-fb-primary:disabled{opacity:.6;cursor:default;filter:none}
9674
+ @media (prefers-reduced-motion:reduce){.bt-star:hover .bt-star-icon,.bt-star-count{animation:none}}
9675
+ `;
9676
+
9677
+ // src/client/settings.js
9678
+ var import_react26 = require("react");
9125
9679
  var capitalize2 = (word) => word[0].toUpperCase() + word.slice(1);
9126
- var SETTINGS_PAGES = [["general", "General"], ["bots", "Bots"], ["groups", "Group chats"], ["connectors", "Connectors"], ["schedules", "Scheduled tasks"], ["usage", "Usage"], ["secrets", "Secrets"], ["feedback", "Feedback"]];
9127
- var PAGE_ICONS = { ...SETTINGS_ICONS, connectors: ConnectorIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon };
9680
+ 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"]];
9681
+ var PAGE_ICONS = { ...SETTINGS_ICONS, connectors: ConnectorIcon, schedules: ClockIcon, secrets: KeyIcon, feedback: FeedbackIcon, about: InfoIcon };
9128
9682
  function SetSection({ title, children }) {
9129
- 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));
9683
+ 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));
9130
9684
  }
9131
9685
  function SetRow({ label, hint, children }) {
9132
- return (0, import_react25.createElement)(
9686
+ return (0, import_react26.createElement)(
9133
9687
  "div",
9134
9688
  { className: "bt-set-row" },
9135
- (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),
9136
- children ? (0, import_react25.createElement)("div", { className: "bt-set-control" }, children) : null
9689
+ (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),
9690
+ children ? (0, import_react26.createElement)("div", { className: "bt-set-control" }, children) : null
9137
9691
  );
9138
9692
  }
9139
9693
  function GeneralPage({ roster, actions, run }) {
9140
9694
  const prefs = roster.prefs ?? {};
9141
9695
  const themes = allThemes();
9142
9696
  const current = themes[prefs.theme] ? prefs.theme : "deepseek";
9143
- const [custom, setCustom] = (0, import_react25.useState)(prefs.accent ?? "#4d6bfe");
9144
- const timer = (0, import_react25.useRef)(0);
9145
- (0, import_react25.useEffect)(() => () => clearTimeout(timer.current), []);
9697
+ const [custom, setCustom] = (0, import_react26.useState)(prefs.accent ?? "#4d6bfe");
9698
+ const timer = (0, import_react26.useRef)(0);
9699
+ (0, import_react26.useEffect)(() => () => clearTimeout(timer.current), []);
9146
9700
  const pickCustom = (value) => {
9147
9701
  setCustom(value);
9148
9702
  clearTimeout(timer.current);
9149
9703
  timer.current = setTimeout(() => run(actions.setPrefs({ accent: value })), 350);
9150
9704
  };
9151
9705
  return [
9152
- (0, import_react25.createElement)(
9706
+ (0, import_react26.createElement)(
9153
9707
  SetSection,
9154
9708
  { key: "look", title: t("Appearance") },
9155
- (0, import_react25.createElement)(
9709
+ (0, import_react26.createElement)(
9156
9710
  SetRow,
9157
9711
  { label: t("Theme"), hint: t("Colors for the whole team, on every device.") },
9158
- (0, import_react25.createElement)(
9712
+ (0, import_react26.createElement)(
9159
9713
  "select",
9160
9714
  { className: "bt-select", "aria-label": t("Theme"), value: current, onChange: (event) => run(actions.setPrefs({ theme: event.target.value })) },
9161
- Object.entries(themes).map(([id, theme]) => (0, import_react25.createElement)("option", { key: id, value: id }, t(theme.label)))
9715
+ Object.entries(themes).map(([id, theme]) => (0, import_react26.createElement)("option", { key: id, value: id }, t(theme.label)))
9162
9716
  )
9163
9717
  ),
9164
- (0, import_react25.createElement)(
9718
+ (0, import_react26.createElement)(
9165
9719
  SetRow,
9166
9720
  { label: t("Accent"), hint: t("Unread marks, badges, and the brand mark.") },
9167
- (0, import_react25.createElement)(
9721
+ (0, import_react26.createElement)(
9168
9722
  "div",
9169
9723
  { className: "bt-swatches" },
9170
- (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 })) }),
9171
- ACCENT_INKS.map((ink) => (0, import_react25.createElement)("button", {
9724
+ (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 })) }),
9725
+ ACCENT_INKS.map((ink) => (0, import_react26.createElement)("button", {
9172
9726
  key: ink,
9173
9727
  type: "button",
9174
9728
  className: "bt-swatch",
@@ -9178,21 +9732,21 @@ function GeneralPage({ roster, actions, run }) {
9178
9732
  style: { background: INKS[ink][0] },
9179
9733
  onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] }))
9180
9734
  })),
9181
- (0, import_react25.createElement)(
9735
+ (0, import_react26.createElement)(
9182
9736
  "label",
9183
9737
  { className: "bt-swatch bt-swatch-custom", title: t("Custom accent"), "data-on": Boolean(prefs.accent) && !ACCENT_INKS.some((ink) => INKS[ink][0] === prefs.accent) },
9184
- (0, import_react25.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) })
9738
+ (0, import_react26.createElement)("input", { type: "color", value: custom, "aria-label": t("Custom accent"), onChange: (event) => pickCustom(event.target.value) })
9185
9739
  )
9186
9740
  )
9187
9741
  )
9188
9742
  ),
9189
- (0, import_react25.createElement)(
9743
+ (0, import_react26.createElement)(
9190
9744
  SetSection,
9191
9745
  { key: "mode", title: t("Mode") },
9192
- (0, import_react25.createElement)(
9746
+ (0, import_react26.createElement)(
9193
9747
  SetRow,
9194
9748
  { label: t("Classic Agent mode") },
9195
- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.setSurface("agent") }, t("Switch"))
9749
+ (0, import_react26.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.setSurface("agent") }, t("Switch"))
9196
9750
  )
9197
9751
  )
9198
9752
  ];
@@ -9201,52 +9755,52 @@ function MainBotsSection({ roster, actions, run }) {
9201
9755
  const mains = (roster.mainBotIds ?? []).map((id) => roster.byId[id]).filter(Boolean);
9202
9756
  const [first, ...others] = mains;
9203
9757
  const candidates = roster.bots.filter((bot) => !isMainOf(roster, bot.id));
9204
- return (0, import_react25.createElement)(
9758
+ return (0, import_react26.createElement)(
9205
9759
  SetSection,
9206
9760
  { title: t("Main Bot") },
9207
- (0, import_react25.createElement)(
9761
+ (0, import_react26.createElement)(
9208
9762
  SetRow,
9209
9763
  {
9210
9764
  label: t("Main Bot"),
9211
9765
  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
9212
9766
  },
9213
- (0, import_react25.createElement)("select", {
9767
+ (0, import_react26.createElement)("select", {
9214
9768
  className: "bt-select",
9215
9769
  "aria-label": t("Main Bot"),
9216
9770
  value: first?.id ?? "",
9217
9771
  onChange: (event) => run(actions.setMain(event.target.value, true, first ? { replace: first.id } : {}))
9218
- }, 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)))
9772
+ }, 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)))
9219
9773
  ),
9220
- others.map((bot) => (0, import_react25.createElement)(
9774
+ others.map((bot) => (0, import_react26.createElement)(
9221
9775
  SetRow,
9222
9776
  { key: bot.id, label: bot.name, hint: t("Also a Main Bot.") },
9223
- (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")),
9224
- (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"))
9777
+ (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")),
9778
+ (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"))
9225
9779
  )),
9226
- candidates.length > 0 ? (0, import_react25.createElement)(
9780
+ candidates.length > 0 ? (0, import_react26.createElement)(
9227
9781
  SetRow,
9228
9782
  { label: t("Add a Main Bot"), hint: t("Main Bots can create and change Bots, and run every group chat.") },
9229
- (0, import_react25.createElement)("select", {
9783
+ (0, import_react26.createElement)("select", {
9230
9784
  className: "bt-select",
9231
9785
  "aria-label": t("Add a Main Bot"),
9232
9786
  value: "",
9233
9787
  onChange: (event) => {
9234
9788
  if (event.target.value) void run(actions.setMain(event.target.value, true));
9235
9789
  }
9236
- }, (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)))
9790
+ }, (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)))
9237
9791
  ) : null
9238
9792
  );
9239
9793
  }
9240
9794
  var MOTIONS = [["quiet", "Quiet"], ["normal", "Normal"], ["lively", "Lively"]];
9241
9795
  function MotionSection({ roster, actions, run }) {
9242
9796
  const motion = roster.prefs?.motion === "quiet" || roster.prefs?.motion === "lively" ? roster.prefs.motion : "normal";
9243
- return (0, import_react25.createElement)(
9797
+ return (0, import_react26.createElement)(
9244
9798
  SetSection,
9245
9799
  { title: t("Animation") },
9246
- (0, import_react25.createElement)(
9800
+ (0, import_react26.createElement)(
9247
9801
  SetRow,
9248
9802
  { label: t("Bot animation") },
9249
- (0, import_react25.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react25.createElement)("button", {
9803
+ (0, import_react26.createElement)(Segmented, { label: t("Bot animation"), value: motion, className: "bt-motion-seg" }, MOTIONS.map(([id, label]) => (0, import_react26.createElement)("button", {
9250
9804
  key: id,
9251
9805
  type: "button",
9252
9806
  "aria-pressed": motion === id,
@@ -9258,69 +9812,69 @@ function MotionSection({ roster, actions, run }) {
9258
9812
  );
9259
9813
  }
9260
9814
  function BotsPage({ roster, actions, run }) {
9261
- const [creating, setCreating] = (0, import_react25.useState)(false);
9815
+ const [creating, setCreating] = (0, import_react26.useState)(false);
9262
9816
  return [
9263
- (0, import_react25.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
9264
- (0, import_react25.createElement)(MotionSection, { key: "motion", roster, actions, run }),
9265
- 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)(
9817
+ (0, import_react26.createElement)(MainBotsSection, { key: "main", roster, actions, run }),
9818
+ (0, import_react26.createElement)(MotionSection, { key: "motion", roster, actions, run }),
9819
+ 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)(
9266
9820
  "div",
9267
9821
  { key: "new", className: "bt-actions" },
9268
- (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"))
9822
+ (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"))
9269
9823
  ),
9270
- (0, import_react25.createElement)(
9824
+ (0, import_react26.createElement)(
9271
9825
  SetSection,
9272
9826
  { key: "bots", title: t("Bots · {count}", { count: roster.bots.length }) },
9273
- roster.bots.map((bot) => (0, import_react25.createElement)(
9827
+ roster.bots.map((bot) => (0, import_react26.createElement)(
9274
9828
  "button",
9275
9829
  { key: bot.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("bots", bot.id) },
9276
- (0, import_react25.createElement)(
9830
+ (0, import_react26.createElement)(
9277
9831
  "span",
9278
9832
  { className: "bt-set-bot" },
9279
- (0, import_react25.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
9280
- (0, import_react25.createElement)("span", { className: "bt-row-title" }, bot.name),
9281
- bot.role ? (0, import_react25.createElement)("span", { className: "bt-tag" }, bot.role) : null,
9282
- isMainOf(roster, bot.id) ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
9283
- bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
9833
+ (0, import_react26.createElement)(BotAvatar, { bot, size: 28, badge: false, live: false }),
9834
+ (0, import_react26.createElement)("span", { className: "bt-row-title" }, bot.name),
9835
+ bot.role ? (0, import_react26.createElement)("span", { className: "bt-tag" }, bot.role) : null,
9836
+ isMainOf(roster, bot.id) ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Main Bot")) : null,
9837
+ bot.hidden && !isMainOf(roster, bot.id) ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
9284
9838
  ),
9285
- (0, import_react25.createElement)("span", { className: "bt-chevron" }, (0, import_react25.createElement)(ChevronRightIcon))
9839
+ (0, import_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.createElement)(ChevronRightIcon))
9286
9840
  ))
9287
9841
  )
9288
9842
  ];
9289
9843
  }
9290
9844
  function GroupsPage({ roster, actions, run }) {
9291
- const [creating, setCreating] = (0, import_react25.useState)(false);
9845
+ const [creating, setCreating] = (0, import_react26.useState)(false);
9292
9846
  return [
9293
- 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)(
9847
+ 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)(
9294
9848
  "div",
9295
9849
  { key: "new", className: "bt-actions" },
9296
- (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"))
9850
+ (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"))
9297
9851
  ),
9298
- (0, import_react25.createElement)(
9852
+ (0, import_react26.createElement)(
9299
9853
  SetSection,
9300
9854
  { key: "groups", title: t("Group chats · {count}", { count: roster.rooms.length }) },
9301
- 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) => {
9855
+ 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) => {
9302
9856
  const admin = roster.byId[room2.admin];
9303
- return (0, import_react25.createElement)(
9857
+ return (0, import_react26.createElement)(
9304
9858
  "button",
9305
9859
  { key: room2.id, type: "button", className: "bt-set-row", onClick: () => actions.openSettings("groups", room2.id) },
9306
- (0, import_react25.createElement)(
9860
+ (0, import_react26.createElement)(
9307
9861
  "span",
9308
9862
  { className: "bt-set-bot" },
9309
- (0, import_react25.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
9310
- (0, import_react25.createElement)("span", { className: "bt-row-title" }, room2.name),
9311
- (0, import_react25.createElement)("span", { className: "bt-tag" }, t("{count} members", { count: room2.members.length })),
9312
- admin ? (0, import_react25.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react25.createElement)(AdminStar), admin.name) : null,
9313
- room2.hidden ? (0, import_react25.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
9863
+ (0, import_react26.createElement)(RoomAvatar, { room: room2, roster, size: 28 }),
9864
+ (0, import_react26.createElement)("span", { className: "bt-row-title" }, room2.name),
9865
+ (0, import_react26.createElement)("span", { className: "bt-tag" }, t("{count} members", { count: room2.members.length })),
9866
+ admin ? (0, import_react26.createElement)("span", { className: "bt-tag bt-tag-admin" }, (0, import_react26.createElement)(AdminStar), admin.name) : null,
9867
+ room2.hidden ? (0, import_react26.createElement)("span", { className: "bt-tag" }, t("Hidden")) : null
9314
9868
  ),
9315
- (0, import_react25.createElement)("span", { className: "bt-chevron" }, (0, import_react25.createElement)(ChevronRightIcon))
9869
+ (0, import_react26.createElement)("span", { className: "bt-chevron" }, (0, import_react26.createElement)(ChevronRightIcon))
9316
9870
  );
9317
9871
  })
9318
9872
  )
9319
9873
  ];
9320
9874
  }
9321
- function SettingsDialog({ page, id, roster, actions, leaving = false }) {
9322
- const [error, setError] = (0, import_react25.useState)("");
9323
- const close = (0, import_react25.useCallback)(() => actions.closeSettings(), []);
9875
+ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }) {
9876
+ const [error, setError] = (0, import_react26.useState)("");
9877
+ const close = (0, import_react26.useCallback)(() => actions.closeSettings(), []);
9324
9878
  useEscape(close);
9325
9879
  const run = (promise) => Promise.resolve(promise).then(() => {
9326
9880
  setError("");
@@ -9338,32 +9892,32 @@ function SettingsDialog({ page, id, roster, actions, leaving = false }) {
9338
9892
  const nav = useGlide(shown, { axis: "y" });
9339
9893
  const pageKey = `${shown}:${bot?.id ?? room2?.id ?? ""}`;
9340
9894
  const depth = bot || room2 ? 1 : 0;
9341
- const last = (0, import_react25.useRef)({ key: pageKey, depth, move: "up" });
9895
+ const last = (0, import_react26.useRef)({ key: pageKey, depth, move: "up" });
9342
9896
  if (last.current.key !== pageKey) {
9343
9897
  last.current = { key: pageKey, depth, move: depth > last.current.depth ? "in" : depth < last.current.depth ? "out" : "up" };
9344
9898
  }
9345
- const [usageOpened, setUsageOpened] = (0, import_react25.useState)(shown === "usage");
9899
+ const [usageOpened, setUsageOpened] = (0, import_react26.useState)(shown === "usage");
9346
9900
  if (shown === "usage" && !usageOpened) setUsageOpened(true);
9347
- const errorLine = error ? (0, import_react25.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
9348
- return (0, import_react25.createElement)(
9901
+ const errorLine = error ? (0, import_react26.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
9902
+ return (0, import_react26.createElement)(
9349
9903
  "div",
9350
9904
  { className: "bt-settings-layer", "data-leaving": leaving || void 0 },
9351
- (0, import_react25.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
9352
- (0, import_react25.createElement)(
9905
+ (0, import_react26.createElement)("div", { className: "bt-scrim", onMouseDown: close }),
9906
+ (0, import_react26.createElement)(
9353
9907
  "div",
9354
9908
  { className: "bt-settings", role: "dialog", "aria-modal": true, "aria-label": t("DS Bot settings"), "data-wide": shown === "usage" ? "" : void 0 },
9355
- (0, import_react25.createElement)(
9909
+ (0, import_react26.createElement)(
9356
9910
  "nav",
9357
9911
  { ref: nav.box, className: "bt-settings-nav", "aria-label": t("Settings pages") },
9358
- (0, import_react25.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
9359
- SETTINGS_PAGES.map(([key, text]) => (0, import_react25.createElement)("button", {
9912
+ (0, import_react26.createElement)("span", { ref: nav.thumb, className: "bt-thumb", "aria-hidden": true }),
9913
+ SETTINGS_PAGES.map(([key, text]) => (0, import_react26.createElement)("button", {
9360
9914
  key,
9361
9915
  type: "button",
9362
9916
  className: "bt-settings-item",
9363
9917
  "aria-current": shown === key ? "page" : void 0,
9364
9918
  onClick: () => actions.openSettings(key)
9365
- }, (0, import_react25.createElement)(PAGE_ICONS[key]), t(text))),
9366
- (0, import_react25.createElement)("button", {
9919
+ }, (0, import_react26.createElement)(PAGE_ICONS[key]), t(text))),
9920
+ (0, import_react26.createElement)("button", {
9367
9921
  type: "button",
9368
9922
  className: "bt-settings-item bt-settings-shell",
9369
9923
  title: t("Models, providers, and the rest of DeepSeek Harness"),
@@ -9371,62 +9925,63 @@ function SettingsDialog({ page, id, roster, actions, leaving = false }) {
9371
9925
  close();
9372
9926
  openHarnessSettings();
9373
9927
  }
9374
- }, (0, import_react25.createElement)(SlidersIcon), t("Harness settings"))
9928
+ }, (0, import_react26.createElement)(SlidersIcon), t("Harness settings"))
9375
9929
  ),
9376
- (0, import_react25.createElement)(
9930
+ (0, import_react26.createElement)(
9377
9931
  "div",
9378
9932
  { className: "bt-settings-main" },
9379
- (0, import_react25.createElement)(
9933
+ (0, import_react26.createElement)(
9380
9934
  "div",
9381
9935
  { className: "bt-settings-head" },
9382
- 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,
9383
- (0, import_react25.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label)
9936
+ 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,
9937
+ (0, import_react26.createElement)("h2", { key: bot?.id ?? room2?.id ?? shown }, bot?.name ?? room2?.name ?? label)
9384
9938
  ),
9385
- shown === "usage" ? null : (0, import_react25.createElement)(
9939
+ shown === "usage" ? null : (0, import_react26.createElement)(
9386
9940
  "div",
9387
9941
  { key: pageKey, className: "bt-settings-scroll bt-settings-page", "data-move": last.current.move },
9388
9942
  errorLine,
9389
- shown === "general" ? (0, import_react25.createElement)(GeneralPage, { roster, actions, run }) : null,
9390
- bot ? (0, import_react25.createElement)("div", { className: "bt-settings-form" }, (0, import_react25.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
9391
- shown === "bots" && !bot ? (0, import_react25.createElement)(BotsPage, { roster, actions, run }) : null,
9392
- room2 ? (0, import_react25.createElement)("div", { className: "bt-settings-form" }, (0, import_react25.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
9393
- shown === "groups" && !room2 ? (0, import_react25.createElement)(GroupsPage, { roster, actions, run }) : null,
9394
- shown === "connectors" ? (0, import_react25.createElement)(ConnectorsPage, { roster, actions }) : null,
9395
- shown === "schedules" ? (0, import_react25.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
9396
- shown === "secrets" ? (0, import_react25.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
9397
- shown === "feedback" ? (0, import_react25.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null
9943
+ shown === "general" ? (0, import_react26.createElement)(GeneralPage, { roster, actions, run }) : null,
9944
+ bot ? (0, import_react26.createElement)("div", { className: "bt-settings-form" }, (0, import_react26.createElement)(BotSettingsForm, { key: bot.id, bot, roster, actions, run })) : null,
9945
+ shown === "bots" && !bot ? (0, import_react26.createElement)(BotsPage, { roster, actions, run }) : null,
9946
+ room2 ? (0, import_react26.createElement)("div", { className: "bt-settings-form" }, (0, import_react26.createElement)(GroupSettingsForm, { key: room2.id, room: room2, roster, actions, run })) : null,
9947
+ shown === "groups" && !room2 ? (0, import_react26.createElement)(GroupsPage, { roster, actions, run }) : null,
9948
+ shown === "connectors" ? (0, import_react26.createElement)(ConnectorsPage, { roster, actions }) : null,
9949
+ shown === "schedules" ? (0, import_react26.createElement)(SchedulesPage, { roster, actions, start: id ?? void 0 }) : null,
9950
+ shown === "secrets" ? (0, import_react26.createElement)(SecretsPage, { roster, actions, run, Section: SetSection }) : null,
9951
+ shown === "feedback" ? (0, import_react26.createElement)(FeedbackPage, { actions, run, Section: SetSection, Row: SetRow }) : null,
9952
+ shown === "about" ? (0, import_react26.createElement)(AboutPage, { useUpdate, actions, run, Section: SetSection, Row: SetRow }) : null
9398
9953
  ),
9399
- usageOpened ? (0, import_react25.createElement)(
9954
+ usageOpened ? (0, import_react26.createElement)(
9400
9955
  "div",
9401
9956
  { key: "usage", className: "bt-settings-scroll bt-settings-usage", "data-off": shown === "usage" ? void 0 : "", "aria-hidden": shown === "usage" ? void 0 : true },
9402
9957
  shown === "usage" ? errorLine : null,
9403
- (0, import_react25.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
9958
+ (0, import_react26.createElement)(UsagePage, { roster, actions, focusId: usageFocus })
9404
9959
  ) : null
9405
9960
  ),
9406
- (0, import_react25.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react25.createElement)(CloseIcon))
9961
+ (0, import_react26.createElement)("button", { type: "button", className: "bt-settings-close", "aria-label": t("Close settings"), onClick: close }, (0, import_react26.createElement)(CloseIcon))
9407
9962
  )
9408
9963
  );
9409
9964
  }
9410
9965
 
9411
9966
  // src/client/context-menu.js
9412
- var import_react26 = require("react");
9967
+ var import_react27 = require("react");
9413
9968
  function ContextMenu({ menu, roster, actions, leaving = false }) {
9414
9969
  useEscape(() => actions.closeMenu());
9415
- (0, import_react26.useEffect)(() => {
9970
+ (0, import_react27.useEffect)(() => {
9416
9971
  const onDown = (event) => {
9417
9972
  if (!(event.target instanceof Element) || !event.target.closest(".bt-menu")) actions.closeMenu();
9418
9973
  };
9419
9974
  window.addEventListener("mousedown", onDown);
9420
9975
  return () => window.removeEventListener("mousedown", onDown);
9421
9976
  }, []);
9422
- const ref = (0, import_react26.useRef)(null);
9423
- const [place, setPlace] = (0, import_react26.useState)({ left: menu.x, top: menu.y });
9424
- (0, import_react26.useLayoutEffect)(() => {
9977
+ const ref = (0, import_react27.useRef)(null);
9978
+ const [place, setPlace] = (0, import_react27.useState)({ left: menu.x, top: menu.y });
9979
+ (0, import_react27.useLayoutEffect)(() => {
9425
9980
  const box = ref.current?.getBoundingClientRect();
9426
9981
  if (!box) return;
9427
9982
  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)) });
9428
9983
  }, [menu.x, menu.y]);
9429
- if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react26.createElement)(AgentMenu, { menu, roster, actions, leaving });
9984
+ if (menu.kind === "agent" || menu.kind === "agent-session") return (0, import_react27.createElement)(AgentMenu, { menu, roster, actions, leaving });
9430
9985
  const bot = roster.byId[menu.id];
9431
9986
  const room2 = roster.roomsById[menu.id];
9432
9987
  const entry = bot ?? room2;
@@ -9457,14 +10012,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
9457
10012
  isMainBot ? null : [room2 ? t("Delete group") : t("Delete"), TrashIcon, () => bot ? actions.deleteBot(bot.id) : actions.deleteRoom(menu.id), true]
9458
10013
  ]
9459
10014
  ].map((group) => group.filter(Boolean)).filter((group) => group.length > 0);
9460
- return (0, import_react26.createElement)(
10015
+ return (0, import_react27.createElement)(
9461
10016
  "div",
9462
10017
  { ref, className: "bt-menu", role: "menu", "aria-label": t("{name} actions", { name: bot?.name ?? room2.name }), style: place, "data-leaving": leaving || void 0 },
9463
- groups.map((group, index) => (0, import_react26.createElement)(
9464
- import_react26.Fragment,
10018
+ groups.map((group, index) => (0, import_react27.createElement)(
10019
+ import_react27.Fragment,
9465
10020
  { key: index },
9466
- index > 0 ? (0, import_react26.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
9467
- group.map(([label, Icon, run, danger]) => (0, import_react26.createElement)("button", {
10021
+ index > 0 ? (0, import_react27.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
10022
+ group.map(([label, Icon, run, danger]) => (0, import_react27.createElement)("button", {
9468
10023
  key: label,
9469
10024
  type: "button",
9470
10025
  role: "menuitem",
@@ -9473,14 +10028,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) {
9473
10028
  actions.closeMenu();
9474
10029
  void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu"));
9475
10030
  }
9476
- }, (0, import_react26.createElement)(Icon), label))
10031
+ }, (0, import_react27.createElement)(Icon), label))
9477
10032
  ))
9478
10033
  );
9479
10034
  }
9480
10035
  function AgentMenu({ menu, roster, actions, leaving }) {
9481
- const ref = (0, import_react26.useRef)(null);
9482
- const [place, setPlace] = (0, import_react26.useState)({ left: menu.x, top: menu.y });
9483
- (0, import_react26.useLayoutEffect)(() => {
10036
+ const ref = (0, import_react27.useRef)(null);
10037
+ const [place, setPlace] = (0, import_react27.useState)({ left: menu.x, top: menu.y });
10038
+ (0, import_react27.useLayoutEffect)(() => {
9484
10039
  const box = ref.current?.getBoundingClientRect();
9485
10040
  if (!box) return;
9486
10041
  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)) });
@@ -9502,14 +10057,14 @@ function AgentMenu({ menu, roster, actions, leaving }) {
9502
10057
  [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true]
9503
10058
  ]
9504
10059
  ];
9505
- return (0, import_react26.createElement)(
10060
+ return (0, import_react27.createElement)(
9506
10061
  "div",
9507
10062
  { ref, className: "bt-menu", role: "menu", "aria-label": menu.kind === "agent" ? "DSH Agent" : t("Session actions"), style: place, "data-leaving": leaving || void 0 },
9508
- groups.map((group, index) => (0, import_react26.createElement)(
9509
- import_react26.Fragment,
10063
+ groups.map((group, index) => (0, import_react27.createElement)(
10064
+ import_react27.Fragment,
9510
10065
  { key: index },
9511
- index > 0 ? (0, import_react26.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
9512
- group.map(([label, Icon, run, danger]) => (0, import_react26.createElement)("button", {
10066
+ index > 0 ? (0, import_react27.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null,
10067
+ group.map(([label, Icon, run, danger]) => (0, import_react27.createElement)("button", {
9513
10068
  key: label,
9514
10069
  type: "button",
9515
10070
  role: "menuitem",
@@ -9518,7 +10073,7 @@ function AgentMenu({ menu, roster, actions, leaving }) {
9518
10073
  actions.closeMenu();
9519
10074
  void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error));
9520
10075
  }
9521
- }, (0, import_react26.createElement)(Icon), label))
10076
+ }, (0, import_react27.createElement)(Icon), label))
9522
10077
  ))
9523
10078
  );
9524
10079
  }
@@ -9589,7 +10144,7 @@ function migrateStorage(storage = globalThis.localStorage) {
9589
10144
  }
9590
10145
 
9591
10146
  // src/client/details-drawer.js
9592
- var import_react27 = require("react");
10147
+ var import_react28 = require("react");
9593
10148
  function transcriptOf(list, id, name, hiddenTurns = []) {
9594
10149
  const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline;
9595
10150
  if (!Array.isArray(outline)) return "";
@@ -9611,45 +10166,79 @@ var hostOf = (url) => {
9611
10166
  };
9612
10167
  var shortUrl = (url) => String(url ?? "").replace(/^https?:\/\//i, "").replace(/^www\./i, "").replace(/\/$/, "");
9613
10168
  var EMPTY_TABS = { ids: [], active: null };
10169
+ var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" };
10170
+ var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]);
10171
+ function PermissionRow({ bot, roster, actions, run }) {
10172
+ const [pending, setPending] = (0, import_react28.useState)(null);
10173
+ const catalog = roster.permissions;
10174
+ const saved = bot.permission ?? catalog?.defaultPreset;
10175
+ (0, import_react28.useEffect)(() => {
10176
+ if (pending === saved) setPending(null);
10177
+ }, [pending, saved]);
10178
+ if (!catalog?.options?.length) return null;
10179
+ const current = pending ?? saved;
10180
+ const pick2 = (value) => {
10181
+ if (value === current && bot.permission !== void 0) return;
10182
+ setPending(value);
10183
+ void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null));
10184
+ };
10185
+ return (0, import_react28.createElement)(
10186
+ "div",
10187
+ { className: "bt-drawer-permission" },
10188
+ (0, import_react28.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
10189
+ (0, import_react28.createElement)(
10190
+ Segmented,
10191
+ { label: t("Permissions"), value: current },
10192
+ catalog.options.map((option) => (0, import_react28.createElement)("button", {
10193
+ key: option.value,
10194
+ type: "button",
10195
+ "aria-pressed": option.value === current,
10196
+ title: option.description,
10197
+ onClick: () => pick2(option.value),
10198
+ className: DANGER.has(option.value) ? "bt-perm-danger" : void 0
10199
+ }, PERMISSION_LABELS[option.value] ? t(PERMISSION_LABELS[option.value]) : option.name))
10200
+ )
10201
+ );
10202
+ }
9614
10203
  function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) {
9615
10204
  const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS);
9616
10205
  const pages = useBrowserPages((map) => map);
9617
- (0, import_react27.useEffect)(() => {
10206
+ (0, import_react28.useEffect)(() => {
9618
10207
  actions.browserEnsure?.(bot.id);
9619
10208
  }, [bot.id]);
9620
10209
  if (roster.browser !== true || !actions.browserShow) {
9621
- return (0, import_react27.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app."));
10210
+ return (0, import_react28.createElement)("div", { className: "bt-muted" }, t("The browser is in the DSH Desktop app."));
9622
10211
  }
9623
10212
  if (state.ids.length === 0) {
9624
- return (0, import_react27.createElement)(
10213
+ return (0, import_react28.createElement)(
9625
10214
  "div",
9626
10215
  { className: "bt-browser-empty-mark" },
9627
- (0, import_react27.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
9628
- (0, import_react27.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
9629
- (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 })),
9630
- (0, import_react27.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
10216
+ (0, import_react28.createElement)(BotMark, { bot, size: 40, state: "idle", live: true }),
10217
+ (0, import_react28.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
10218
+ (0, import_react28.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 })),
10219
+ (0, import_react28.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => actions.openBrowser(sessionId) }, t("Open browser"))
9631
10220
  );
9632
10221
  }
9633
- return (0, import_react27.createElement)(
10222
+ return (0, import_react28.createElement)(
9634
10223
  "div",
9635
10224
  { className: "bt-bcards" },
9636
10225
  state.ids.map((tabId, index) => {
9637
10226
  const page = pages[tabId] ?? {};
9638
10227
  const label = page.title || shortUrl(page.url) || t("New tab");
9639
- return (0, import_react27.createElement)(
10228
+ return (0, import_react28.createElement)(
9640
10229
  "div",
9641
10230
  { key: tabId, className: "bt-bcard", "data-active": state.active === tabId || void 0, style: { animationDelay: `${index * 30}ms` } },
9642
- (0, import_react27.createElement)(
10231
+ (0, import_react28.createElement)(
9643
10232
  "button",
9644
10233
  { type: "button", className: "bt-bcard-shot", "aria-label": t("Open {title}", { title: label }), title: page.url, onClick: () => actions.browserPick(sessionId, tabId) },
9645
- page.excerpt ? (0, import_react27.createElement)(
10234
+ page.excerpt ? (0, import_react28.createElement)(
9646
10235
  "span",
9647
10236
  { className: "bt-bcard-preview" },
9648
- (0, import_react27.createElement)("span", { className: "bt-bcard-ptitle" }, label),
9649
- (0, import_react27.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
9650
- ) : (0, import_react27.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react27.createElement)(GlobeIcon), (0, import_react27.createElement)("span", null, label))
10237
+ (0, import_react28.createElement)("span", { className: "bt-bcard-ptitle" }, label),
10238
+ (0, import_react28.createElement)("span", { className: "bt-bcard-excerpt" }, page.excerpt)
10239
+ ) : (0, import_react28.createElement)("span", { className: "bt-bcard-blank" }, (0, import_react28.createElement)(GlobeIcon), (0, import_react28.createElement)("span", null, label))
9651
10240
  ),
9652
- (0, import_react27.createElement)("button", {
10241
+ (0, import_react28.createElement)("button", {
9653
10242
  type: "button",
9654
10243
  className: "bt-bcard-x",
9655
10244
  "aria-label": t("Close tab"),
@@ -9658,62 +10247,62 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow
9658
10247
  void actions.browserCloseTab(bot.id, tabId);
9659
10248
  }
9660
10249
  }, "×"),
9661
- (0, import_react27.createElement)(
10250
+ (0, import_react28.createElement)(
9662
10251
  "div",
9663
10252
  { className: "bt-bcard-row" },
9664
- (0, import_react27.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
9665
- (0, import_react27.createElement)("span", { className: "bt-bcard-title" }, label),
9666
- page.url ? (0, import_react27.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null
10253
+ (0, import_react28.createElement)(Favicon, { src: page.favicon, className: "bt-bcard-fav" }),
10254
+ (0, import_react28.createElement)("span", { className: "bt-bcard-title" }, label),
10255
+ page.url ? (0, import_react28.createElement)("span", { className: "bt-bcard-host" }, hostOf(page.url)) : null
9667
10256
  )
9668
10257
  );
9669
10258
  }),
9670
- (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"))
10259
+ (0, import_react28.createElement)("button", { type: "button", className: "bt-bcard-new", style: { animationDelay: `${state.ids.length * 30}ms` }, onClick: () => actions.browserNewTab(sessionId) }, (0, import_react28.createElement)(PlusIcon), t("New tab"))
9671
10260
  );
9672
10261
  }
9673
10262
  function About({ text, title = "About" }) {
9674
- const [open, setOpen] = (0, import_react27.useState)(false);
10263
+ const [open, setOpen] = (0, import_react28.useState)(false);
9675
10264
  const long = text.split("\n").length > 5 || text.length > 240;
9676
- const body = (0, import_react27.useRef)(null);
10265
+ const body = (0, import_react28.useRef)(null);
9677
10266
  useMorph(body, open);
9678
- return (0, import_react27.createElement)(
10267
+ return (0, import_react28.createElement)(
9679
10268
  "div",
9680
10269
  null,
9681
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t(title)),
9682
- (0, import_react27.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text),
9683
- long ? (0, import_react27.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null
10270
+ (0, import_react28.createElement)("div", { className: "bt-section-title" }, t(title)),
10271
+ (0, import_react28.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text),
10272
+ long ? (0, import_react28.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null
9684
10273
  );
9685
10274
  }
9686
10275
  function GroupOverview({ room: room2, roster, actions, usage, openSettings }) {
9687
10276
  const members = room2.members.map((id) => roster.byId[id]).filter(Boolean);
9688
10277
  return [
9689
- 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")),
9690
- (0, import_react27.createElement)(
10278
+ room2.notice ? (0, import_react28.createElement)(About, { key: "notice", title: "Notice", text: room2.notice }) : (0, import_react28.createElement)("button", { key: "notice", type: "button", className: "bt-more", onClick: openSettings }, t("Add a notice in group settings")),
10279
+ (0, import_react28.createElement)(
9691
10280
  "div",
9692
10281
  { key: "members" },
9693
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length })),
9694
- (0, import_react27.createElement)("div", { className: "bt-members", role: "list" }, members.map((member) => (0, import_react27.createElement)(
10282
+ (0, import_react28.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: members.length })),
10283
+ (0, import_react28.createElement)("div", { className: "bt-members", role: "list" }, members.map((member) => (0, import_react28.createElement)(
9695
10284
  "div",
9696
10285
  { key: member.id, className: "bt-member", role: "listitem" },
9697
- (0, import_react27.createElement)(
10286
+ (0, import_react28.createElement)(
9698
10287
  "button",
9699
10288
  { type: "button", className: "bt-member-name", title: t("Open {name}'s chat", { name: member.name }), onClick: () => actions.openSession(member.id) },
9700
- (0, import_react27.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
9701
- (0, import_react27.createElement)("span", null, member.name)
10289
+ (0, import_react28.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: room2.admin === member.id, live: false }),
10290
+ (0, import_react28.createElement)("span", null, member.name)
9702
10291
  ),
9703
- room2.admin === member.id ? (0, import_react27.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
10292
+ room2.admin === member.id ? (0, import_react28.createElement)("span", { className: "bt-chip bt-chip-admin" }, t("Admin")) : null
9704
10293
  )))
9705
10294
  ),
9706
- (0, import_react27.createElement)(
10295
+ (0, import_react28.createElement)(
9707
10296
  "div",
9708
10297
  { key: "mode" },
9709
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
9710
- (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")))
10298
+ (0, import_react28.createElement)("div", { className: "bt-section-title" }, t("Who answers")),
10299
+ (0, import_react28.createElement)("div", { className: "bt-kv" }, (0, import_react28.createElement)("span", null, t(ROOM_MODES.find(([id]) => id === shownMode(room2, roster))?.[1] ?? "Everyone")))
9711
10300
  ),
9712
- usage ? (0, import_react27.createElement)(
10301
+ usage ? (0, import_react28.createElement)(
9713
10302
  "div",
9714
10303
  { key: "usage" },
9715
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Usage")),
9716
- (0, import_react27.createElement)("div", { className: "bt-kv" }, (0, import_react27.createElement)("span", null, room2.name), (0, import_react27.createElement)("span", null, usage))
10304
+ (0, import_react28.createElement)("div", { className: "bt-section-title" }, t("Usage")),
10305
+ (0, import_react28.createElement)("div", { className: "bt-kv" }, (0, import_react28.createElement)("span", null, room2.name), (0, import_react28.createElement)("span", null, usage))
9717
10306
  ) : null
9718
10307
  ];
9719
10308
  }
@@ -9721,15 +10310,15 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
9721
10310
  const owner = roster.byId[sessionId];
9722
10311
  const part = owner ? currentPart(owner) : sessionId;
9723
10312
  const isMainBot = isMainOf(roster, owner?.id);
9724
- const [tab, setTab] = (0, import_react27.useState)("details");
10313
+ const [tab, setTab] = (0, import_react28.useState)("details");
9725
10314
  const room2 = roster.roomsById[sessionId];
9726
- const [view, setView] = (0, import_react27.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
9727
- const [editing, setEditing] = (0, import_react27.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
9728
- const [draftName, setDraftName] = (0, import_react27.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
9729
- const [copied, setCopied] = (0, import_react27.useState)(false);
9730
- const [error, setError] = (0, import_react27.useState)("");
10315
+ const [view, setView] = (0, import_react28.useState)(startTab === "settings" && (owner || room2) ? "settings" : "overview");
10316
+ const [editing, setEditing] = (0, import_react28.useState)(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])));
10317
+ const [draftName, setDraftName] = (0, import_react28.useState)(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ""));
10318
+ const [copied, setCopied] = (0, import_react28.useState)(false);
10319
+ const [error, setError] = (0, import_react28.useState)("");
9731
10320
  const glide = useGlide(tab);
9732
- const last = (0, import_react27.useRef)({ view, tab, move: void 0 });
10321
+ const last = (0, import_react28.useRef)({ view, tab, move: void 0 });
9733
10322
  if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" };
9734
10323
  else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" };
9735
10324
  const move = last.current.move;
@@ -9754,36 +10343,36 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
9754
10343
  setTimeout(() => setCopied(false), 1600);
9755
10344
  });
9756
10345
  };
9757
- const errorLine = error ? (0, import_react27.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
10346
+ const errorLine = error ? (0, import_react28.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null;
9758
10347
  if ((bot || room2) && view === "settings") {
9759
10348
  return [
9760
- (0, import_react27.createElement)(
10349
+ (0, import_react28.createElement)(
9761
10350
  "div",
9762
10351
  { key: "head", className: "bt-subhead", "data-move": move },
9763
- (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)),
9764
- (0, import_react27.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
9765
- 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))
10352
+ (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to details"), onClick: () => setView("overview") }, (0, import_react28.createElement)(ChevronLeftIcon)),
10353
+ (0, import_react28.createElement)("span", { className: "bt-subhead-title" }, bot ? t("Settings") : t("Group settings")),
10354
+ hasStrip ? null : (0, import_react28.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close details"), onClick: close }, (0, import_react28.createElement)(CloseIcon))
9766
10355
  ),
9767
- (0, import_react27.createElement)(
10356
+ (0, import_react28.createElement)(
9768
10357
  "div",
9769
10358
  { key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move },
9770
10359
  errorLine,
9771
- bot ? (0, import_react27.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react27.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
10360
+ bot ? (0, import_react28.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react28.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
9772
10361
  )
9773
10362
  ];
9774
10363
  }
9775
- const top = (0, import_react27.createElement)(
10364
+ const top = (0, import_react28.createElement)(
9776
10365
  "div",
9777
10366
  { className: "bt-drawer-top", "data-move": side },
9778
- 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,
9779
- 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,
9780
- 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,
9781
- hasStrip ? null : (0, import_react27.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react27.createElement)(CloseIcon))
10367
+ bot || room2 ? (0, import_react28.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_react28.createElement)(GearIcon)) : null,
10368
+ bot && roster.browser === true && actions.openBrowser ? (0, import_react28.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_react28.createElement)(GlobeIcon)) : null,
10369
+ bot || room2 ? (0, import_react28.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_react28.createElement)("span", { key: "copied", className: "bt-pop-in" }, "✓") : (0, import_react28.createElement)(ShareIcon)) : null,
10370
+ hasStrip ? null : (0, import_react28.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Close details"), onClick: close }, (0, import_react28.createElement)(CloseIcon))
9782
10371
  );
9783
10372
  if (!bot && !room2) {
9784
10373
  return [
9785
- (0, import_react27.createElement)(import_react27.Fragment, { key: "top" }, top),
9786
- (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.")))
10374
+ (0, import_react28.createElement)(import_react28.Fragment, { key: "top" }, top),
10375
+ (0, import_react28.createElement)("div", { key: "body", className: "bt-drawer-body" }, (0, import_react28.createElement)("div", { className: "bt-muted" }, t("This conversation is not part of your Bot team.")))
9787
10376
  ];
9788
10377
  }
9789
10378
  const current = bot?.name ?? room2.name;
@@ -9797,12 +10386,12 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
9797
10386
  const subtitle = bot ? bot.role : roomSubtitle(room2, roster);
9798
10387
  const tabs = bot ? ["details", "browser"] : ["details"];
9799
10388
  return [
9800
- (0, import_react27.createElement)(import_react27.Fragment, { key: "top" }, top),
9801
- (0, import_react27.createElement)(
10389
+ (0, import_react28.createElement)(import_react28.Fragment, { key: "top" }, top),
10390
+ (0, import_react28.createElement)(
9802
10391
  "div",
9803
10392
  { key: "id", className: "bt-drawer-id", "data-move": side },
9804
- 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 }),
9805
- editing ? (0, import_react27.createElement)("input", {
10393
+ bot ? (0, import_react28.createElement)(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== void 0), main: isMainBot, badge: false, gaze: true, pokeable: true }) : (0, import_react28.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
10394
+ editing ? (0, import_react28.createElement)("input", {
9806
10395
  className: "bt-drawer-name-input",
9807
10396
  autoFocus: true,
9808
10397
  value: draftName,
@@ -9817,48 +10406,49 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
9817
10406
  setEditing(false);
9818
10407
  }
9819
10408
  }
9820
- }) : (0, import_react27.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => {
10409
+ }) : (0, import_react28.createElement)("button", { type: "button", className: "bt-drawer-name", "aria-label": t("Rename {name}", { name: current }), title: t("Rename"), onClick: () => {
9821
10410
  setDraftName(room2 && !room2.named ? "" : current);
9822
10411
  setEditing(true);
9823
10412
  } }, current),
9824
- subtitle ? (0, import_react27.createElement)("span", { className: "bt-drawer-role" }, subtitle) : null
10413
+ subtitle ? (0, import_react28.createElement)("span", { className: "bt-drawer-role" }, subtitle) : null
9825
10414
  ),
9826
- (0, import_react27.createElement)(
10415
+ (0, import_react28.createElement)(
9827
10416
  "div",
9828
10417
  { key: "tabs", ref: glide.box, className: "bt-tabs", role: "tablist", "data-move": side },
9829
- (0, import_react27.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
9830
- 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])))
10418
+ (0, import_react28.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }),
10419
+ tabs.map((key) => (0, import_react28.createElement)("button", { key, type: "button", role: "tab", className: "bt-tab", "aria-selected": tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key])))
9831
10420
  ),
9832
- (0, import_react27.createElement)(
10421
+ (0, import_react28.createElement)(
9833
10422
  "div",
9834
10423
  { key: `body:${tab}`, className: "bt-drawer-body", "data-move": move },
9835
10424
  errorLine,
9836
- room2 && tab === "details" ? (0, import_react27.createElement)(GroupOverview, { room: room2, roster, actions, usage, openSettings: () => setView("settings") }) : null,
10425
+ room2 && tab === "details" ? (0, import_react28.createElement)(GroupOverview, { room: room2, roster, actions, usage, openSettings: () => setView("settings") }) : null,
9837
10426
  bot && tab === "details" ? [
9838
- (0, import_react27.createElement)(
10427
+ (0, import_react28.createElement)(
9839
10428
  "div",
9840
10429
  { key: "model", className: "bt-drawer-model" },
9841
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Model")),
9842
- (0, import_react27.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
9843
- modelOf(bot, roster) ? null : (0, import_react27.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name }))
10430
+ (0, import_react28.createElement)("div", { className: "bt-section-title" }, t("Model")),
10431
+ (0, import_react28.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
10432
+ modelOf(bot, roster) ? null : (0, import_react28.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name }))
9844
10433
  ),
9845
- 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")),
9846
- roster.memory === false ? null : (0, import_react27.createElement)(MemoryRow, { key: "memory", bot, actions }),
9847
- usage ? (0, import_react27.createElement)(
10434
+ (0, import_react28.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
10435
+ bot.instructions ? (0, import_react28.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react28.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
10436
+ roster.memory === false ? null : (0, import_react28.createElement)(MemoryRow, { key: "memory", bot, actions }),
10437
+ usage ? (0, import_react28.createElement)(
9848
10438
  "div",
9849
10439
  { key: "usage" },
9850
- (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Usage")),
9851
- (0, import_react27.createElement)("div", { className: "bt-kv" }, (0, import_react27.createElement)("span", null, bot.name), (0, import_react27.createElement)("span", null, usage))
10440
+ (0, import_react28.createElement)("div", { className: "bt-section-title" }, t("Usage")),
10441
+ (0, import_react28.createElement)("div", { className: "bt-kv" }, (0, import_react28.createElement)("span", null, bot.name), (0, import_react28.createElement)("span", null, usage))
9852
10442
  ) : null,
9853
- (0, import_react27.createElement)(ScheduleSummary, { key: "routines", bot, actions })
10443
+ (0, import_react28.createElement)(ScheduleSummary, { key: "routines", bot, actions })
9854
10444
  ] : null,
9855
- bot && tab === "browser" ? (0, import_react27.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
10445
+ bot && tab === "browser" ? (0, import_react28.createElement)(BrowserCards, { bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) : null
9856
10446
  )
9857
10447
  ];
9858
10448
  }
9859
10449
 
9860
10450
  // src/client/browser-pane.js
9861
- var import_react28 = require("react");
10451
+ var import_react29 = require("react");
9862
10452
  var import_react_dom7 = require("react-dom");
9863
10453
  var PANEL_LINGER_MS = 300;
9864
10454
  var WIDTH_KEY = STORAGE.browserWidth;
@@ -9873,10 +10463,10 @@ var EMPTY_TABS2 = { ids: [], active: null };
9873
10463
  var SPRING_LEAD2 = springEasing(576, 39.4);
9874
10464
  var SPRING_TRAIL2 = springEasing(144, 19.7);
9875
10465
  var icon = (paths) => function BrowserIcon() {
9876
- return (0, import_react28.createElement)(
10466
+ return (0, import_react29.createElement)(
9877
10467
  "svg",
9878
10468
  { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
9879
- paths.map((d, index) => (0, import_react28.createElement)("path", { key: index, d }))
10469
+ paths.map((d, index) => (0, import_react29.createElement)("path", { key: index, d }))
9880
10470
  );
9881
10471
  };
9882
10472
  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"]);
@@ -9888,9 +10478,9 @@ var CloseIcon2 = icon(["M5.5 5.5l9 9M14.5 5.5l-9 9"]);
9888
10478
  var PlusTabIcon = icon(["M10 5v10M5 10h10"]);
9889
10479
  var LockIcon2 = icon(["M6.5 9V7a3.5 3.5 0 0 1 7 0v2", "M5.5 9h9v7h-9z"]);
9890
10480
  function Favicon({ src, className }) {
9891
- const [failed, setFailed] = (0, import_react28.useState)(null);
9892
- if (!src || failed === src) return (0, import_react28.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react28.createElement)(GlobeIcon));
9893
- return (0, import_react28.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
10481
+ const [failed, setFailed] = (0, import_react29.useState)(null);
10482
+ if (!src || failed === src) return (0, import_react29.createElement)("span", { className: `${className} bt-ptab-globe` }, (0, import_react29.createElement)(GlobeIcon));
10483
+ return (0, import_react29.createElement)("img", { className, src, alt: "", "aria-hidden": true, onError: () => setFailed(src) });
9894
10484
  }
9895
10485
  var savedWidth = () => {
9896
10486
  try {
@@ -9949,11 +10539,11 @@ var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(pref
9949
10539
  var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab");
9950
10540
  function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) {
9951
10541
  const pages = useBrowserPages((map) => map);
9952
- const wrap = (0, import_react28.useRef)(null);
9953
- const ind = (0, import_react28.useRef)(null);
9954
- const spot = (0, import_react28.useRef)(null);
9955
- const under = (0, import_react28.useRef)(null);
9956
- (0, import_react28.useLayoutEffect)(() => {
10542
+ const wrap = (0, import_react29.useRef)(null);
10543
+ const ind = (0, import_react29.useRef)(null);
10544
+ const spot = (0, import_react29.useRef)(null);
10545
+ const under = (0, import_react29.useRef)(null);
10546
+ (0, import_react29.useLayoutEffect)(() => {
9957
10547
  const box = wrap.current;
9958
10548
  const capsule = ind.current;
9959
10549
  if (!box || !capsule) return void 0;
@@ -9982,7 +10572,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
9982
10572
  trail.cancel();
9983
10573
  };
9984
10574
  });
9985
- (0, import_react28.useEffect)(() => {
10575
+ (0, import_react29.useEffect)(() => {
9986
10576
  const box = wrap.current;
9987
10577
  if (!box) return void 0;
9988
10578
  const observer = new ResizeObserver(() => {
@@ -9996,13 +10586,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
9996
10586
  observer.observe(box);
9997
10587
  return () => observer.disconnect();
9998
10588
  }, []);
9999
- return (0, import_react28.createElement)(
10589
+ return (0, import_react29.createElement)(
10000
10590
  "div",
10001
10591
  { className: "bt-ptabs", role: "tablist", "aria-label": t("Panel tabs") },
10002
- (0, import_react28.createElement)(
10592
+ (0, import_react29.createElement)(
10003
10593
  "div",
10004
10594
  { className: "bt-ptabs-in", ref: wrap },
10005
- (0, import_react28.createElement)(
10595
+ (0, import_react29.createElement)(
10006
10596
  "button",
10007
10597
  {
10008
10598
  type: "button",
@@ -10012,13 +10602,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
10012
10602
  title: bot.name,
10013
10603
  onClick: () => onView("details")
10014
10604
  },
10015
- (0, import_react28.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
10016
- view === "details" ? (0, import_react28.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null
10605
+ (0, import_react29.createElement)(BotMark, { bot, size: 20, state: reading ? "searching" : "idle" }),
10606
+ view === "details" ? (0, import_react29.createElement)("span", { className: "bt-ptab-name" }, bot.name) : null
10017
10607
  ),
10018
10608
  ids.map((tabId) => {
10019
10609
  const page = pages[tabId] ?? EMPTY_PAGE;
10020
10610
  const on = view === "browser" && tabId === active;
10021
- return (0, import_react28.createElement)(
10611
+ return (0, import_react29.createElement)(
10022
10612
  "button",
10023
10613
  {
10024
10614
  key: tabId,
@@ -10039,9 +10629,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
10039
10629
  }
10040
10630
  }
10041
10631
  },
10042
- (0, import_react28.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
10043
- (0, import_react28.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
10044
- (0, import_react28.createElement)("span", {
10632
+ (0, import_react29.createElement)(Favicon, { src: page.favicon, className: "bt-ptab-fav" }),
10633
+ (0, import_react29.createElement)("span", { className: "bt-ptab-title" }, tabLabel(page)),
10634
+ (0, import_react29.createElement)("span", {
10045
10635
  className: "bt-ptab-x",
10046
10636
  role: "button",
10047
10637
  "aria-label": t("Close tab"),
@@ -10053,20 +10643,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions,
10053
10643
  }, "×")
10054
10644
  );
10055
10645
  }),
10056
- (0, import_react28.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true })
10646
+ (0, import_react29.createElement)("span", { ref: ind, className: "bt-ptab-ind", "aria-hidden": true })
10057
10647
  ),
10058
10648
  // New tab and Close stay outside the scrolling tabs, so a narrow panel keeps both.
10059
- (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)),
10060
- (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))
10649
+ (0, import_react29.createElement)("button", { type: "button", className: "bt-ptab-new", "aria-label": t("New tab"), title: t("New tab"), onClick: () => actions.browserNewTab(subject) }, (0, import_react29.createElement)(PlusTabIcon)),
10650
+ (0, import_react29.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_react29.createElement)(CloseIcon2))
10061
10651
  );
10062
10652
  }
10063
10653
  function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) {
10064
10654
  const page = useBrowserPages((pages) => pages[tabId ?? ""]) ?? EMPTY_PAGE;
10065
- const frame = (0, import_react28.useRef)(null);
10066
- const input = (0, import_react28.useRef)(null);
10067
- const [draft, setDraft] = (0, import_react28.useState)(null);
10655
+ const frame = (0, import_react29.useRef)(null);
10656
+ const input = (0, import_react29.useRef)(null);
10657
+ const [draft, setDraft] = (0, import_react29.useState)(null);
10068
10658
  const showGuest = landed && !leaving && Boolean(page.url) && !page.error;
10069
- (0, import_react28.useLayoutEffect)(() => {
10659
+ (0, import_react29.useLayoutEffect)(() => {
10070
10660
  const element = frame.current;
10071
10661
  if (!element || leaving || !tabId) {
10072
10662
  void actions.browserShow(null);
@@ -10092,7 +10682,7 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
10092
10682
  window.removeEventListener("resize", schedule);
10093
10683
  };
10094
10684
  }, [tabId, showGuest, leaving]);
10095
- (0, import_react28.useEffect)(() => {
10685
+ (0, import_react29.useEffect)(() => {
10096
10686
  setDraft(null);
10097
10687
  if (landed && !page.url) input.current?.focus();
10098
10688
  }, [tabId, landed]);
@@ -10108,27 +10698,27 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
10108
10698
  };
10109
10699
  const editing = draft !== null;
10110
10700
  const secure = /^https:/i.test(page.url);
10111
- return (0, import_react28.createElement)(
10701
+ return (0, import_react29.createElement)(
10112
10702
  "div",
10113
10703
  { className: "bt-panel-view bt-panel-browser", "aria-label": t("{name}'s browser", { name: bot.name }) },
10114
- (0, import_react28.createElement)(
10704
+ (0, import_react29.createElement)(
10115
10705
  "div",
10116
10706
  { className: "bt-browser-bar" },
10117
- (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)),
10118
- (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)),
10119
- (0, import_react28.createElement)("button", {
10707
+ (0, import_react29.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_react29.createElement)(BackIcon)),
10708
+ (0, import_react29.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_react29.createElement)(ForwardIcon)),
10709
+ (0, import_react29.createElement)("button", {
10120
10710
  type: "button",
10121
10711
  className: "bt-icon-btn",
10122
10712
  disabled: !page.url,
10123
10713
  "aria-label": page.loading ? t("Stop") : t("Reload"),
10124
10714
  title: page.loading ? t("Stop") : t("Reload"),
10125
10715
  onClick: () => actions.browserCommand(tabId, page.loading ? "stop" : "reload")
10126
- }, (0, import_react28.createElement)(page.loading ? StopIcon : ReloadIcon)),
10127
- (0, import_react28.createElement)(
10716
+ }, (0, import_react29.createElement)(page.loading ? StopIcon : ReloadIcon)),
10717
+ (0, import_react29.createElement)(
10128
10718
  "form",
10129
10719
  { className: "bt-browser-address", onSubmit: go, "data-editing": editing || void 0 },
10130
- (0, import_react28.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react28.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
10131
- (0, import_react28.createElement)("input", {
10720
+ (0, import_react29.createElement)("span", { className: "bt-browser-scheme", "aria-hidden": true }, (0, import_react29.createElement)(secure && !editing ? LockIcon2 : GlobeIcon)),
10721
+ (0, import_react29.createElement)("input", {
10132
10722
  ref: input,
10133
10723
  type: "text",
10134
10724
  spellCheck: false,
@@ -10152,25 +10742,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages })
10152
10742
  }
10153
10743
  })
10154
10744
  ),
10155
- page.loading ? (0, import_react28.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
10745
+ page.loading ? (0, import_react29.createElement)("span", { className: "bt-browser-progress", "aria-hidden": true }) : null
10156
10746
  ),
10157
- (0, import_react28.createElement)(
10747
+ (0, import_react29.createElement)(
10158
10748
  "div",
10159
10749
  { ref: frame, className: "bt-browser-frame", "data-guest": showGuest || void 0 },
10160
- page.error ? (0, import_react28.createElement)(
10750
+ page.error ? (0, import_react29.createElement)(
10161
10751
  "div",
10162
10752
  { className: "bt-browser-empty" },
10163
- (0, import_react28.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
10164
- (0, import_react28.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
10165
- page.url ? (0, import_react28.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => {
10753
+ (0, import_react29.createElement)("div", { className: "bt-browser-empty-title" }, t("This page didn’t load")),
10754
+ (0, import_react29.createElement)("div", { className: "bt-browser-empty-text" }, t(page.error)),
10755
+ page.url ? (0, import_react29.createElement)("button", { type: "button", className: "bt-browser-retry", onClick: () => {
10166
10756
  void actions.browserNavigate(tabId, page.url);
10167
10757
  } }, t("Try again")) : null
10168
- ) : !page.url ? (0, import_react28.createElement)(
10758
+ ) : !page.url ? (0, import_react29.createElement)(
10169
10759
  "div",
10170
10760
  { className: "bt-browser-empty" },
10171
- (0, import_react28.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }),
10172
- (0, import_react28.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
10173
- (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 }))
10761
+ (0, import_react29.createElement)(BotMark, { bot, size: 48, state: "idle", live: true }),
10762
+ (0, import_react29.createElement)("div", { className: "bt-browser-empty-title" }, t("{name}'s browser", { name: bot.name })),
10763
+ (0, import_react29.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 }))
10174
10764
  ) : null
10175
10765
  )
10176
10766
  );
@@ -10182,41 +10772,41 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
10182
10772
  const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2);
10183
10773
  const active = tabState.active;
10184
10774
  const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE);
10185
- const root = (0, import_react28.useRef)(null);
10186
- const fly = (0, import_react28.useRef)(null);
10187
- const [landed, setLanded] = (0, import_react28.useState)(false);
10188
- const [settling, setSettling] = (0, import_react28.useState)(false);
10189
- const [width, setWidth] = (0, import_react28.useState)(savedWidth);
10190
- const [viewport, setViewport] = (0, import_react28.useState)(() => window.innerWidth);
10191
- const [dragging, setDragging] = (0, import_react28.useState)(false);
10192
- const [resizing, setResizing] = (0, import_react28.useState)(false);
10775
+ const root = (0, import_react29.useRef)(null);
10776
+ const fly = (0, import_react29.useRef)(null);
10777
+ const [landed, setLanded] = (0, import_react29.useState)(false);
10778
+ const [settling, setSettling] = (0, import_react29.useState)(false);
10779
+ const [width, setWidth] = (0, import_react29.useState)(savedWidth);
10780
+ const [viewport, setViewport] = (0, import_react29.useState)(() => window.innerWidth);
10781
+ const [dragging, setDragging] = (0, import_react29.useState)(false);
10782
+ const [resizing, setResizing] = (0, import_react29.useState)(false);
10193
10783
  const instant = dragging || resizing;
10194
- const [reading, setReading] = (0, import_react28.useState)(false);
10784
+ const [reading, setReading] = (0, import_react29.useState)(false);
10195
10785
  const panelWidth = fitWidth(width ?? (view === "browser" ? DEFAULT_WIDTH : DETAILS_WIDTH), widthRange(view, viewport));
10196
- (0, import_react28.useLayoutEffect)(() => {
10786
+ (0, import_react29.useLayoutEffect)(() => {
10197
10787
  if (view === "browser" && width === null) setWidth(DEFAULT_WIDTH);
10198
10788
  }, [view, width]);
10199
- (0, import_react28.useLayoutEffect)(() => {
10789
+ (0, import_react29.useLayoutEffect)(() => {
10200
10790
  chatRoom.hold(instant);
10201
10791
  }, [instant]);
10202
- const [last, setLast] = (0, import_react28.useState)({ view, width: panelWidth });
10792
+ const [last, setLast] = (0, import_react29.useState)({ view, width: panelWidth });
10203
10793
  if (last.view !== view || last.width !== panelWidth) {
10204
10794
  setLast({ view, width: panelWidth });
10205
10795
  if (last.view !== view) setSettling(last.width !== panelWidth && !instant);
10206
10796
  }
10207
- const shownView = (0, import_react28.useRef)(view);
10208
- (0, import_react28.useLayoutEffect)(() => {
10797
+ const shownView = (0, import_react29.useRef)(view);
10798
+ (0, import_react29.useLayoutEffect)(() => {
10209
10799
  const switched = settling && shownView.current !== view;
10210
10800
  shownView.current = view;
10211
10801
  if (switched) chatRoom.hold(true);
10212
10802
  chatRoom.set(leaving ? null : panelWidth);
10213
10803
  if (switched) chatRoom.hold(false);
10214
10804
  }, [view, panelWidth, leaving]);
10215
- (0, import_react28.useEffect)(() => () => {
10805
+ (0, import_react29.useEffect)(() => () => {
10216
10806
  chatRoom.set(null);
10217
10807
  chatRoom.hold(false);
10218
10808
  }, []);
10219
- (0, import_react28.useEffect)(() => {
10809
+ (0, import_react29.useEffect)(() => {
10220
10810
  let timer;
10221
10811
  const onResize = () => {
10222
10812
  setResizing(true);
@@ -10230,28 +10820,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
10230
10820
  window.removeEventListener("resize", onResize);
10231
10821
  };
10232
10822
  }, []);
10233
- (0, import_react28.useEffect)(() => {
10823
+ (0, import_react29.useEffect)(() => {
10234
10824
  if (!settling) return void 0;
10235
10825
  const timer = setTimeout(() => setSettling(false), SPRING_SHAPE.duration + 40);
10236
10826
  return () => clearTimeout(timer);
10237
10827
  }, [settling, view]);
10238
- (0, import_react28.useEffect)(() => {
10828
+ (0, import_react29.useEffect)(() => {
10239
10829
  const timer = setTimeout(() => setLanded(true), 360);
10240
10830
  return () => clearTimeout(timer);
10241
10831
  }, []);
10242
- (0, import_react28.useEffect)(() => {
10832
+ (0, import_react29.useEffect)(() => {
10243
10833
  if (canBrowse) actions.browserEnsure(bot.id);
10244
10834
  }, [bot?.id, canBrowse]);
10245
- (0, import_react28.useEffect)(() => {
10835
+ (0, import_react29.useEffect)(() => {
10246
10836
  if (view === "browser" && (active === null || !tabState.ids.includes(active))) actions.setPanelView("details");
10247
10837
  }, [view, active, tabState.ids.length]);
10248
- (0, import_react28.useEffect)(() => {
10838
+ (0, import_react29.useEffect)(() => {
10249
10839
  if (view !== "browser" || leaving) void actions.browserShow?.(null);
10250
10840
  }, [view, leaving]);
10251
- (0, import_react28.useEffect)(() => () => {
10841
+ (0, import_react29.useEffect)(() => () => {
10252
10842
  void actions.browserShow?.(null);
10253
10843
  }, []);
10254
- (0, import_react28.useEffect)(() => {
10844
+ (0, import_react29.useEffect)(() => {
10255
10845
  if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0;
10256
10846
  setReading(true);
10257
10847
  const timer = setTimeout(() => setReading(false), READ_FLASH_MS);
@@ -10288,7 +10878,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
10288
10878
  return actions[name](...args);
10289
10879
  };
10290
10880
  }
10291
- (0, import_react28.useLayoutEffect)(() => {
10881
+ (0, import_react29.useLayoutEffect)(() => {
10292
10882
  const move = fly.current;
10293
10883
  if (!move) return;
10294
10884
  fly.current = null;
@@ -10371,7 +10961,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
10371
10961
  for (const name of ends) grip.addEventListener(name, onEnd);
10372
10962
  };
10373
10963
  const hasStrip = canBrowse && tabState.ids.length > 0;
10374
- return (0, import_react28.createElement)(
10964
+ return (0, import_react29.createElement)(
10375
10965
  "aside",
10376
10966
  {
10377
10967
  ref: root,
@@ -10388,15 +10978,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu
10388
10978
  if (event.target === event.currentTarget) setLanded(true);
10389
10979
  }
10390
10980
  },
10391
- leaving ? null : (0, import_react28.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }),
10981
+ leaving ? null : (0, import_react29.createElement)("div", { className: "bt-panel-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("Resize panel"), onPointerDown: startResize }),
10392
10982
  // Over the whole window while dragging: the cursor stays a resize cursor and the
10393
10983
  // pages underneath (the chat, the guest) see no pointer.
10394
- dragging ? (0, import_react_dom7.createPortal)((0, import_react28.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
10395
- hasStrip ? (0, import_react28.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
10396
- view === "browser" && bot ? (0, import_react28.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react28.createElement)(
10984
+ dragging ? (0, import_react_dom7.createPortal)((0, import_react29.createElement)("div", { className: "bt-drag-shield", "aria-hidden": true }), document.body) : null,
10985
+ hasStrip ? (0, import_react29.createElement)(PanelStrip, { bot, subject: pane.id, view, ids: tabState.ids, active, reading, onView: switchView, actions: panelActions, useBrowserPages }) : null,
10986
+ view === "browser" && bot ? (0, import_react29.createElement)(BrowserView, { key: `browser:${active}`, bot, tabId: active, leaving, landed: landed && !settling, actions, useBrowserPages }) : (0, import_react29.createElement)(
10397
10987
  "div",
10398
10988
  { key: `details:${pane.id}:${pane.renaming ? "rename" : ""}:${pane.tab ?? ""}`, className: "bt-panel-view" },
10399
- (0, import_react28.createElement)(DetailsDrawer, {
10989
+ (0, import_react29.createElement)(DetailsDrawer, {
10400
10990
  sessionId: pane.id,
10401
10991
  startRenaming: pane.renaming,
10402
10992
  startTab: pane.tab,
@@ -10492,24 +11082,24 @@ var BROWSER_CSS = `
10492
11082
  `;
10493
11083
 
10494
11084
  // src/client/overlays.js
10495
- var import_react29 = require("react");
11085
+ var import_react30 = require("react");
10496
11086
  var import_react_dom8 = require("react-dom");
10497
11087
  var DIALOG_EXIT_MS = 160;
10498
11088
  var MENU_EXIT_MS3 = 120;
10499
- function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useRegistry, useBrowserPages, useBrowserTabs, actions }) {
11089
+ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useRegistry, useBrowserPages, useBrowserTabs, useUpdate, actions }) {
10500
11090
  const roster = useRoster((value) => value);
10501
11091
  const ui = useUi((value) => value);
10502
11092
  useRegistry((value) => value);
10503
11093
  const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null);
10504
- const previous = (0, import_react29.useRef)(current);
10505
- (0, import_react29.useEffect)(() => {
11094
+ const previous = (0, import_react30.useRef)(current);
11095
+ (0, import_react30.useEffect)(() => {
10506
11096
  if (previous.current === current) return;
10507
11097
  previous.current = current;
10508
11098
  if (current) actions.followPanel?.(current);
10509
11099
  }, [current]);
10510
11100
  const open = ui.details ? { id: ui.details, view: ui.view ?? "details", renaming: ui.renaming, tab: ui.tab } : null;
10511
- const [kept, setKept] = (0, import_react29.useState)(open);
10512
- (0, import_react29.useEffect)(() => {
11101
+ const [kept, setKept] = (0, import_react30.useState)(open);
11102
+ (0, import_react30.useEffect)(() => {
10513
11103
  if (open) {
10514
11104
  setKept(open);
10515
11105
  return void 0;
@@ -10521,17 +11111,17 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity
10521
11111
  const [settings, settingsLeaving] = useLinger(ui.settings, DIALOG_EXIT_MS);
10522
11112
  const [memory, memoryLeaving] = useLinger(ui.memory && roster.byId[ui.memory] ? ui.memory : null, DIALOG_EXIT_MS);
10523
11113
  const [menu, menuLeaving] = useLinger(ui.menu, MENU_EXIT_MS3);
10524
- return (0, import_react29.createElement)(
10525
- import_react29.Fragment,
11114
+ return (0, import_react30.createElement)(
11115
+ import_react30.Fragment,
10526
11116
  null,
10527
- 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,
10528
- ui.newChat ? (0, import_react_dom8.createPortal)((0, import_react29.createElement)(NewChat, { key: ui.newChat, mode: ui.newChat, roster, actions }), document.body) : null,
10529
- 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,
10530
- ui.exchange ? (0, import_react_dom8.createPortal)((0, import_react29.createElement)(ExchangeDialog, { pair: ui.exchange, roster, actions }), document.body) : null,
10531
- 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,
10532
- 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,
10533
- settings ? (0, import_react_dom8.createPortal)((0, import_react29.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, leaving: settingsLeaving }), document.body) : null,
10534
- 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
11117
+ pane ? (0, import_react_dom8.createPortal)((0, import_react30.createElement)(BotPanel, { pane, leaving: open === null, roster, actions, useSessions, useSessionStatus, useActivity, useBrowserTabs, useBrowserPages }), document.body) : null,
11118
+ ui.newChat ? (0, import_react_dom8.createPortal)((0, import_react30.createElement)(NewChat, { key: ui.newChat, mode: ui.newChat, roster, actions }), document.body) : null,
11119
+ ui.palette ? (0, import_react_dom8.createPortal)((0, import_react30.createElement)(CommandPalette, { key: ui.palette.token, start: ui.palette.query, token: ui.palette.token, roster, actions, useSessions }), document.body) : null,
11120
+ ui.exchange ? (0, import_react_dom8.createPortal)((0, import_react30.createElement)(ExchangeDialog, { pair: ui.exchange, roster, actions }), document.body) : null,
11121
+ ui.voice ? (0, import_react_dom8.createPortal)((0, import_react30.createElement)(VoiceMode, { key: ui.voice, sessionId: ui.voice, roster, actions, useSessions, useSessionStatus }), document.body) : null,
11122
+ menu ? (0, import_react_dom8.createPortal)((0, import_react30.createElement)(ContextMenu, { key: `${menu.kind ?? ""}:${menu.id}:${menu.x}:${menu.y}`, menu, roster, actions, leaving: menuLeaving }), document.body) : null,
11123
+ settings ? (0, import_react_dom8.createPortal)((0, import_react30.createElement)(SettingsDialog, { page: settings.page, id: settings.id, roster, actions, useUpdate, leaving: settingsLeaving }), document.body) : null,
11124
+ memory && roster.byId[memory] ? (0, import_react_dom8.createPortal)((0, import_react30.createElement)(MemoryDialog, { key: memory, bot: roster.byId[memory], actions, leaving: memoryLeaving }), document.body) : null
10535
11125
  );
10536
11126
  }
10537
11127
 
@@ -11362,7 +11952,7 @@ function writeMode(mode, storage = globalThis.localStorage) {
11362
11952
  }
11363
11953
 
11364
11954
  // src/client/index.js
11365
- var inject = ["sessions", "uiWorkspace", "slots", "theme", "layout", "configForms"];
11955
+ var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout", "configForms"];
11366
11956
  function apply(ctx) {
11367
11957
  ctx.effect(() => installLocale(ctx.get("locale")), "ds-bot: interface copy");
11368
11958
  const roster = createSource(EMPTY_ROSTER);
@@ -11532,8 +12122,8 @@ function apply(ctx) {
11532
12122
  if (stopped) return;
11533
12123
  const value = await call("update-check", { fresh: false }).catch(() => null);
11534
12124
  if (stopped) return;
11535
- if (value) update.set(value);
11536
- timer = setTimeout(loop, value?.phase === "installing" ? 2e3 : 60 * 6e4);
12125
+ if (value) update.set((previous) => ({ ...value, app: previous.app }));
12126
+ timer = setTimeout(loop, value?.phase === "installing" || value?.dsh?.phase === "installing" ? 2e3 : 60 * 6e4);
11537
12127
  };
11538
12128
  wakeUpdate = loop;
11539
12129
  void loop();
@@ -11544,6 +12134,22 @@ function apply(ctx) {
11544
12134
  };
11545
12135
  };
11546
12136
  }, "ds-bot: update check");
12137
+ const appUpdates = window.dshDesktop?.protocolVersion === 1 ? window.dshDesktop.updates ?? null : null;
12138
+ if (appUpdates?.status && appUpdates.subscribe) {
12139
+ ctx.effect(() => {
12140
+ let stopped = false;
12141
+ const take = (app) => {
12142
+ if (!stopped && app) update.set((previous) => ({ ...previous, app }));
12143
+ };
12144
+ void appUpdates.status().then(take, () => {
12145
+ });
12146
+ const unsubscribe = appUpdates.subscribe(take);
12147
+ return () => {
12148
+ stopped = true;
12149
+ unsubscribe?.();
12150
+ };
12151
+ }, "ds-bot: app update status");
12152
+ }
11547
12153
  const bridge = window.dshDesktop?.protocolVersion === 1 ? window.dshDesktop.browser ?? null : null;
11548
12154
  const browsers = bridge ? createBotBrowsers({ bridge, createSource }) : null;
11549
12155
  if (browsers) {
@@ -11906,6 +12512,7 @@ function apply(ctx) {
11906
12512
  updateBot: (id, patch) => after(call("update-bot", { id, ...patch })),
11907
12513
  refreshModels: () => after(call("models")).catch(() => {
11908
12514
  }),
12515
+ efforts: (ref) => call("efforts", { ref }),
11909
12516
  deleteBot: (id) => after(call("delete-bot", { id })),
11910
12517
  duplicateBot: (id) => after(call("duplicate-bot", { id })),
11911
12518
  setPrefs: (prefs) => after(call("set-prefs", prefs)),
@@ -11992,11 +12599,33 @@ function apply(ctx) {
11992
12599
  actions.signIn = () => ui.set((state) => ({ ...state, signIn: (state.signIn ?? 0) + 1 }));
11993
12600
  actions.closeSignIn = () => ui.set((state) => ({ ...state, signIn: null }));
11994
12601
  actions.accountT = () => ctx.get("locale").bind("settings.account");
12602
+ actions.speech = () => ctx.get("remote.speech") ?? void 0;
12603
+ actions.voiceSetup = () => {
12604
+ const navigation = ctx.get("pluginNavigation");
12605
+ return navigation ? () => navigation.openBundle(VOICE_BUNDLE) : null;
12606
+ };
11995
12607
  actions.seatAccount = (on) => ui.set((state) => state.accountSeated === on ? state : { ...state, accountSeated: on });
11996
12608
  actions.installUpdate = async () => {
11997
- update.set(await call("update-install"));
12609
+ const value = await call("update-install");
12610
+ update.set((previous) => ({ ...previous, ...value }));
11998
12611
  wakeUpdate();
11999
12612
  };
12613
+ actions.installDshUpdate = async () => {
12614
+ const dsh = await call("dsh-update-install");
12615
+ update.set((previous) => ({ ...previous, dsh }));
12616
+ wakeUpdate();
12617
+ };
12618
+ actions.openAppUpdates = async (verified) => {
12619
+ const app = await appUpdates?.status?.();
12620
+ if (app) update.set((previous) => ({ ...previous, app }));
12621
+ if (sameVersion(app?.version, verified)) await appUpdates.open();
12622
+ };
12623
+ actions.checkUpdate = async () => {
12624
+ const value = await call("update-check", { fresh: true });
12625
+ update.set((previous) => ({ ...value, app: previous.app }));
12626
+ wakeUpdate();
12627
+ };
12628
+ actions.repoStars = () => call("repo-stars");
12000
12629
  actions.noteTurnTime = (sessionId, turn, key, time) => {
12001
12630
  if (sessionId === void 0 || !Number.isFinite(turn) || !Number.isFinite(time)) return;
12002
12631
  turnClock.set((state) => {
@@ -12037,6 +12666,18 @@ function apply(ctx) {
12037
12666
  const browserPages = browsers?.pages ?? createSource({});
12038
12667
  const browserTabs = browsers?.tabs ?? createSource({});
12039
12668
  const face = () => ({ actions, hooks: { roster, ui, activity, memoryNews, registry, turnClock, live, browserPages, browserTabs, surface, update } });
12669
+ const assistantFace = (sessionId) => {
12670
+ const binding = ctx.sessions.binding(sessionId);
12671
+ if (binding === void 0) throw new Error(`ds-bot: unknown session "${sessionId}"`);
12672
+ const chat = ctx.uiConversation.binding(binding).target("chat");
12673
+ return { ...face(), keyedHooks: {
12674
+ turnSteps: (turn) => {
12675
+ const snapshot = chat.getSnapshot();
12676
+ if (snapshot === void 0) throw new Error("ds-bot: Chat target is unavailable");
12677
+ return snapshot.nodes.turnDataSource(Number(turn), "assistant-step");
12678
+ }
12679
+ } };
12680
+ };
12040
12681
  const shadow = (name, options, component) => ctx.slots.inject(name, () => ctx.slots.register({ name, ...options }, component));
12041
12682
  const installShell = () => [
12042
12683
  installCompiledStyles(SHELL_CSS, "shell"),
@@ -12046,6 +12687,7 @@ function apply(ctx) {
12046
12687
  installCompiledStyles(USAGE_CSS, "usage"),
12047
12688
  installCompiledStyles(SCHEDULE_CSS, "schedules"),
12048
12689
  installCompiledStyles(FEEDBACK_CSS, "feedback"),
12690
+ installCompiledStyles(ABOUT_CSS, "about"),
12049
12691
  browsers ? installCompiledStyles(BROWSER_CSS, "browser") : null,
12050
12692
  installTheme(),
12051
12693
  shadow("sidebar.workspaces", { priority: -10, inject: face }, TeamSidebar),
@@ -12071,7 +12713,7 @@ function apply(ctx) {
12071
12713
  // the mode is held at `verbose` while this group is mounted.
12072
12714
  holdFlatTranscript(ctx.configForms.get("ui-chat")),
12073
12715
  shadow("conversation.session.header", { priority: -10, inject: face }, HeaderPill),
12074
- shadow("conversation.chat.node", { key: "assistant-step", priority: -10, locale: "chat", inject: face }, AssistantCell),
12716
+ shadow("conversation.chat.node", { key: "assistant-step", priority: -10, locale: "chat", inject: assistantFace }, AssistantCell),
12075
12717
  shadow("conversation.chat.node", { key: "turn-trigger", priority: -10, locale: "chat", inject: face }, TriggerCell),
12076
12718
  shadow("conversation.chat.node", { key: "tool-call", priority: -10, locale: "chat", inject: face }, ToolCell),
12077
12719
  shadow("conversation.chat.node", { key: "turn-process", priority: -10, locale: "chat", inject: face }, TurnProcessCell),