ds-bot 0.1.0 → 0.1.1

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 天",
@@ -802,6 +818,7 @@ var HOST_ERRORS = [
802
818
  ];
803
819
  var HOST_ZH = {
804
820
  "Request failed": "请求失败",
821
+ "Permissions are not available in this DeepSeek Harness": "这个 DeepSeek Harness 不支持权限设置",
805
822
  "Avatar images must be PNG, JPEG, WebP, or GIF": "头像图片必须是 PNG、JPEG、WebP 或 GIF",
806
823
  "Avatar image is too large": "头像图片太大了",
807
824
  "A Bot needs a name": "Bot 需要一个名字",
@@ -2014,6 +2031,8 @@ button.bt-q-opt:active{background:var(--bt-active)}
2014
2031
  .bt-set-body{gap:20px}
2015
2032
  .bt-drawer-model{display:flex;flex-direction:column}
2016
2033
  .bt-drawer-model .bt-note{margin-top:6px}
2034
+ .bt-drawer-permission{display:flex;flex-direction:column}
2035
+ .bt-seg button.bt-perm-danger,.bt-seg button.bt-perm-danger[aria-pressed=true]{color:#e02135}
2017
2036
 
2018
2037
  /* Settings dialog: a 198px page list beside the page. */
2019
2038
  .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 +2132,8 @@ button.bt-set-row:hover{background:var(--bt-hover)}
2113
2132
  .bt-voice-status{font-size:13px;line-height:18px;color:var(--bt-ink-2);min-height:18px}
2114
2133
  .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
2134
  .bt-voice-note{font-size:12px;color:#e02135}
2135
+ .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}
2136
+ .bt-voice-setup:hover{background:var(--bt-active)}
2116
2137
  .bt-voice-bar{display:flex;gap:16px;padding:0 0 32px}
2117
2138
  .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
2139
  .bt-voice-btn:hover{background:var(--bt-active)}
@@ -3513,12 +3534,22 @@ var SECRET_CSS = `
3513
3534
  @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
3535
  `;
3515
3536
 
3537
+ // src/client/turn-reasoning.js
3538
+ function turnReasoning(steps, current) {
3539
+ const members = steps.length ? steps : [current];
3540
+ if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return "";
3541
+ return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n");
3542
+ }
3543
+ var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "");
3544
+ var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning");
3545
+ var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim();
3546
+
3516
3547
  // src/client/cells.js
3517
3548
  var import_react6 = require("react");
3518
3549
  var import_react_dom = require("react-dom");
3519
3550
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
3520
3551
  var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") });
3521
- function Bubbles({ texts, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead }) {
3552
+ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead }) {
3522
3553
  const onClick = (event) => {
3523
3554
  const target = mentionTarget(event);
3524
3555
  if (target === null) return;
@@ -3530,10 +3561,10 @@ function Bubbles({ texts, roster, selfId, actions, streaming, sessionId, msgKey,
3530
3561
  return (0, import_react6.createElement)(
3531
3562
  "div",
3532
3563
  { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick },
3533
- texts.map((text, index) => {
3564
+ texts2.map((text, index) => {
3534
3565
  const key = msgKey ? `${msgKey}:${index}` : void 0;
3535
3566
  const picked = key ? reactions?.[index] ?? [] : [];
3536
- const last = index === texts.length - 1;
3567
+ const last = index === texts2.length - 1;
3537
3568
  const live = streaming && last;
3538
3569
  return (0, import_react6.createElement)(
3539
3570
  "div",
@@ -3638,8 +3669,7 @@ function MessageTools({ text, name, sessionId, react, actions }) {
3638
3669
  ), document.body) : null
3639
3670
  );
3640
3671
  }
3641
- function ThinkingRow({ text, running }) {
3642
- if (running || !text) return null;
3672
+ function ThinkingRow({ text }) {
3643
3673
  return (0, import_react6.createElement)(
3644
3674
  "details",
3645
3675
  { className: "bt-think" },
@@ -3647,22 +3677,22 @@ function ThinkingRow({ text, running }) {
3647
3677
  (0, import_react6.createElement)("div", { className: "bt-think-text" }, text)
3648
3678
  );
3649
3679
  }
3650
- function AssistantCell({ node, groupPart, sessionId, useRoster, useUi, actions }) {
3680
+ function AssistantCell({ node, groupPart, sessionId, useRoster, useUi, useTurnSteps, actions }) {
3651
3681
  const roster = useRoster((value) => value);
3652
3682
  const data = node.data;
3683
+ const steps = useTurnSteps(String(data.turn));
3653
3684
  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);
3685
+ const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text);
3655
3686
  const running = data.status === "running";
3656
- useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts.length > 0 ? { text: true } : null);
3687
+ useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null);
3657
3688
  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;
3689
+ const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data);
3690
+ if (texts2.length === 0 && !reasoning) return null;
3661
3691
  return (0, import_react6.createElement)(
3662
3692
  import_react6.Fragment,
3663
3693
  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
3694
+ reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null,
3695
+ texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId: roster.byId[sessionId]?.id ?? sessionId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : null
3666
3696
  );
3667
3697
  }
3668
3698
  function EventLine({ prefix, bots, joiner = t("and"), onOpen }) {
@@ -4667,6 +4697,7 @@ function UpdateButton({ useUpdate, actions }) {
4667
4697
  const [open, setOpen] = (0, import_react8.useState)(false);
4668
4698
  const [box, setBox] = (0, import_react8.useState)(null);
4669
4699
  const [error, setError] = (0, import_react8.useState)("");
4700
+ const [dshError, setDshError] = (0, import_react8.useState)("");
4670
4701
  const button = (0, import_react8.useRef)(null);
4671
4702
  (0, import_react8.useEffect)(() => {
4672
4703
  if (!open) return void 0;
@@ -4689,7 +4720,11 @@ function UpdateButton({ useUpdate, actions }) {
4689
4720
  };
4690
4721
  }, [open]);
4691
4722
  const phase = update?.phase ?? "idle";
4692
- if (!update?.available && phase === "idle") return null;
4723
+ const dsh = update?.dsh;
4724
+ const dshPhase = dsh?.phase ?? "idle";
4725
+ const dshRow = dsh && dsh.verified && dsh.current && dsh.verified !== dsh.current;
4726
+ if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null;
4727
+ const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase;
4693
4728
  const toggle = () => {
4694
4729
  const rect = button.current?.getBoundingClientRect();
4695
4730
  if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 });
@@ -4699,8 +4734,14 @@ function UpdateButton({ useUpdate, actions }) {
4699
4734
  setError("");
4700
4735
  void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure))));
4701
4736
  };
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 });
4737
+ const startDsh = () => {
4738
+ setDshError("");
4739
+ void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure))));
4740
+ };
4741
+ 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
4742
  const message = error || (phase === "failed" ? update.error : "");
4743
+ const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : "");
4744
+ const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy && dsh.kind === "npm" ? t("Update DS Bot {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current });
4704
4745
  return (0, import_react8.createElement)(
4705
4746
  import_react8.Fragment,
4706
4747
  null,
@@ -4708,30 +4749,51 @@ function UpdateButton({ useUpdate, actions }) {
4708
4749
  ref: button,
4709
4750
  type: "button",
4710
4751
  className: "bt-round bt-update",
4711
- "data-phase": phase,
4752
+ "data-phase": cloudPhase,
4712
4753
  "aria-label": label,
4713
4754
  title: label,
4714
4755
  "aria-haspopup": "dialog",
4715
4756
  "aria-expanded": open,
4716
4757
  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),
4758
+ }, (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
4759
  open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)(
4719
4760
  "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)(
4761
+ { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box },
4762
+ update?.available || phase !== "idle" ? (0, import_react8.createElement)(
4763
+ import_react8.Fragment,
4764
+ null,
4765
+ (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })),
4766
+ (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 })),
4767
+ message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null,
4768
+ (0, import_react8.createElement)(
4769
+ "div",
4770
+ { className: "bt-update-actions" },
4771
+ update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null,
4772
+ phase === "installed" ? null : (0, import_react8.createElement)("button", {
4773
+ type: "button",
4774
+ className: "bt-update-go",
4775
+ disabled: phase === "installing",
4776
+ onClick: start
4777
+ }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update"))
4778
+ )
4779
+ ) : null,
4780
+ dshRow ? (0, import_react8.createElement)(
4725
4781
  "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
- )
4782
+ { className: "bt-update-dsh" },
4783
+ (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })),
4784
+ (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText),
4785
+ dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null,
4786
+ (0, import_react8.createElement)(
4787
+ "div",
4788
+ { className: "bt-update-actions" },
4789
+ dsh.kind === "app" ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => actions.openAppUpdates() }, t("Open updates")) : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", {
4790
+ type: "button",
4791
+ className: "bt-update-go",
4792
+ disabled: dshPhase === "installing",
4793
+ onClick: startDsh
4794
+ }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update"))
4795
+ )
4796
+ ) : null
4735
4797
  ), document.body) : null
4736
4798
  );
4737
4799
  }
@@ -5027,6 +5089,7 @@ var SIDEBAR_CSS = `
5027
5089
  .bt-update-go:hover:not(:disabled){opacity:.9}
5028
5090
  .bt-update-go:active:not(:disabled){transform:scale(.96)}
5029
5091
  .bt-update-go:disabled{opacity:.6;cursor:default}
5092
+ .bt-update-dsh{margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)}
5030
5093
  @media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}}
5031
5094
 
5032
5095
  /* The DSH Agent row and its expanding Session list. The black whale is the
@@ -5354,10 +5417,11 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) {
5354
5417
  }
5355
5418
  }, [quote?.token]);
5356
5419
  if (sessionId === void 0) return null;
5420
+ const pluginMic = actions.speech() !== void 0;
5357
5421
  return (0, import_react10.createElement)(
5358
5422
  import_react10.Fragment,
5359
5423
  null,
5360
- (0, import_react10.createElement)("button", {
5424
+ pluginMic ? null : (0, import_react10.createElement)("button", {
5361
5425
  type: "button",
5362
5426
  className: "bt-mic",
5363
5427
  "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"),
@@ -5449,6 +5513,132 @@ function ComposerPlus({ sessionId, useRoster, actions }) {
5449
5513
  );
5450
5514
  }
5451
5515
 
5516
+ // src/client/speech.js
5517
+ var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle";
5518
+ var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]);
5519
+ async function speechCatalog(speech) {
5520
+ const result = await speech.catalog();
5521
+ if (!result?.ok) throw new Error(result?.error?.message ?? "Speech catalog failed");
5522
+ const catalog = result.value;
5523
+ const provider = catalog.providers.find((item) => item.id === catalog.selection.providerId);
5524
+ return { catalog, usable: USABLE.has(provider?.preparation?.phase) };
5525
+ }
5526
+ function waveBytes(samples, rate = 16e3) {
5527
+ const bytes = new Uint8Array(44 + samples.length * 2);
5528
+ const view = new DataView(bytes.buffer);
5529
+ const ascii = (at, text) => {
5530
+ for (let index = 0; index < text.length; index += 1) bytes[at + index] = text.charCodeAt(index);
5531
+ };
5532
+ ascii(0, "RIFF");
5533
+ view.setUint32(4, 36 + samples.length * 2, true);
5534
+ ascii(8, "WAVEfmt ");
5535
+ view.setUint32(16, 16, true);
5536
+ view.setUint16(20, 1, true);
5537
+ view.setUint16(22, 1, true);
5538
+ view.setUint32(24, rate, true);
5539
+ view.setUint32(28, rate * 2, true);
5540
+ view.setUint16(32, 2, true);
5541
+ view.setUint16(34, 16, true);
5542
+ ascii(36, "data");
5543
+ view.setUint32(40, samples.length * 2, true);
5544
+ samples.forEach((sample, index) => {
5545
+ const value = Math.max(-1, Math.min(1, sample));
5546
+ view.setInt16(44 + index * 2, value < 0 ? Math.round(value * 32768) : Math.round(value * 32767), true);
5547
+ });
5548
+ return bytes;
5549
+ }
5550
+ function base64Of(bytes) {
5551
+ let text = "";
5552
+ for (let index = 0; index < bytes.length; index += 32768) text += String.fromCharCode(...bytes.subarray(index, index + 32768));
5553
+ return btoa(text);
5554
+ }
5555
+ var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause });
5556
+ async function openCapture() {
5557
+ const devices = navigator.mediaDevices;
5558
+ if (!devices?.getUserMedia || typeof MediaRecorder === "undefined") throw captureError("unavailable");
5559
+ let stream;
5560
+ try {
5561
+ stream = await devices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false });
5562
+ } catch (error) {
5563
+ throw captureError(error?.name === "NotAllowedError" ? "blocked" : "failed", error);
5564
+ }
5565
+ const audio = new AudioContext();
5566
+ const analyser = audio.createAnalyser();
5567
+ analyser.fftSize = 512;
5568
+ audio.createMediaStreamSource(stream).connect(analyser);
5569
+ const frame = new Float32Array(analyser.fftSize);
5570
+ const recorder = new MediaRecorder(stream);
5571
+ const chunks = [];
5572
+ recorder.ondataavailable = (event) => {
5573
+ if (event.data.size > 0) chunks.push(event.data);
5574
+ };
5575
+ recorder.start();
5576
+ let closed = false;
5577
+ const close = () => {
5578
+ if (closed) return;
5579
+ closed = true;
5580
+ if (recorder.state !== "inactive") recorder.stop();
5581
+ stream.getTracks().forEach((track) => track.stop());
5582
+ void audio.close().catch(() => {
5583
+ });
5584
+ };
5585
+ return {
5586
+ level() {
5587
+ if (closed) return 0;
5588
+ analyser.getFloatTimeDomainData(frame);
5589
+ let sum = 0;
5590
+ for (const sample of frame) sum += sample * sample;
5591
+ return Math.min(1, Math.sqrt(sum / frame.length) * 4);
5592
+ },
5593
+ cancel: close,
5594
+ async finish(maxSeconds) {
5595
+ if (closed || recorder.state === "inactive") {
5596
+ close();
5597
+ return null;
5598
+ }
5599
+ try {
5600
+ await new Promise((resolve) => {
5601
+ recorder.onstop = resolve;
5602
+ recorder.stop();
5603
+ });
5604
+ stream.getTracks().forEach((track) => track.stop());
5605
+ const blob = new Blob(chunks, { type: recorder.mimeType });
5606
+ if (blob.size === 0) return null;
5607
+ const decoded = await audio.decodeAudioData(await blob.arrayBuffer());
5608
+ const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3));
5609
+ const offline = new OfflineAudioContext(1, length, 16e3);
5610
+ const source = offline.createBufferSource();
5611
+ source.buffer = decoded;
5612
+ source.connect(offline.destination);
5613
+ source.start();
5614
+ return waveBytes((await offline.startRendering()).getChannelData(0));
5615
+ } finally {
5616
+ close();
5617
+ }
5618
+ }
5619
+ };
5620
+ }
5621
+ async function transcribeWave(speech, catalog, wave, signal) {
5622
+ if (wave.byteLength > catalog.maxAudioBytes) throw captureError("too-long");
5623
+ const result = await speech.transcribe({ audioBase64: base64Of(wave), ...catalog.selection }, signal);
5624
+ if (!result?.ok) throw Object.assign(new Error(result?.error?.message ?? "Transcription failed"), { kind: "service" });
5625
+ return String(result.value?.text ?? "").trim();
5626
+ }
5627
+ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) {
5628
+ let spokeAt = 0;
5629
+ let quietSince = 0;
5630
+ return (level, now) => {
5631
+ if (level >= loud) {
5632
+ spokeAt ||= now;
5633
+ quietSince = 0;
5634
+ return "speech";
5635
+ }
5636
+ if (!spokeAt) return "waiting";
5637
+ if (level < quiet) quietSince ||= now;
5638
+ return quietSince && now - quietSince >= quietMs ? "done" : "speech";
5639
+ };
5640
+ }
5641
+
5452
5642
  // src/client/overlay-hooks.js
5453
5643
  var import_react11 = require("react");
5454
5644
  var escapeLayers = [];
@@ -5680,10 +5870,19 @@ var MENU_EXIT_MS = 120;
5680
5870
  function groupModels(models) {
5681
5871
  const groups = /* @__PURE__ */ new Map();
5682
5872
  for (const model of models ?? []) {
5683
- if (!groups.has(model.providerName)) groups.set(model.providerName, []);
5684
- groups.get(model.providerName).push(model);
5873
+ const provider = model.provider ?? model.providerName ?? model.ref.split("/")[0];
5874
+ if (!groups.has(provider)) groups.set(provider, []);
5875
+ groups.get(provider).push(model);
5685
5876
  }
5686
- return [...groups];
5877
+ const names = /* @__PURE__ */ new Map();
5878
+ for (const [provider, list] of groups) {
5879
+ const name = list[0].providerName ?? provider;
5880
+ names.set(name, (names.get(name) ?? 0) + 1);
5881
+ }
5882
+ return [...groups].map(([provider, list]) => {
5883
+ const name = list[0].providerName ?? provider;
5884
+ return [names.get(name) > 1 ? `${name} (${provider})` : name, list];
5885
+ });
5687
5886
  }
5688
5887
  var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref);
5689
5888
  function modelOf(bot, roster) {
@@ -5693,10 +5892,57 @@ function modelOf(bot, roster) {
5693
5892
  const [provider, ...rest] = ref.split("/");
5694
5893
  return { ref, name: served?.name ?? rest.join("/"), provider: served?.providerName ?? provider, served: served !== void 0 };
5695
5894
  }
5895
+ var effortCache = /* @__PURE__ */ new Map();
5896
+ function useEfforts(served, ref, actions) {
5897
+ const [fetched, setFetched] = (0, import_react13.useState)(void 0);
5898
+ (0, import_react13.useEffect)(() => {
5899
+ setFetched(void 0);
5900
+ if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0;
5901
+ let live = true;
5902
+ if (effortCache.has(ref)) setFetched(effortCache.get(ref));
5903
+ else void Promise.resolve(actions.efforts(ref)).then((value) => {
5904
+ const reasoning = value?.efforts ? value : null;
5905
+ effortCache.set(ref, reasoning);
5906
+ if (live) setFetched(reasoning);
5907
+ }).catch(() => {
5908
+ });
5909
+ return () => {
5910
+ live = false;
5911
+ };
5912
+ }, [ref, served?.reasoning !== void 0, actions]);
5913
+ return served?.reasoning ?? fetched;
5914
+ }
5915
+ function effortRows(reasoning) {
5916
+ if (reasoning?.efforts === void 0 || reasoning.efforts.length === 0) return [];
5917
+ return [
5918
+ { effort: void 0, label: t("Default") },
5919
+ ...reasoning.efforts.map((effort) => ({ effort: effort.id, label: effort.name }))
5920
+ ];
5921
+ }
5922
+ function EffortRows({ reasoning, current, onPick }) {
5923
+ return (0, import_react13.createElement)(
5924
+ import_react13.Fragment,
5925
+ null,
5926
+ (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
5927
+ (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")),
5928
+ effortRows(reasoning).map((row) => (0, import_react13.createElement)(
5929
+ "button",
5930
+ {
5931
+ key: row.effort ?? "default",
5932
+ type: "button",
5933
+ role: "menuitemradio",
5934
+ "aria-checked": row.effort === current,
5935
+ onClick: () => onPick(row.effort)
5936
+ },
5937
+ (0, import_react13.createElement)("span", { className: "bt-menu-label" }, row.label),
5938
+ (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react13.createElement)(CheckIcon) : null)
5939
+ ))
5940
+ );
5941
+ }
5696
5942
  function ModelRows({ roster, current, onPick }) {
5697
5943
  return groupModels(roster.models).map(([provider, list]) => (0, import_react13.createElement)(
5698
5944
  import_react13.Fragment,
5699
- { key: provider },
5945
+ { key: list[0].provider ?? provider },
5700
5946
  (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider),
5701
5947
  list.map((model) => (0, import_react13.createElement)(
5702
5948
  "button",
@@ -5714,13 +5960,18 @@ function ModelRows({ roster, current, onPick }) {
5714
5960
  ))
5715
5961
  ));
5716
5962
  }
5717
- function ModelMenu({ bot, roster, actions, run, wide = false, onPick }) {
5963
+ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) {
5718
5964
  const [place, setPlace] = (0, import_react13.useState)(null);
5719
5965
  const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS);
5720
5966
  const button = (0, import_react13.useRef)(null);
5721
5967
  const menu = (0, import_react13.useRef)(null);
5722
5968
  const shown = modelOf(bot, roster);
5723
5969
  const current = bot.model ?? bot.appliedModel;
5970
+ const served = servedOf(roster, current);
5971
+ const reasoning = useEfforts(served, current, actions);
5972
+ const picked = reasoning?.efforts?.some((effort) => effort.id === bot.effort) ? bot.effort : void 0;
5973
+ const currentEffort = picked ?? reasoning?.defaultEffort;
5974
+ const effortLabel = currentEffort === void 0 ? reasoning?.efforts?.length > 0 ? t("Default") : void 0 : reasoning.efforts.find((effort) => effort.id === currentEffort)?.name ?? currentEffort;
5724
5975
  const close = () => setPlace(null);
5725
5976
  (0, import_react13.useEffect)(() => {
5726
5977
  if (place === null) return void 0;
@@ -5764,6 +6015,11 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick }) {
5764
6015
  if (onPick) onPick(ref);
5765
6016
  else if (ref !== current || !bot.model) void run(actions.updateBot(bot.id, { model: ref }));
5766
6017
  };
6018
+ const pickEffort = (effort) => {
6019
+ close();
6020
+ if (onPickEffort) onPickEffort(effort);
6021
+ else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null }));
6022
+ };
5767
6023
  const models = roster.models ?? [];
5768
6024
  return (0, import_react13.createElement)(
5769
6025
  import_react13.Fragment,
@@ -5785,7 +6041,7 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick }) {
5785
6041
  "span",
5786
6042
  { className: "bt-model-copy" },
5787
6043
  (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
6044
+ 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
6045
  ),
5790
6046
  (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
6047
  ),
@@ -5793,6 +6049,7 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick }) {
5793
6049
  "div",
5794
6050
  { 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
6051
  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.")),
6052
+ effortRows(reasoning).length > 0 ? (0, import_react13.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
5796
6053
  (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }),
5797
6054
  (0, import_react13.createElement)("button", { type: "button", role: "menuitem", onClick: () => {
5798
6055
  close();
@@ -5824,15 +6081,21 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) {
5824
6081
  };
5825
6082
  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
6083
  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)),
6084
+ ...groupModels(models).map(([provider, list]) => (0, import_react13.createElement)(
6085
+ "div",
6086
+ { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider },
6087
+ (0, import_react13.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider),
6088
+ (0, import_react13.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react13.createElement)("button", {
6089
+ key: model.ref,
6090
+ type: "button",
6091
+ className: "bt-model-chip",
6092
+ disabled: busy !== "",
6093
+ "aria-busy": busy === model.ref || void 0,
6094
+ "aria-label": `${model.name} · ${provider}`,
6095
+ title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref,
6096
+ onClick: () => pick2(model.ref)
6097
+ }, model.name, busy === model.ref ? (0, import_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null)))
6098
+ )),
5836
6099
  (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
6100
  ];
5838
6101
  return (0, import_react13.createElement)(
@@ -5905,6 +6168,9 @@ var MODEL_CSS = `
5905
6168
  .bt-model-none{padding:8px;font-size:13px;line-height:18px;color:var(--bt-ink-2)}
5906
6169
  .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
6170
  .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}
6171
+ .bt-model-choice-group{display:flex;flex-direction:column;gap:6px;width:100%;min-width:0}
6172
+ .bt-model-choice-provider{padding-top:4px;font-size:12px;line-height:16px;font-weight:500;color:var(--bt-ink-2);overflow-wrap:anywhere}
6173
+ .bt-model-choice-chips{display:flex;flex-wrap:wrap;gap:6px}
5908
6174
  .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
6175
  .bt-model-chip:hover:not(:disabled){background:var(--bt-hover)}
5910
6176
  .bt-model-chip:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px}
@@ -7031,9 +7297,10 @@ function startModel(roster) {
7031
7297
  const models = roster.models ?? [];
7032
7298
  return (models.find((model) => model.ref === roster.defaultModel) ?? models[0])?.ref ?? "";
7033
7299
  }
7034
- function createPayload({ name, look, image, model, brief }) {
7300
+ function createPayload({ name, look, image, model, effort, brief }) {
7035
7301
  const payload = { name, color: look.color, avatar: image ? { shape: look.shape, image } : { shape: look.shape } };
7036
7302
  if (model) payload.model = model;
7303
+ if (effort) payload.effort = effort;
7037
7304
  if (brief) payload.brief = brief;
7038
7305
  return payload;
7039
7306
  }
@@ -7051,6 +7318,7 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
7051
7318
  const [image, setImage] = (0, import_react17.useState)(null);
7052
7319
  const [name, setName] = (0, import_react17.useState)(start?.name ?? "");
7053
7320
  const [model, setModel] = (0, import_react17.useState)(() => startModel(roster));
7321
+ const [effort, setEffort] = (0, import_react17.useState)(void 0);
7054
7322
  const [busy, setBusy] = (0, import_react17.useState)(false);
7055
7323
  const [error, setError] = (0, import_react17.useState)("");
7056
7324
  const fileRef = (0, import_react17.useRef)(null);
@@ -7067,7 +7335,7 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
7067
7335
  setError("");
7068
7336
  setBusy(true);
7069
7337
  try {
7070
- const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, brief }));
7338
+ const bot = await actions.createBot(createPayload({ name: finalName, look, image, model, effort, brief }));
7071
7339
  onCreated(bot);
7072
7340
  } catch (failure) {
7073
7341
  setError(failure?.message ?? String(failure));
@@ -7151,7 +7419,10 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr
7151
7419
  "div",
7152
7420
  { className: "bt-field" },
7153
7421
  (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 }),
7422
+ (0, import_react17.createElement)(ModelMenu, { bot: { id: "new-bot", name: finalName, model: model || void 0, effort }, roster, actions, wide: true, onPick: (ref) => {
7423
+ setModel(ref);
7424
+ setEffort(void 0);
7425
+ }, onPickEffort: setEffort }),
7155
7426
  (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
7427
  ),
7157
7428
  error ? (0, import_react17.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, error) : null,
@@ -7513,7 +7784,15 @@ function ExchangeDialog({ pair, roster, actions }) {
7513
7784
  // src/client/voice.js
7514
7785
  var import_react20 = require("react");
7515
7786
  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" };
7787
+ var VOICE_LABELS = {
7788
+ listening: "Listening",
7789
+ thinking: "Thinking",
7790
+ speaking: "Speaking",
7791
+ paused: "Paused",
7792
+ setup: "Voice input needs its speech model",
7793
+ unsupported: "Voice chat is not available in this browser"
7794
+ };
7795
+ 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
7796
  function useMicLevel(enabled) {
7518
7797
  const [level, setLevel] = (0, import_react20.useState)(0);
7519
7798
  (0, import_react20.useEffect)(() => {
@@ -7566,27 +7845,59 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
7566
7845
  const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0;
7567
7846
  return entry ? `${outline.length}|${entry.response ?? ""}` : "0|";
7568
7847
  });
7569
- const [phase, setPhase] = (0, import_react20.useState)(speechApi() ? "listening" : "unsupported");
7848
+ const [speech] = (0, import_react20.useState)(() => actions.speech());
7849
+ const [phase, setPhase] = (0, import_react20.useState)(speech ? "starting" : speechApi() ? "listening" : "unsupported");
7850
+ const [catalog, setCatalog] = (0, import_react20.useState)(null);
7570
7851
  const [heard, setHeard] = (0, import_react20.useState)("");
7571
7852
  const [said, setSaid] = (0, import_react20.useState)("");
7853
+ const [note, setNote] = (0, import_react20.useState)("");
7854
+ const [serviceLevel, setServiceLevel] = (0, import_react20.useState)(0);
7855
+ const [cycle, setCycle] = (0, import_react20.useState)(0);
7572
7856
  const waiting = (0, import_react20.useRef)(null);
7857
+ const reply = (0, import_react20.useRef)(lastReply);
7858
+ reply.current = lastReply;
7859
+ const turn = (0, import_react20.useRef)(0);
7573
7860
  const paneLeft = usePaneLeft();
7574
7861
  const close = () => {
7862
+ turn.current += 1;
7575
7863
  window.speechSynthesis?.cancel();
7576
7864
  actions.closeVoice();
7577
7865
  };
7578
7866
  useEscape(close);
7867
+ const submit = (text) => {
7868
+ setHeard(text);
7869
+ setPhase("thinking");
7870
+ waiting.current = reply.current.split("|")[0];
7871
+ void actions.send(sessionId, text.trim()).catch(() => setPhase("listening"));
7872
+ };
7579
7873
  const dictation = useDictation((text, final) => {
7580
7874
  setHeard(text);
7581
7875
  if (!final) return;
7582
7876
  dictation.stop();
7583
- setPhase("thinking");
7584
- waiting.current = lastReply.split("|")[0];
7585
- void actions.send(sessionId, text.trim()).catch(() => setPhase("listening"));
7877
+ submit(text);
7586
7878
  });
7879
+ (0, import_react20.useEffect)(() => {
7880
+ if (!note) return void 0;
7881
+ const timer = setTimeout(() => setNote(""), 2600);
7882
+ return () => clearTimeout(timer);
7883
+ }, [note]);
7884
+ (0, import_react20.useEffect)(() => {
7885
+ if (!speech) return void 0;
7886
+ let live = true;
7887
+ speechCatalog(speech).then(({ catalog: next, usable }) => {
7888
+ if (!live) return;
7889
+ setCatalog(next);
7890
+ setPhase(usable ? "listening" : "setup");
7891
+ }, () => {
7892
+ if (live) setPhase("setup");
7893
+ });
7894
+ return () => {
7895
+ live = false;
7896
+ };
7897
+ }, [speech]);
7587
7898
  const listen = phase === "listening";
7588
7899
  (0, import_react20.useEffect)(() => {
7589
- if (!listen || dictation.listening) return void 0;
7900
+ if (speech || !listen || dictation.listening) return void 0;
7590
7901
  const timer = setTimeout(() => dictation.start({ continuous: false, onEnd: () => {
7591
7902
  } }), 250);
7592
7903
  return () => clearTimeout(timer);
@@ -7594,20 +7905,82 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
7594
7905
  (0, import_react20.useEffect)(() => {
7595
7906
  if (phase !== "listening") dictation.stop();
7596
7907
  }, [phase]);
7908
+ (0, import_react20.useEffect)(() => {
7909
+ if (!speech || !catalog || !listen) return void 0;
7910
+ const own = turn.current;
7911
+ let capture = null;
7912
+ let frame = 0;
7913
+ let stopped = false;
7914
+ let smooth = 0;
7915
+ const detect = turnDetector();
7916
+ const limit = Math.max(1, catalog.maxDurationSeconds - 1) * 1e3;
7917
+ const finish = async (current) => {
7918
+ setPhase("transcribing");
7919
+ try {
7920
+ const wave = await current.finish(catalog.maxDurationSeconds);
7921
+ const text = wave ? await transcribeWave(speech, catalog, wave, new AbortController().signal) : "";
7922
+ if (turn.current !== own) return;
7923
+ if (text) submit(text);
7924
+ else setPhase("listening");
7925
+ } catch (error) {
7926
+ if (turn.current !== own) return;
7927
+ setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that"));
7928
+ setPhase("listening");
7929
+ }
7930
+ };
7931
+ openCapture().then((opened) => {
7932
+ if (stopped) {
7933
+ opened.cancel();
7934
+ return;
7935
+ }
7936
+ capture = opened;
7937
+ const began = performance.now();
7938
+ const tick = () => {
7939
+ const now = performance.now();
7940
+ const level2 = capture.level();
7941
+ smooth = smooth * 0.8 + level2 * 0.2;
7942
+ setServiceLevel(Math.round(smooth * 100) / 100);
7943
+ const state = detect(level2, now);
7944
+ if (state === "done" || state === "speech" && now - began >= limit) {
7945
+ const current = capture;
7946
+ capture = null;
7947
+ setServiceLevel(0);
7948
+ void finish(current);
7949
+ return;
7950
+ }
7951
+ if (state === "waiting" && now - began >= limit) {
7952
+ setCycle((value) => value + 1);
7953
+ return;
7954
+ }
7955
+ frame = requestAnimationFrame(tick);
7956
+ };
7957
+ tick();
7958
+ }, (error) => {
7959
+ if (stopped) return;
7960
+ setNote(t(CAPTURE_NOTES[error?.kind] ?? "Couldn't hear that"));
7961
+ setPhase("paused");
7962
+ });
7963
+ return () => {
7964
+ stopped = true;
7965
+ cancelAnimationFrame(frame);
7966
+ capture?.cancel();
7967
+ setServiceLevel(0);
7968
+ };
7969
+ }, [speech, catalog, listen, cycle]);
7597
7970
  (0, import_react20.useEffect)(() => {
7598
7971
  if (phase !== "thinking" || running || waiting.current === null) return;
7599
7972
  const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)];
7600
7973
  if (count === waiting.current || !text.trim()) return;
7601
7974
  waiting.current = null;
7602
- const speech = plainSpeech(text);
7603
- setSaid(speech);
7975
+ const spoken = plainSpeech(text);
7976
+ setSaid(spoken);
7604
7977
  setHeard("");
7605
- if (!window.speechSynthesis || !speech) {
7978
+ if (!window.speechSynthesis || !spoken) {
7606
7979
  setPhase("listening");
7607
7980
  return;
7608
7981
  }
7609
- const utterance = new SpeechSynthesisUtterance(speech);
7610
- utterance.lang = /[\u3400-\u9fff]/.test(speech) ? "zh-CN" : "en-US";
7982
+ const utterance = new SpeechSynthesisUtterance(spoken);
7983
+ utterance.lang = /[\u3400-\u9fff]/.test(spoken) ? "zh-CN" : "en-US";
7611
7984
  utterance.onend = () => setPhase((current) => current === "speaking" ? "listening" : current);
7612
7985
  utterance.onerror = utterance.onend;
7613
7986
  setPhase("speaking");
@@ -7615,23 +7988,28 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
7615
7988
  window.speechSynthesis.speak(utterance);
7616
7989
  }, [phase, running, lastReply]);
7617
7990
  (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";
7991
+ const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening);
7992
+ const level = speech ? serviceLevel : micLevel;
7993
+ const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle";
7994
+ const blocked = phase === "unsupported" || phase === "setup" || phase === "starting";
7620
7995
  const togglePause = () => {
7621
7996
  if (phase === "paused") {
7622
7997
  setPhase("listening");
7623
7998
  return;
7624
7999
  }
8000
+ turn.current += 1;
7625
8001
  window.speechSynthesis?.cancel();
7626
8002
  setPhase("paused");
7627
8003
  };
8004
+ const openSetup = phase === "setup" ? actions.voiceSetup() : null;
7628
8005
  const caption = phase === "speaking" ? said : heard;
8006
+ const shownNote = note || dictation.note;
7629
8007
  return (0, import_react20.createElement)(
7630
8008
  "div",
7631
8009
  { className: "bt-pane bt-voice-pane", style: { left: paneLeft } },
7632
8010
  (0, import_react20.createElement)(
7633
8011
  "div",
7634
- { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase },
8012
+ { className: "bt-voice-stage", role: "dialog", "aria-label": t("Voice chat with {name}", { name }), "data-phase": phase === "transcribing" ? "thinking" : phase },
7635
8013
  (0, import_react20.createElement)(
7636
8014
  "div",
7637
8015
  { className: "bt-voice-orb", style: { "--bt-level": level } },
@@ -7643,10 +8021,14 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
7643
8021
  (0, import_react20.createElement)(
7644
8022
  "div",
7645
8023
  { className: "bt-voice-status", role: "status" },
7646
- phase === "thinking" ? (0, import_react20.createElement)(Shimmer, null, t("Thinking…")) : VOICE_LABELS[phase] ? t(VOICE_LABELS[phase]) : ""
8024
+ 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
8025
  ),
7648
8026
  (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
8027
+ openSetup ? (0, import_react20.createElement)("button", { type: "button", className: "bt-voice-setup", onClick: () => {
8028
+ close();
8029
+ openSetup();
8030
+ } }, t("Set up voice input")) : null,
8031
+ shownNote ? (0, import_react20.createElement)("div", { className: "bt-voice-note" }, shownNote) : null
7650
8032
  ),
7651
8033
  (0, import_react20.createElement)(
7652
8034
  "div",
@@ -7655,7 +8037,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus }
7655
8037
  type: "button",
7656
8038
  className: "bt-voice-btn",
7657
8039
  "aria-pressed": phase === "paused",
7658
- disabled: phase === "unsupported",
8040
+ disabled: blocked,
7659
8041
  "aria-label": phase === "paused" ? t("Resume listening") : t("Pause"),
7660
8042
  onClick: togglePause
7661
8043
  }, (0, import_react20.createElement)(MicIcon, { size: 20 }), phase === "paused" ? (0, import_react20.createElement)("span", { className: "bt-voice-slash", "aria-hidden": true }) : null),
@@ -9611,6 +9993,40 @@ var hostOf = (url) => {
9611
9993
  };
9612
9994
  var shortUrl = (url) => String(url ?? "").replace(/^https?:\/\//i, "").replace(/^www\./i, "").replace(/\/$/, "");
9613
9995
  var EMPTY_TABS = { ids: [], active: null };
9996
+ var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" };
9997
+ var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]);
9998
+ function PermissionRow({ bot, roster, actions, run }) {
9999
+ const [pending, setPending] = (0, import_react27.useState)(null);
10000
+ const catalog = roster.permissions;
10001
+ const saved = bot.permission ?? catalog?.defaultPreset;
10002
+ (0, import_react27.useEffect)(() => {
10003
+ if (pending === saved) setPending(null);
10004
+ }, [pending, saved]);
10005
+ if (!catalog?.options?.length) return null;
10006
+ const current = pending ?? saved;
10007
+ const pick2 = (value) => {
10008
+ if (value === current && bot.permission !== void 0) return;
10009
+ setPending(value);
10010
+ void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null));
10011
+ };
10012
+ return (0, import_react27.createElement)(
10013
+ "div",
10014
+ { className: "bt-drawer-permission" },
10015
+ (0, import_react27.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
10016
+ (0, import_react27.createElement)(
10017
+ Segmented,
10018
+ { label: t("Permissions"), value: current },
10019
+ catalog.options.map((option) => (0, import_react27.createElement)("button", {
10020
+ key: option.value,
10021
+ type: "button",
10022
+ "aria-pressed": option.value === current,
10023
+ title: option.description,
10024
+ onClick: () => pick2(option.value),
10025
+ className: DANGER.has(option.value) ? "bt-perm-danger" : void 0
10026
+ }, PERMISSION_LABELS[option.value] ? t(PERMISSION_LABELS[option.value]) : option.name))
10027
+ )
10028
+ );
10029
+ }
9614
10030
  function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) {
9615
10031
  const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS);
9616
10032
  const pages = useBrowserPages((map) => map);
@@ -9842,6 +10258,7 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
9842
10258
  (0, import_react27.createElement)(ModelMenu, { bot, roster, actions, run, wide: true }),
9843
10259
  modelOf(bot, roster) ? null : (0, import_react27.createElement)("div", { className: "bt-note" }, t("Pick a model and {name} can start.", { name: bot.name }))
9844
10260
  ),
10261
+ (0, import_react27.createElement)(PermissionRow, { key: "permission", bot, roster, actions, run }),
9845
10262
  bot.instructions ? (0, import_react27.createElement)(About, { key: "about", text: bot.instructions }) : (0, import_react27.createElement)("button", { key: "about", type: "button", className: "bt-more", onClick: () => setView("settings") }, t("Add instructions in Bot settings")),
9846
10263
  roster.memory === false ? null : (0, import_react27.createElement)(MemoryRow, { key: "memory", bot, actions }),
9847
10264
  usage ? (0, import_react27.createElement)(
@@ -11362,7 +11779,7 @@ function writeMode(mode, storage = globalThis.localStorage) {
11362
11779
  }
11363
11780
 
11364
11781
  // src/client/index.js
11365
- var inject = ["sessions", "uiWorkspace", "slots", "theme", "layout", "configForms"];
11782
+ var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout", "configForms"];
11366
11783
  function apply(ctx) {
11367
11784
  ctx.effect(() => installLocale(ctx.get("locale")), "ds-bot: interface copy");
11368
11785
  const roster = createSource(EMPTY_ROSTER);
@@ -11533,7 +11950,7 @@ function apply(ctx) {
11533
11950
  const value = await call("update-check", { fresh: false }).catch(() => null);
11534
11951
  if (stopped) return;
11535
11952
  if (value) update.set(value);
11536
- timer = setTimeout(loop, value?.phase === "installing" ? 2e3 : 60 * 6e4);
11953
+ timer = setTimeout(loop, value?.phase === "installing" || value?.dsh?.phase === "installing" ? 2e3 : 60 * 6e4);
11537
11954
  };
11538
11955
  wakeUpdate = loop;
11539
11956
  void loop();
@@ -11906,6 +12323,7 @@ function apply(ctx) {
11906
12323
  updateBot: (id, patch) => after(call("update-bot", { id, ...patch })),
11907
12324
  refreshModels: () => after(call("models")).catch(() => {
11908
12325
  }),
12326
+ efforts: (ref) => call("efforts", { ref }),
11909
12327
  deleteBot: (id) => after(call("delete-bot", { id })),
11910
12328
  duplicateBot: (id) => after(call("duplicate-bot", { id })),
11911
12329
  setPrefs: (prefs) => after(call("set-prefs", prefs)),
@@ -11992,11 +12410,23 @@ function apply(ctx) {
11992
12410
  actions.signIn = () => ui.set((state) => ({ ...state, signIn: (state.signIn ?? 0) + 1 }));
11993
12411
  actions.closeSignIn = () => ui.set((state) => ({ ...state, signIn: null }));
11994
12412
  actions.accountT = () => ctx.get("locale").bind("settings.account");
12413
+ actions.speech = () => ctx.get("remote.speech") ?? void 0;
12414
+ actions.voiceSetup = () => {
12415
+ const navigation = ctx.get("pluginNavigation");
12416
+ return navigation ? () => navigation.openBundle(VOICE_BUNDLE) : null;
12417
+ };
11995
12418
  actions.seatAccount = (on) => ui.set((state) => state.accountSeated === on ? state : { ...state, accountSeated: on });
11996
12419
  actions.installUpdate = async () => {
11997
- update.set(await call("update-install"));
12420
+ const value = await call("update-install");
12421
+ update.set((previous) => ({ ...previous, ...value }));
12422
+ wakeUpdate();
12423
+ };
12424
+ actions.installDshUpdate = async () => {
12425
+ const dsh = await call("dsh-update-install");
12426
+ update.set((previous) => ({ ...previous, dsh }));
11998
12427
  wakeUpdate();
11999
12428
  };
12429
+ actions.openAppUpdates = () => window.dshDesktop?.updates?.open?.();
12000
12430
  actions.noteTurnTime = (sessionId, turn, key, time) => {
12001
12431
  if (sessionId === void 0 || !Number.isFinite(turn) || !Number.isFinite(time)) return;
12002
12432
  turnClock.set((state) => {
@@ -12037,6 +12467,18 @@ function apply(ctx) {
12037
12467
  const browserPages = browsers?.pages ?? createSource({});
12038
12468
  const browserTabs = browsers?.tabs ?? createSource({});
12039
12469
  const face = () => ({ actions, hooks: { roster, ui, activity, memoryNews, registry, turnClock, live, browserPages, browserTabs, surface, update } });
12470
+ const assistantFace = (sessionId) => {
12471
+ const binding = ctx.sessions.binding(sessionId);
12472
+ if (binding === void 0) throw new Error(`ds-bot: unknown session "${sessionId}"`);
12473
+ const chat = ctx.uiConversation.binding(binding).target("chat");
12474
+ return { ...face(), keyedHooks: {
12475
+ turnSteps: (turn) => {
12476
+ const snapshot = chat.getSnapshot();
12477
+ if (snapshot === void 0) throw new Error("ds-bot: Chat target is unavailable");
12478
+ return snapshot.nodes.turnDataSource(Number(turn), "assistant-step");
12479
+ }
12480
+ } };
12481
+ };
12040
12482
  const shadow = (name, options, component) => ctx.slots.inject(name, () => ctx.slots.register({ name, ...options }, component));
12041
12483
  const installShell = () => [
12042
12484
  installCompiledStyles(SHELL_CSS, "shell"),
@@ -12071,7 +12513,7 @@ function apply(ctx) {
12071
12513
  // the mode is held at `verbose` while this group is mounted.
12072
12514
  holdFlatTranscript(ctx.configForms.get("ui-chat")),
12073
12515
  shadow("conversation.session.header", { priority: -10, inject: face }, HeaderPill),
12074
- shadow("conversation.chat.node", { key: "assistant-step", priority: -10, locale: "chat", inject: face }, AssistantCell),
12516
+ shadow("conversation.chat.node", { key: "assistant-step", priority: -10, locale: "chat", inject: assistantFace }, AssistantCell),
12075
12517
  shadow("conversation.chat.node", { key: "turn-trigger", priority: -10, locale: "chat", inject: face }, TriggerCell),
12076
12518
  shadow("conversation.chat.node", { key: "tool-call", priority: -10, locale: "chat", inject: face }, ToolCell),
12077
12519
  shadow("conversation.chat.node", { key: "turn-process", priority: -10, locale: "chat", inject: face }, TurnProcessCell),