dsh-codex-subscription 3.0.0-beta.8 → 3.0.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/lib/client.js CHANGED
@@ -1090,6 +1090,11 @@ window.__ModuleLoader__.load({
1090
1090
  "runtime/cancel",
1091
1091
  "storage/status",
1092
1092
  "storage/clear-forecast",
1093
+ "voice/start",
1094
+ "voice/stop",
1095
+ "voice/status",
1096
+ "voice/preview",
1097
+ "voice/catalog",
1093
1098
  "remote/status",
1094
1099
  "remote/enable",
1095
1100
  "remote/disable",
@@ -1326,10 +1331,6 @@ window.__ModuleLoader__.load({
1326
1331
  className: "codexImageToolTitle",
1327
1332
  children: t("imageGenerate")
1328
1333
  }),
1329
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1330
- className: "codexImageBeta",
1331
- children: t("imageBeta")
1332
- }),
1333
1334
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1334
1335
  className: "codexImageToolState",
1335
1336
  children: status
@@ -2676,7 +2677,7 @@ window.__ModuleLoader__.load({
2676
2677
  }
2677
2678
  return matrix;
2678
2679
  }
2679
- function parseColor(value) {
2680
+ function parseColor$1(value) {
2680
2681
  const text = String(value ?? "").trim().toLowerCase();
2681
2682
  if (!text || text === "none") return "none";
2682
2683
  if (text === "currentcolor") return "#000000";
@@ -3038,7 +3039,7 @@ window.__ModuleLoader__.load({
3038
3039
  if (!target || !/Gradient$/.test(target.name)) throw Error(`SVG paint ${reference} does not reference a gradient defined in the same SVG`);
3039
3040
  const stops = target.children.filter((child) => child.name === "stop");
3040
3041
  if (!stops.length) throw Error(`Gradient ${target.attrs.id} has no stops`);
3041
- const channels = stops.map((stop) => parseColor(stop.attrs["stop-color"] ?? /stop-color\s*:\s*([^;]+)/.exec(stop.attrs.style ?? "")?.[1] ?? "#000000")).filter((color) => color !== "none").map((color) => [
3042
+ const channels = stops.map((stop) => parseColor$1(stop.attrs["stop-color"] ?? /stop-color\s*:\s*([^;]+)/.exec(stop.attrs.style ?? "")?.[1] ?? "#000000")).filter((color) => color !== "none").map((color) => [
3042
3043
  1,
3043
3044
  3,
3044
3045
  5
@@ -3051,7 +3052,7 @@ window.__ModuleLoader__.load({
3051
3052
  warnings.add(`Gradient ${target.attrs.id} was flattened to its average color; stack several bands for a real gradient.`);
3052
3053
  return `#${mean.map((value) => value.toString(16).padStart(2, "0")).join("")}`;
3053
3054
  };
3054
- const paint = (value) => String(value).startsWith("url(") ? gradientColor(value) : parseColor(value);
3055
+ const paint = (value) => String(value).startsWith("url(") ? gradientColor(value) : parseColor$1(value);
3055
3056
  const emit = (subpath, matrix, style, id) => {
3056
3057
  [subpath.start, ...subpath.curves.flatMap((curve) => [
3057
3058
  [curve[0], curve[1]],
@@ -5684,10 +5685,17 @@ window.__ModuleLoader__.load({
5684
5685
  inputImageDetail_high: "高",
5685
5686
  inputImageDetail_original: "原图",
5686
5687
  connectors: "ChatGPT 连接器",
5687
- connectorsHint: "让 DSH 里的任何模型使用你在 ChatGPT 网页里已连接的应用(GitHub、Gmail、Google 云端硬盘、日历等)。只读:模型只能读取;允许写入:发邮件、改文件等写操作每次都要你批准。应用返回的内容来自第三方,模型不会照其中的指令行事。需先在 ChatGPT 里连接应用。",
5688
+ connectorsHint: "让 DSH 里的任何模型使用你在 ChatGPT 里已连接的应用(GitHub、Gmail、云端硬盘、日历等)。需先在 ChatGPT 里连接应用;应用返回的内容来自第三方,模型不会照其中的指令行事。",
5689
+ connectorsMode_off: "连接器关闭,模型看不到这些应用。",
5690
+ connectorsMode_read: "模型只能读取(搜邮件、读文件、查 PR),不会修改任何数据。",
5691
+ connectorsMode_ask: "发邮件、改文件等写操作每次都会弹出确认,你批准后才执行。",
5692
+ connectorsMode_write: "写操作直接执行,不再弹出确认,误操作无法撤回,请谨慎使用。",
5693
+ hintMore: "展开",
5694
+ hintLess: "收起",
5688
5695
  connectors_off: "关闭",
5689
5696
  connectors_read: "只读",
5690
- connectors_ask: "允许写入(每次审批)",
5697
+ connectors_ask: "询问后写入",
5698
+ connectors_write: "直接写入",
5691
5699
  reasoningSummary: "推理摘要",
5692
5700
  reasoningSummaryHint: "让模型在会话的“思考”部分给出推理摘要。自动由模型决定;简洁更短;详细更完整;关闭则不要摘要。",
5693
5701
  retryPolicy: "请求重试",
@@ -5705,12 +5713,54 @@ window.__ModuleLoader__.load({
5705
5713
  subagentBackend_dsh: "DSH",
5706
5714
  subagentBackend_codex: "Codex",
5707
5715
  connectionTitle: "连接方式",
5716
+ diagnosticsLog: "运行日志",
5717
+ diagnosticsLogHint: "按时间记录运行中发生的事,错误和警告在最上面的筛选里。内容已去掉令牌、邮箱和路径。",
5718
+ logLevel_all: "全部",
5719
+ logLevel_warn: "警告和错误",
5720
+ logLevel_error: "仅错误",
5721
+ logEmpty: "这个级别暂时没有记录。",
5722
+ logCopy: "复制日志",
5723
+ logCopied: "已复制日志",
5724
+ logEarlier: "上次运行",
5708
5725
  remoteTitle: "手机远程控制",
5709
5726
  remote_off: "关闭",
5710
5727
  remote_on: "开启",
5728
+ voiceStart: "语音对话",
5729
+ voiceEnd: "结束语音",
5730
+ voiceConnecting: "正在连接语音…",
5731
+ voiceListening: "在听",
5732
+ voiceSpeaking: "在说",
5733
+ voiceWorking: "DSH 正在处理…",
5734
+ voiceMute: "关闭麦克风",
5735
+ voiceUnmute: "打开麦克风",
5736
+ voiceMuteSpeaker: "关闭声音",
5737
+ voiceUnmuteSpeaker: "打开声音",
5738
+ voiceMicBlocked: "没有麦克风权限,请在浏览器或系统里允许后再试。",
5739
+ voiceFailed: "语音没有接通,请稍后再试。",
5740
+ voiceTitle: "语音对话",
5741
+ voiceHint: "输入框里没有内容时,发送键变成语音键:点一下就能和 DSH 说话,让它做事,它的回答会念出来。语音通话由 ChatGPT 订阅提供,通话内容会经过 OpenAI。",
5742
+ voiceVoice: "音色",
5743
+ voiceLanguage: "回答语言",
5744
+ voiceAuto: "跟你说的语言一致",
5745
+ voicePlay: "试听",
5746
+ voiceLoading: "加载中…",
5747
+ voiceNoSample: "这个音色暂时没有试听样本。",
5748
+ voiceUnavailable: "暂无试听",
5749
+ voiceMore: "更多音色(暂无试听)",
5750
+ voiceLang_zh: "中文",
5751
+ voiceLang_en: "English",
5752
+ voiceLang_ja: "日本語",
5753
+ voiceLang_ko: "한국어",
5754
+ voiceLang_es: "Español",
5755
+ voiceLang_fr: "Français",
5756
+ voiceLang_de: "Deutsch",
5757
+ voiceLang_pt: "Português",
5758
+ voiceLang_ru: "Русский",
5711
5759
  remoteHint: "实验功能,默认关闭。开启后,这台电脑上的 DSH 会出现在 ChatGPT 手机 App 的 Codex 主机列表里,可用手机查看对话、发消息、切换模型和审批。需要订阅账号登录;电脑要保持开机和联网。",
5712
- remoteConnected: "已连接",
5760
+ remoteConnected: "电脑已连上 ChatGPT,还没有手机连接",
5713
5761
  remoteConnecting: "正在连接",
5762
+ remoteWaiting: "正在等待上一次连接释放,通常几分钟内自动连上",
5763
+ remoteConflictLong: "长时间连不上,通常是另一台电脑、另一份 DSH(或另一个窗口)也在用同一个 ChatGPT 账号开着手机远程,彼此在抢连接。请关闭其他地方的手机远程,再回到这里点“关闭”后重新“开启”。",
5714
5764
  remoteError: "连接失败,会自动重试",
5715
5765
  remoteStopped: "已停止",
5716
5766
  remotePaired: "已配对的手机",
@@ -5718,7 +5768,7 @@ window.__ModuleLoader__.load({
5718
5768
  remoteRevoke: "移除",
5719
5769
  remoteLastSeen: "最近 {date}",
5720
5770
  remotePhoneUnknown: "未命名手机",
5721
- remotePhones: "{count} 个手机连接",
5771
+ remotePhones: "手机已连接 {count} 部",
5722
5772
  remotePair: "显示配对码",
5723
5773
  remotePairAgain: "重新生成配对码",
5724
5774
  remotePairFailed: "无法生成配对码,请确认已登录订阅账号。",
@@ -6020,9 +6070,9 @@ window.__ModuleLoader__.load({
6020
6070
  searchTitle: "搜索来源",
6021
6071
  searchScope: "自动按当前会话模型分流;手动选择会覆盖所有模型和会话。",
6022
6072
  searchAuto: "自动",
6023
- searchAutoHint: "Codex 模型用订阅搜索,其他模型用 DSH",
6073
+ searchAutoHint: "Codex 模型用订阅搜索,其他模型用 DSH;一方未登录或没有 Key 时自动改用另一方",
6024
6074
  searchDsh: "DSH 默认",
6025
- searchDshHint: "所有模型使用 DSH 当前搜索服务",
6075
+ searchDshHint: "所有模型使用 DSH 当前搜索服务;没有 API Key 时用已登录的 DeepSeek 账号",
6026
6076
  searchCodex: "Codex 订阅",
6027
6077
  searchCodexHint: "所有模型通过已登录的 ChatGPT 订阅搜索",
6028
6078
  preferenceFailed: "设置未保存。",
@@ -6216,7 +6266,6 @@ window.__ModuleLoader__.load({
6216
6266
  modelFailed: "模型目录加载失败:{value}",
6217
6267
  groupFailed: "{name}:{value}",
6218
6268
  imageGenerate: "生成图片",
6219
- imageBeta: "Beta",
6220
6269
  imageGenerating: "正在生成…",
6221
6270
  imageGenerated: "已生成",
6222
6271
  imageFailed: "生成失败",
@@ -6363,10 +6412,17 @@ window.__ModuleLoader__.load({
6363
6412
  inputImageDetail_high: "High",
6364
6413
  inputImageDetail_original: "Original",
6365
6414
  connectors: "ChatGPT connectors",
6366
- connectorsHint: "Lets any model in DSH use the apps you have connected in ChatGPT (GitHub, Gmail, Google Drive, Calendar and more). Read-only: models can only read. Allow writes: anything that changes data, such as sending mail or editing files, asks for your approval every time. What apps return is third-party content and models do not follow instructions inside it. Connect the apps in ChatGPT first.",
6415
+ connectorsHint: "Lets any model in DSH use the apps you have connected in ChatGPT (GitHub, Gmail, Drive, Calendar and more). Connect the apps in ChatGPT first; what apps return is third-party content and models do not follow instructions inside it.",
6416
+ connectorsMode_off: "Connectors are off; models cannot see these apps.",
6417
+ connectorsMode_read: "Models can only read (search mail, read files, look up pull requests) and never change data.",
6418
+ connectorsMode_ask: "Anything that changes data, such as sending mail or editing files, asks for your approval every time.",
6419
+ connectorsMode_write: "Changing actions run immediately without a prompt and cannot be undone, so use this carefully.",
6420
+ hintMore: "More",
6421
+ hintLess: "Less",
6367
6422
  connectors_off: "Off",
6368
6423
  connectors_read: "Read-only",
6369
- connectors_ask: "Allow writes (ask each time)",
6424
+ connectors_ask: "Write after asking",
6425
+ connectors_write: "Write directly",
6370
6426
  reasoningSummary: "Reasoning summary",
6371
6427
  reasoningSummaryHint: "Shows a summary of the reasoning in the Thinking section of the conversation. Auto lets the model decide; concise is shorter; detailed is fuller; off asks for none.",
6372
6428
  retryPolicy: "Request retries",
@@ -6384,12 +6440,54 @@ window.__ModuleLoader__.load({
6384
6440
  subagentBackend_dsh: "DSH",
6385
6441
  subagentBackend_codex: "Codex",
6386
6442
  connectionTitle: "Connection",
6443
+ diagnosticsLog: "Log",
6444
+ diagnosticsLogHint: "What happened while running, in order. Filter to warnings and errors above. Tokens, emails and paths are removed.",
6445
+ logLevel_all: "All",
6446
+ logLevel_warn: "Warnings and errors",
6447
+ logLevel_error: "Errors only",
6448
+ logEmpty: "Nothing recorded at this level yet.",
6449
+ logCopy: "Copy log",
6450
+ logCopied: "Log copied",
6451
+ logEarlier: "previous run",
6387
6452
  remoteTitle: "Phone remote control",
6388
6453
  remote_off: "Off",
6389
6454
  remote_on: "On",
6455
+ voiceStart: "Voice conversation",
6456
+ voiceEnd: "End voice",
6457
+ voiceConnecting: "Connecting voice…",
6458
+ voiceListening: "Listening",
6459
+ voiceSpeaking: "Speaking",
6460
+ voiceWorking: "DSH is working on it…",
6461
+ voiceMute: "Turn the microphone off",
6462
+ voiceUnmute: "Turn the microphone on",
6463
+ voiceMuteSpeaker: "Turn the sound off",
6464
+ voiceUnmuteSpeaker: "Turn the sound on",
6465
+ voiceMicBlocked: "No microphone permission: allow it in the browser or system and try again.",
6466
+ voiceFailed: "The voice call did not connect. Try again in a moment.",
6467
+ voiceTitle: "Voice conversation",
6468
+ voiceHint: "When the message box is empty, the send button becomes a voice button: tap it to talk to DSH, ask it to do things, and hear the answer. Calls use your ChatGPT subscription and go through OpenAI.",
6469
+ voiceVoice: "Voice",
6470
+ voiceLanguage: "Answer language",
6471
+ voiceAuto: "Same as you speak",
6472
+ voicePlay: "Listen",
6473
+ voiceLoading: "Loading…",
6474
+ voiceNoSample: "No sample is available for this voice.",
6475
+ voiceUnavailable: "no sample yet",
6476
+ voiceMore: "More voices (no sample yet)",
6477
+ voiceLang_zh: "中文",
6478
+ voiceLang_en: "English",
6479
+ voiceLang_ja: "日本語",
6480
+ voiceLang_ko: "한국어",
6481
+ voiceLang_es: "Español",
6482
+ voiceLang_fr: "Français",
6483
+ voiceLang_de: "Deutsch",
6484
+ voiceLang_pt: "Português",
6485
+ voiceLang_ru: "Русский",
6390
6486
  remoteHint: "Experimental, off by default. When on, this computer's DSH appears as a host in the ChatGPT mobile app's Codex list, so you can follow conversations, send messages, switch models and approve actions from your phone. Needs a signed-in subscription account; the computer must stay on and online.",
6391
- remoteConnected: "Connected",
6487
+ remoteConnected: "This computer is online with ChatGPT; no phone connected yet",
6392
6488
  remoteConnecting: "Connecting",
6489
+ remoteWaiting: "Waiting for the previous connection to be released; usually reconnects within a few minutes",
6490
+ remoteConflictLong: "Still not connecting? Usually another computer, another DSH copy or another window is using phone remote control with the same ChatGPT account and they keep taking the connection from each other. Turn phone remote control off elsewhere, then switch it Off and On again here.",
6393
6491
  remoteError: "Connection failed, retrying",
6394
6492
  remoteStopped: "Stopped",
6395
6493
  remotePaired: "Paired phones",
@@ -6404,7 +6502,7 @@ window.__ModuleLoader__.load({
6404
6502
  remotePairSteps: "Scan this in the ChatGPT app's Codex section, or type the manual code.",
6405
6503
  remoteManualCode: "Manual pairing code",
6406
6504
  remoteQrLabel: "Pairing QR code",
6407
- remoteExpires: "Valid for about {minutes} minute(s)",
6505
+ remoteExpires: "Expires in about {minutes} min",
6408
6506
  remoteExpired: "The pairing code expired. Create a new one.",
6409
6507
  remoteLimits: "From the phone you can follow and continue DSH conversations, start new ones, send photos, switch models, choose permissions, approve actions and view changes. The phone cannot run other commands on the computer.",
6410
6508
  reviewModelTitle: "Auto review model",
@@ -6699,9 +6797,9 @@ window.__ModuleLoader__.load({
6699
6797
  searchTitle: "Search source",
6700
6798
  searchScope: "Auto follows the current session model; an explicit choice overrides every model and session.",
6701
6799
  searchAuto: "Auto",
6702
- searchAutoHint: "Codex models use subscription search; other models use DSH",
6800
+ searchAutoHint: "Codex models use subscription search; other models use DSH; if one side is not signed in or has no key, the other is used",
6703
6801
  searchDsh: "DSH default",
6704
- searchDshHint: "Use DSH's current search service for every model",
6802
+ searchDshHint: "Use DSH's current search service for every model; without an API key the signed-in DeepSeek account is used",
6705
6803
  searchCodex: "Codex subscription",
6706
6804
  searchCodexHint: "Search through the signed-in ChatGPT subscription for every model",
6707
6805
  preferenceFailed: "The setting was not saved.",
@@ -6895,7 +6993,6 @@ window.__ModuleLoader__.load({
6895
6993
  modelFailed: "Could not load models: {value}",
6896
6994
  groupFailed: "{name}: {value}",
6897
6995
  imageGenerate: "Generate image",
6898
- imageBeta: "Beta",
6899
6996
  imageGenerating: "Generating…",
6900
6997
  imageGenerated: "Generated",
6901
6998
  imageFailed: "Generation failed",
@@ -6964,11 +7061,26 @@ window.__ModuleLoader__.load({
6964
7061
  .codexSubscriptionCard{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:14px 16px;display:flex;flex-direction:column;gap:12px}
6965
7062
  .codexSubscriptionUsageCard{padding:12px 14px;gap:9px}
6966
7063
  .codexSubscriptionPreferencesCard{padding:12px 14px;gap:10px}
6967
- .codexSubscriptionPreference{min-height:32px;box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:12px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}
7064
+ .codexSubscriptionPreference{min-height:32px;box-sizing:border-box;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px 12px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}
6968
7065
  .codexSubscriptionPreferenceCopy{display:flex;min-width:0;flex-direction:column;gap:2px}
6969
7066
  .codexSubscriptionPreferenceLabel{display:flex;align-items:center;gap:6px}
6970
- .codexSubscriptionPreferenceHint{max-width:300px;font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}
6971
- .codexSubscriptionQuotaModes{display:flex;align-items:center;gap:3px;padding:2px;border-radius:9px;background:var(--dsw-alias-bg-module-platform)}
7067
+ .codexSubscriptionPreference>.codexSubscriptionPreferenceCopy{flex:1 1 240px}
7068
+ .codexSubscriptionPreferenceHint{max-width:none;font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}
7069
+ .codexSubscriptionHintText{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
7070
+ .codexSubscriptionHintFold[data-open=true] .codexSubscriptionHintText{display:block;-webkit-line-clamp:unset}
7071
+ .codexSubscriptionHintToggle{display:inline-flex;align-items:center;gap:3px;margin-top:2px;padding:0;border:0;background:none;font:inherit;font-size:11px;line-height:17px;color:var(--dsw-alias-label-secondary);cursor:pointer}
7072
+ .codexSubscriptionHintToggle::after{content:"";width:5px;height:5px;margin-top:-2px;border:solid currentColor;border-width:0 1.5px 1.5px 0;transform:rotate(45deg);transition:transform .15s}
7073
+ .codexSubscriptionHintFold[data-open=true] .codexSubscriptionHintToggle::after{margin-top:2px;transform:rotate(-135deg)}
7074
+ .codexSubscriptionHintToggle:hover{color:var(--dsw-alias-label-primary)}
7075
+ .codexSubscriptionHintToggle:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px;border-radius:3px}
7076
+ .codexSubscriptionPreferenceStack{margin-top:6px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}
7077
+ .codexSubscriptionPreferenceStack>.codexSubscriptionQuotaModes{flex:1 1 100%;margin-left:0;display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr)}
7078
+ .codexSubscriptionPreferenceStack .codexSubscriptionQuotaMode{min-height:30px;padding:4px 8px;text-align:center;white-space:normal;line-height:16px}
7079
+ .codexSubscriptionPreferenceLabel small{padding:0 6px;border-radius:999px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-tertiary);font-size:10px;font-weight:500;line-height:16px}
7080
+ @container (max-width:560px){.codexSubscriptionPreferenceStack>.codexSubscriptionQuotaModes{grid-auto-flow:row;grid-template-columns:repeat(2,minmax(0,1fr))}}
7081
+ .codexSubscriptionModeNote{flex:1 1 100%;padding:7px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
7082
+ .codexSubscriptionModeNote[data-danger=true]{background:color-mix(in srgb,var(--dsw-alias-label-danger,#d92d20) 12%,transparent);color:var(--dsw-alias-label-danger,#d92d20)}
7083
+ .codexSubscriptionQuotaModes{display:flex;flex-wrap:wrap;max-width:100%;align-items:center;gap:3px;padding:2px;border-radius:9px;background:var(--dsw-alias-bg-module-platform)}
6972
7084
  .codexSubscriptionQuotaMode{position:relative;display:flex;align-items:center;justify-content:center;min-height:26px;padding:0 9px;border-radius:7px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;cursor:pointer;white-space:nowrap}
6973
7085
  .codexSubscriptionQuotaMode small{margin-left:3px;font-size:9px;line-height:1;color:var(--dsw-alias-label-tertiary)}
6974
7086
  .codexSubscriptionQuotaMode:has(input:checked){background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 0 0 1px var(--dsw-alias-border-l3)}
@@ -7196,7 +7308,6 @@ window.__ModuleLoader__.load({
7196
7308
  .codexGeneratedImageFrame img{display:block;width:100%;height:100%;object-fit:cover}
7197
7309
  .codexGeneratedImageRetry{min-height:36px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);cursor:pointer}
7198
7310
  @keyframes codexImageSpin{to{transform:rotate(360deg)}}
7199
- .codexImageBeta{padding:0 5px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:16px}
7200
7311
  .codexImageToolGallery{display:flex;align-items:flex-start;flex-direction:column;gap:8px}
7201
7312
  @container (max-width:560px){.codexSubscriptionCreditRows{grid-template-columns:1fr}}
7202
7313
  @container (max-width:480px){.codexSubscriptionAccountRow,.codexSubscriptionSectionHead{align-items:flex-start;flex-direction:column}
@@ -7231,6 +7342,59 @@ window.__ModuleLoader__.load({
7231
7342
  @container (max-width:480px){.codexSubscriptionPreference,.codexSubscriptionContextHead{flex-wrap:wrap}.codexSubscriptionPreferenceHint{max-width:none}.codexSubscriptionPreference>.codexSubscriptionPreferenceCopy{flex:1 1 220px}.codexSubscriptionQuotaModes{max-width:100%;flex-wrap:wrap}.codexSubscriptionQuotaMode{min-height:30px}.codexSubscriptionActions{flex-wrap:wrap}}
7232
7343
  @container (max-width:340px){.codexSettingsTabs{grid-template-columns:repeat(2,minmax(0,1fr))}}
7233
7344
  @media(prefers-reduced-motion:reduce){.codexSubscription *{transition:none!important}}
7345
+ .codexSubscriptionLogHead{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;margin-top:12px}.codexSubscriptionLogHead h4{margin:0;font-size:13px}.codexSubscriptionLog{max-height:320px;overflow:auto;margin:6px 0 0;padding:8px 10px;border:1px solid var(--border,#0002);border-radius:8px;font:11.5px/1.5 ui-monospace,Consolas,monospace;white-space:pre-wrap;word-break:break-word}.codexSubscriptionLog_error{color:#c0392b}.codexSubscriptionLog_warn{color:#b9770e}.codexSubscriptionLog_info{opacity:.8}
7346
+ .codexSubscriptionConnectors>.codexSubscriptionQuotaModes{flex:0 0 auto;display:flex;flex-wrap:nowrap;margin-left:auto}
7347
+ .codexSubscriptionConnectors .codexSubscriptionQuotaMode{min-height:28px;padding:0 10px;white-space:nowrap}
7348
+ .codexSubscriptionConnectors>.codexSubscriptionModeNote{padding:0;background:none;font-size:12px;line-height:18px}
7349
+ .codexSubscriptionConnectors>.codexSubscriptionModeNote[data-danger=true]{background:none}
7350
+ @container (max-width:520px){.codexSubscriptionConnectors>.codexSubscriptionQuotaModes{flex:1 1 100%;margin-left:0;display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr)}.codexSubscriptionConnectors .codexSubscriptionQuotaMode{padding:0 4px;white-space:normal;text-align:center}}
7351
+ .codexVoiceChoice{display:flex;align-items:center;gap:8px}
7352
+ .codexVoiceChoice .codexSubscriptionContextTrigger{min-width:150px;justify-content:space-between}
7353
+ .codexVoiceChoice .codexSubscriptionContextTrigger:first-child:not(:last-child){min-width:0;justify-content:center}
7354
+ /* The voice button wears the send button's own classes, so its colours, shape and theme are always the send button's. */
7355
+ .codexVoiceCall{position:absolute;z-index:2;margin:0}
7356
+ .codexVoiceCall:disabled{cursor:progress}
7357
+ .codexVoiceCall[data-state=connecting]{animation:codexVoicePulse 1.1s ease-in-out infinite}
7358
+ .codexVoiceCallNote{position:absolute;bottom:calc(100% + 8px);z-index:3;width:max-content;max-width:320px;padding:6px 10px;border-radius:8px;background:var(--dsw-alias-surface-elevated,#2a2a2e);color:var(--dsw-alias-label-primary,#fff);font-size:12px;line-height:16px;box-shadow:0 4px 16px rgba(0,0,0,.3);white-space:normal}
7359
+ @keyframes codexVoicePulse{50%{opacity:.55}}
7360
+ @media (prefers-reduced-motion:reduce){.codexVoiceCall{animation:none!important}}
7361
+
7362
+ @keyframes codexVoiceStageIn{from{opacity:0;margin-top:8px}to{opacity:1;margin-top:0}}
7363
+ @media (prefers-reduced-motion:reduce){.codexVoiceOrb,.codexVoiceDot,.codexVoiceStage,.codexVoiceLines p{animation:none!important;transition:none!important}}
7364
+ .codexVoiceStage{position:fixed;z-index:60;display:flex;flex-direction:column;align-items:center;gap:6px;width:min(560px,92vw);pointer-events:none;animation:codexVoiceStageIn .28s cubic-bezier(.33,1,.68,1)}
7365
+ .codexVoiceStage[data-side=above]{transform:translate(-50%,calc(-100% - 16px))}
7366
+ .codexVoiceStage[data-side=below]{transform:translate(-50%,22px);flex-direction:column-reverse}
7367
+ .codexVoiceOrbBox{position:relative;width:120px;height:120px;flex:none;margin:-14px 0}
7368
+ .codexVoiceOrb{position:absolute;inset:0;transition:opacity .5s ease}
7369
+ .codexVoiceOrb[data-ready=false]{opacity:0}
7370
+ .codexVoiceOrb canvas{display:block;width:100%;height:100%}
7371
+ .codexVoiceDot{position:absolute;left:35px;top:35px;width:50px;height:50px;border-radius:50%;background:var(--dsw-alias-label-primary,#fff);animation:codexVoicePulse 1.1s ease-in-out infinite;transition:opacity .35s ease,transform .5s cubic-bezier(.33,1,.68,1)}
7372
+ .codexVoiceDot[data-gone=true]{opacity:0;transform:scale(1.8);animation:none}
7373
+ .codexVoiceLines{display:flex;flex-direction:column;align-items:center;gap:2px;max-width:100%;min-height:0;text-align:center}
7374
+ .codexVoiceLines p{margin:0;max-width:100%;font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary);overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;animation:codexVoiceStageIn .28s cubic-bezier(.33,1,.68,1)}
7375
+ .codexVoiceLines p[data-role=user]{color:var(--dsw-alias-label-secondary)}
7376
+ .codexVoiceLines p[data-old]{opacity:.45}
7377
+ .codexVoiceStatus{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
7378
+ .codexVoiceTools{display:inline-flex;align-items:center;gap:2px}
7379
+ [data-voice-live]>[data-slot="conversation.input.activity"]{display:none!important}
7380
+ .codexVoiceTool{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}
7381
+ .codexVoiceTool:hover{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary)}
7382
+ .codexVoiceTool[aria-pressed=true]{color:var(--dsw-alias-label-tertiary)}
7383
+
7384
+ /* Every fold-out in the settings reads as pressable: a full-width row, a chevron on the right that turns when it opens, a hover state. */
7385
+ .codexSubscriptionHistory>summary,.codexSubscriptionSearchOptions>summary,.codexSubscriptionRuntime summary,.codexSubscriptionDiagnostics details>summary,.codexImageDetails summary{display:flex;align-items:center;gap:8px;width:auto;min-height:32px;margin:0 -10px;padding:4px 10px;border-radius:8px;list-style:none;cursor:pointer;user-select:none;font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary);transition:background .12s}
7386
+ .codexSubscriptionHistory>summary::-webkit-details-marker,.codexSubscriptionSearchOptions>summary::-webkit-details-marker,.codexSubscriptionRuntime summary::-webkit-details-marker,.codexSubscriptionDiagnostics details>summary::-webkit-details-marker,.codexImageDetails summary::-webkit-details-marker{display:none}
7387
+ .codexSubscriptionHistory>summary::after,.codexSubscriptionSearchOptions>summary::after,.codexSubscriptionRuntime summary::after,.codexSubscriptionDiagnostics details>summary::after,.codexImageDetails summary::after{content:"";flex:none;margin-left:auto;width:7px;height:7px;border:solid var(--dsw-alias-label-tertiary);border-width:0 1.6px 1.6px 0;transform:translateY(-2px) rotate(45deg);transition:transform .15s,border-color .12s}
7388
+ .codexSubscriptionHistory[open]>summary::after,.codexSubscriptionSearchOptions[open]>summary::after,.codexSubscriptionRuntime details[open]>summary::after,.codexSubscriptionDiagnostics details[open]>summary::after,.codexImageDetails[open]>summary::after{transform:translateY(2px) rotate(-135deg)}
7389
+ .codexSubscriptionHistory>summary:hover,.codexSubscriptionSearchOptions>summary:hover,.codexSubscriptionRuntime summary:hover,.codexSubscriptionDiagnostics details>summary:hover,.codexImageDetails summary:hover{background:var(--dsw-alias-interactive-bg-hover)}
7390
+ .codexSubscriptionHistory>summary:hover::after,.codexSubscriptionSearchOptions>summary:hover::after,.codexSubscriptionRuntime summary:hover::after,.codexSubscriptionDiagnostics details>summary:hover::after,.codexImageDetails summary:hover::after{border-color:var(--dsw-alias-label-primary)}
7391
+ .codexSubscriptionHistory>summary:focus-visible,.codexSubscriptionSearchOptions>summary:focus-visible,.codexSubscriptionRuntime summary:focus-visible,.codexSubscriptionDiagnostics details>summary:focus-visible,.codexImageDetails summary:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:0}
7392
+ .codexSubscriptionSearchOptions>summary>span{float:none;margin-left:auto;font-weight:400}
7393
+ .codexSubscriptionSearchOptions>summary::after{margin-left:6px}
7394
+ .codexSubscriptionRuntime summary{font-size:12px}
7395
+ .codexSubscriptionHintToggle{margin:2px 0 0 -6px;padding:1px 6px;border-radius:6px;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary);transition:background .12s}
7396
+ .codexSubscriptionHintToggle::after{width:6px;height:6px}
7397
+ .codexSubscriptionHintToggle:hover{background:var(--dsw-alias-interactive-bg-hover)}
7234
7398
  `;
7235
7399
  //#endregion
7236
7400
  //#region src/image-edit-reference.js
@@ -7239,10 +7403,10 @@ window.__ModuleLoader__.load({
7239
7403
  const PIN_OUTER_STROKE = "#111827";
7240
7404
  const PIN_INNER_STROKE = "#ffffff";
7241
7405
  const finiteDimension = (value) => Number.isSafeInteger(value) && value > 0;
7242
- const clamp$2 = (value, min, max) => Math.min(max, Math.max(min, value));
7406
+ const clamp$3 = (value, min, max) => Math.min(max, Math.max(min, value));
7243
7407
  const clampCenter = (value, size, edge) => {
7244
7408
  const margin = Math.min(edge, size / 2);
7245
- return clamp$2(value, margin, size - margin);
7409
+ return clamp$3(value, margin, size - margin);
7246
7410
  };
7247
7411
  function getBitmapFactory(options) {
7248
7412
  const factory = options?.createImageBitmap ?? options?.bitmapFactory ?? globalThis.createImageBitmap;
@@ -7379,7 +7543,7 @@ window.__ModuleLoader__.load({
7379
7543
  `;
7380
7544
  //#endregion
7381
7545
  //#region src/subscription-image-transform.js
7382
- const clamp$1 = (value, min, max) => Math.min(max, Math.max(min, value));
7546
+ const clamp$2 = (value, min, max) => Math.min(max, Math.max(min, value));
7383
7547
  const fitted = () => ({
7384
7548
  zoom: 1,
7385
7549
  x: 0,
@@ -7421,15 +7585,15 @@ window.__ModuleLoader__.load({
7421
7585
  const limitX = Math.max(0, (surface.offsetWidth * zoom - stage.clientWidth) / 2);
7422
7586
  const limitY = Math.max(0, (surface.offsetHeight * zoom - stage.clientHeight) / 2);
7423
7587
  return {
7424
- x: clamp$1(x, -limitX, limitX),
7425
- y: clamp$1(y, -limitY, limitY)
7588
+ x: clamp$2(x, -limitX, limitX),
7589
+ y: clamp$2(y, -limitY, limitY)
7426
7590
  };
7427
7591
  }, []);
7428
7592
  const setZoomAt = (0, react.useCallback)((zoom, clientX, clientY) => {
7429
7593
  const stage = stageRef.current;
7430
7594
  if (!stage) return;
7431
7595
  setTransform((current) => {
7432
- const next = clamp$1(zoom, .5, Math.max(8, actualScale()));
7596
+ const next = clamp$2(zoom, .5, Math.max(8, actualScale()));
7433
7597
  const box = stage.getBoundingClientRect();
7434
7598
  const px = clientX - box.left - box.width / 2, py = clientY - box.top - box.height / 2;
7435
7599
  const ratio = next / current.zoom;
@@ -7570,7 +7734,7 @@ window.__ModuleLoader__.load({
7570
7734
  }) : null;
7571
7735
  };
7572
7736
  const fill$1 = (value, variables) => Object.entries(variables).reduce((text, [key, replacement]) => text.replaceAll(`{${key}}`, String(replacement)), value);
7573
- const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
7737
+ const clamp$1 = (value, min, max) => Math.min(max, Math.max(min, value));
7574
7738
  const bytesLabel = (bytes) => bytes === void 0 ? void 0 : bytes < 1024 * 1024 ? `${Math.max(.1, bytes / 1024).toLocaleString(void 0, { maximumFractionDigits: 1 })} KB` : `${(bytes / 1024 / 1024).toLocaleString(void 0, { maximumFractionDigits: 1 })} MB`;
7575
7739
  const downloadName = (name) => {
7576
7740
  const cleaned = String(name || "image.png").replace(/[<>:"/\\|?*\u0000-\u001f]/gu, "-").replace(/[. ]+$/u, "").trim();
@@ -7855,7 +8019,7 @@ window.__ModuleLoader__.load({
7855
8019
  }
7856
8020
  if (event.key === "[" || event.key === "]") {
7857
8021
  event.preventDefault();
7858
- setBrush((value) => Math.round(clamp(value + (event.key === "]" ? .01 : -.01), .01, .15) * 100) / 100);
8022
+ setBrush((value) => Math.round(clamp$1(value + (event.key === "]" ? .01 : -.01), .01, .15) * 100) / 100);
7859
8023
  return;
7860
8024
  }
7861
8025
  if (event.key === "ArrowLeft" || event.key === "ArrowRight") return;
@@ -7941,8 +8105,8 @@ window.__ModuleLoader__.load({
7941
8105
  const bounds = maskRef.current?.getBoundingClientRect();
7942
8106
  if (bounds === void 0) return void 0;
7943
8107
  return {
7944
- x: clamp((event.clientX - bounds.left) / bounds.width, 0, 1),
7945
- y: clamp((event.clientY - bounds.top) / bounds.height, 0, 1)
8108
+ x: clamp$1((event.clientX - bounds.left) / bounds.width, 0, 1),
8109
+ y: clamp$1((event.clientY - bounds.top) / bounds.height, 0, 1)
7946
8110
  };
7947
8111
  };
7948
8112
  const maskHandlers = {
@@ -8022,8 +8186,8 @@ window.__ModuleLoader__.load({
8022
8186
  if (bounds === void 0) return;
8023
8187
  const annotation = {
8024
8188
  id: crypto.randomUUID(),
8025
- x: clamp((event.clientX - bounds.left) / bounds.width, 0, 1),
8026
- y: clamp((event.clientY - bounds.top) / bounds.height, 0, 1),
8189
+ x: clamp$1((event.clientX - bounds.left) / bounds.width, 0, 1),
8190
+ y: clamp$1((event.clientY - bounds.top) / bounds.height, 0, 1),
8027
8191
  note: ""
8028
8192
  };
8029
8193
  setAnnotations((current) => [...current, annotation]);
@@ -8589,6 +8753,51 @@ window.__ModuleLoader__.load({
8589
8753
  const OUTPUT_VERBOSITY_MEDIUM = "medium";
8590
8754
  const OUTPUT_VERBOSITY_HIGH = "high";
8591
8755
  const DEFAULT_OUTPUT_VERBOSITY = OUTPUT_VERBOSITY_DEFAULT;
8756
+ const VOICE_CALL_FIELD = "voiceCall";
8757
+ const VOICE_VOICE_FIELD = "voiceVoice";
8758
+ const VOICE_VOICES = Object.freeze([
8759
+ "cove",
8760
+ "juniper",
8761
+ "maple",
8762
+ "spruce",
8763
+ "ember",
8764
+ "vale",
8765
+ "breeze",
8766
+ "arbor",
8767
+ "sol",
8768
+ "aube",
8769
+ "bubbie",
8770
+ "haetsal",
8771
+ "hanul",
8772
+ "himari",
8773
+ "leher",
8774
+ "miko",
8775
+ "neer",
8776
+ "porto",
8777
+ "rindo",
8778
+ "rio",
8779
+ "rivage",
8780
+ "selva",
8781
+ "sonna",
8782
+ "tinta",
8783
+ "viola"
8784
+ ]);
8785
+ const VOICE_LANGUAGE_FIELD = "voiceLanguage";
8786
+ const VOICE_LANGUAGES = Object.freeze([
8787
+ "auto",
8788
+ "zh",
8789
+ "en",
8790
+ "ja",
8791
+ "ko",
8792
+ "es",
8793
+ "fr",
8794
+ "de",
8795
+ "pt",
8796
+ "ru"
8797
+ ]);
8798
+ const normalizeVoiceCall = (value) => value === "off" ? "off" : "on";
8799
+ const normalizeVoiceVoice = (value) => VOICE_VOICES.includes(value) ? value : "cove";
8800
+ const normalizeVoiceLanguage = (value) => VOICE_LANGUAGES.includes(value) ? value : "auto";
8592
8801
  const REASONING_SUMMARY_FIELD = "reasoningSummary";
8593
8802
  const REASONING_SUMMARIES = Object.freeze([
8594
8803
  "auto",
@@ -8610,7 +8819,8 @@ window.__ModuleLoader__.load({
8610
8819
  const CONNECTOR_MODES = Object.freeze([
8611
8820
  "off",
8612
8821
  "read",
8613
- "ask"
8822
+ "ask",
8823
+ "write"
8614
8824
  ]);
8615
8825
  const INPUT_IMAGE_DETAIL_FIELD = "inputImageDetail";
8616
8826
  const INPUT_IMAGE_DETAILS = Object.freeze([
@@ -8754,6 +8964,9 @@ window.__ModuleLoader__.load({
8754
8964
  outputVerbosity: normalizeOutputVerbosity(value?.[OUTPUT_VERBOSITY_FIELD]),
8755
8965
  inputImageDetail: normalizeInputImageDetail(value?.[INPUT_IMAGE_DETAIL_FIELD]),
8756
8966
  reasoningSummary: normalizeReasoningSummary(value?.[REASONING_SUMMARY_FIELD]),
8967
+ voiceCall: normalizeVoiceCall(value?.[VOICE_CALL_FIELD]),
8968
+ voiceVoice: normalizeVoiceVoice(value?.[VOICE_VOICE_FIELD]),
8969
+ voiceLanguage: normalizeVoiceLanguage(value?.[VOICE_LANGUAGE_FIELD]),
8757
8970
  retryPolicy: normalizeRetryPolicy(value?.[RETRY_POLICY_FIELD]),
8758
8971
  connectors: normalizeConnectorMode(value?.[CONNECTORS_FIELD]),
8759
8972
  streamIdleTimeoutMinutes: normalizeStreamIdleTimeoutMinutes(value?.[STREAM_IDLE_TIMEOUT_MINUTES_FIELD]),
@@ -8813,6 +9026,9 @@ window.__ModuleLoader__.load({
8813
9026
  [OUTPUT_VERBOSITY_FIELD]: normalizeOutputVerbosity(value?.[OUTPUT_VERBOSITY_FIELD]),
8814
9027
  [INPUT_IMAGE_DETAIL_FIELD]: normalizeInputImageDetail(value?.[INPUT_IMAGE_DETAIL_FIELD]),
8815
9028
  [REASONING_SUMMARY_FIELD]: normalizeReasoningSummary(value?.[REASONING_SUMMARY_FIELD]),
9029
+ [VOICE_CALL_FIELD]: normalizeVoiceCall(value?.[VOICE_CALL_FIELD]),
9030
+ [VOICE_VOICE_FIELD]: normalizeVoiceVoice(value?.[VOICE_VOICE_FIELD]),
9031
+ [VOICE_LANGUAGE_FIELD]: normalizeVoiceLanguage(value?.[VOICE_LANGUAGE_FIELD]),
8816
9032
  [RETRY_POLICY_FIELD]: normalizeRetryPolicy(value?.[RETRY_POLICY_FIELD]),
8817
9033
  [CONNECTORS_FIELD]: normalizeConnectorMode(value?.[CONNECTORS_FIELD]),
8818
9034
  [STREAM_IDLE_TIMEOUT_MINUTES_FIELD]: normalizeStreamIdleTimeoutMinutes(value?.[STREAM_IDLE_TIMEOUT_MINUTES_FIELD]),
@@ -9300,7 +9516,7 @@ window.__ModuleLoader__.load({
9300
9516
  }
9301
9517
  //#endregion
9302
9518
  //#region src/version.js
9303
- const PACKAGE_VERSION = "3.0.0-beta.8";
9519
+ const PACKAGE_VERSION = "3.0.1";
9304
9520
  //#endregion
9305
9521
  //#region src/client-recovery.js
9306
9522
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -9979,6 +10195,1234 @@ window.__ModuleLoader__.load({
9979
10195
  });
9980
10196
  }
9981
10197
  //#endregion
10198
+ //#region src/client-hint.jsx
10199
+ /** A setting's explanation: shows up to two lines; a "More" link appears only when the text is actually cut off. */
10200
+ function Hint({ children, t, as: Tag = "span" }) {
10201
+ const [open, setOpen] = (0, react.useState)(false);
10202
+ const [clipped, setClipped] = (0, react.useState)(false);
10203
+ const text = (0, react.useRef)(null);
10204
+ const id = (0, react.useId)();
10205
+ const foldable = typeof children === "string";
10206
+ (0, react.useLayoutEffect)(() => {
10207
+ const node = text.current;
10208
+ if (!foldable || node === null || open) return;
10209
+ const measure = () => setClipped(node.scrollHeight > node.clientHeight + 1);
10210
+ measure();
10211
+ if (typeof ResizeObserver !== "function") return;
10212
+ const observer = new ResizeObserver(measure);
10213
+ observer.observe(node);
10214
+ return () => observer.disconnect();
10215
+ }, [
10216
+ children,
10217
+ foldable,
10218
+ open
10219
+ ]);
10220
+ if (!foldable) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tag, {
10221
+ className: "codexSubscriptionPreferenceHint",
10222
+ children
10223
+ });
10224
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tag, {
10225
+ className: "codexSubscriptionPreferenceHint codexSubscriptionHintFold",
10226
+ "data-open": open || void 0,
10227
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10228
+ id,
10229
+ ref: text,
10230
+ className: "codexSubscriptionHintText",
10231
+ children
10232
+ }), clipped || open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
10233
+ type: "button",
10234
+ className: "codexSubscriptionHintToggle",
10235
+ "aria-expanded": open,
10236
+ "aria-controls": id,
10237
+ onClick: () => setOpen((value) => !value),
10238
+ children: t(open ? "hintLess" : "hintMore")
10239
+ }) : null]
10240
+ });
10241
+ }
10242
+ //#endregion
10243
+ //#region src/voice-call.js
10244
+ const ICE_WAIT_MS = 3e3;
10245
+ const waitForCandidates = (connection) => new Promise((resolve) => {
10246
+ if (connection.iceGatheringState === "complete") {
10247
+ resolve();
10248
+ return;
10249
+ }
10250
+ const done = () => {
10251
+ clearTimeout(timer);
10252
+ connection.removeEventListener("icegatheringstatechange", check);
10253
+ resolve();
10254
+ };
10255
+ const check = () => {
10256
+ if (connection.iceGatheringState === "complete") done();
10257
+ };
10258
+ const timer = setTimeout(done, ICE_WAIT_MS);
10259
+ connection.addEventListener("icegatheringstatechange", check);
10260
+ });
10261
+ /** Two short soft tones: rising when a call starts, falling when it ends. */
10262
+ const chime = (context, frequencies) => {
10263
+ try {
10264
+ const now = context.currentTime;
10265
+ frequencies.forEach((frequency, index) => {
10266
+ const tone = context.createOscillator();
10267
+ const gain = context.createGain();
10268
+ const start = now + index * .11;
10269
+ tone.type = "sine";
10270
+ tone.frequency.value = frequency;
10271
+ gain.gain.setValueAtTime(0, start);
10272
+ gain.gain.linearRampToValueAtTime(.07, start + .02);
10273
+ gain.gain.exponentialRampToValueAtTime(1e-4, start + .16);
10274
+ tone.connect(gain).connect(context.destination);
10275
+ tone.start(start);
10276
+ tone.stop(start + .18);
10277
+ });
10278
+ } catch {}
10279
+ };
10280
+ /** Loudness of an analyser as 0..1 (the root mean square of the waveform, lifted so quiet speech still moves the orb). */
10281
+ const loudness = (analyser, buffer) => {
10282
+ if (!analyser) return 0;
10283
+ analyser.getByteTimeDomainData(buffer);
10284
+ let sum = 0;
10285
+ for (const value of buffer) {
10286
+ const centred = (value - 128) / 128;
10287
+ sum += centred * centred;
10288
+ }
10289
+ return Math.min(1, Math.sqrt(sum / buffer.length) * 3.2);
10290
+ };
10291
+ /**
10292
+ * A voice call from this window: the microphone and the model's voice travel over WebRTC straight to OpenAI; the
10293
+ * plugin's host turns the offer into a call, joins it, and runs what is asked as turns of this conversation.
10294
+ * Resolves with a handle once the call is connected; rejects with the reason when it could not start.
10295
+ */
10296
+ async function openVoiceCall({ rpc, sessionId, onEnd }) {
10297
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia || typeof RTCPeerConnection !== "function") throw new Error("unsupported");
10298
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: {
10299
+ echoCancellation: true,
10300
+ noiseSuppression: true,
10301
+ autoGainControl: true
10302
+ } });
10303
+ const connection = new RTCPeerConnection();
10304
+ const sound = new Audio();
10305
+ sound.autoplay = true;
10306
+ const AudioContextClass = window.AudioContext ?? window.webkitAudioContext;
10307
+ const context = AudioContextClass ? new AudioContextClass() : void 0;
10308
+ const analysers = {
10309
+ mic: void 0,
10310
+ assistant: void 0
10311
+ };
10312
+ const watch = (name, source) => {
10313
+ if (!context) return;
10314
+ try {
10315
+ const analyser = context.createAnalyser();
10316
+ analyser.fftSize = 512;
10317
+ context.createMediaStreamSource(source).connect(analyser);
10318
+ analysers[name] = analyser;
10319
+ } catch {}
10320
+ };
10321
+ watch("mic", stream);
10322
+ connection.ontrack = (event) => {
10323
+ sound.srcObject = event.streams[0];
10324
+ watch("assistant", event.streams[0]);
10325
+ };
10326
+ for (const track of stream.getTracks()) connection.addTrack(track, stream);
10327
+ connection.createDataChannel("oai-events");
10328
+ let ended = false;
10329
+ const end = async () => {
10330
+ if (ended) return;
10331
+ ended = true;
10332
+ if (context) chime(context, [740, 520]);
10333
+ for (const track of stream.getTracks()) track.stop();
10334
+ sound.srcObject = null;
10335
+ try {
10336
+ connection.close();
10337
+ } catch {}
10338
+ try {
10339
+ await rpc.call(CHANNEL, "voice/stop", { sessionId });
10340
+ } catch {}
10341
+ setTimeout(() => {
10342
+ context?.close().catch(() => {});
10343
+ }, 600);
10344
+ onEnd?.();
10345
+ };
10346
+ connection.addEventListener("connectionstatechange", () => {
10347
+ if ([
10348
+ "failed",
10349
+ "disconnected",
10350
+ "closed"
10351
+ ].includes(connection.connectionState)) end();
10352
+ });
10353
+ try {
10354
+ await context?.resume();
10355
+ await connection.setLocalDescription(await connection.createOffer());
10356
+ await waitForCandidates(connection);
10357
+ const result = await rpc.call(CHANNEL, "voice/start", {
10358
+ sessionId,
10359
+ sdp: connection.localDescription.sdp
10360
+ });
10361
+ if (!result.ok) throw new Error(result.error?.message ?? "start failed");
10362
+ await connection.setRemoteDescription({
10363
+ type: "answer",
10364
+ sdp: result.value.answer
10365
+ });
10366
+ } catch (error) {
10367
+ ended = true;
10368
+ for (const track of stream.getTracks()) track.stop();
10369
+ try {
10370
+ connection.close();
10371
+ } catch {}
10372
+ context?.close().catch(() => {});
10373
+ throw error;
10374
+ }
10375
+ if (context) chime(context, [520, 740]);
10376
+ const buffers = {
10377
+ mic: /* @__PURE__ */ new Uint8Array(512),
10378
+ assistant: /* @__PURE__ */ new Uint8Array(512)
10379
+ };
10380
+ let muted = false;
10381
+ return {
10382
+ stop: end,
10383
+ isMuted: () => muted,
10384
+ setMuted: (value) => {
10385
+ muted = value;
10386
+ for (const track of stream.getAudioTracks()) track.enabled = !value;
10387
+ },
10388
+ isOutputMuted: () => sound.muted,
10389
+ setOutputMuted: (value) => {
10390
+ sound.muted = value;
10391
+ },
10392
+ /** How loud the person (mic) and the model (voice) are right now, 0..1 each. */
10393
+ levels: () => ({
10394
+ mic: muted ? 0 : loudness(analysers.mic, buffers.mic),
10395
+ assistant: loudness(analysers.assistant, buffers.assistant)
10396
+ })
10397
+ };
10398
+ }
10399
+ //#endregion
10400
+ //#region src/voice-orb.js
10401
+ const VERTEX = "attribute vec2 a;void main(){gl_Position=vec4(a,0.,1.);}";
10402
+ const FRAGMENT = `
10403
+ precision highp float;
10404
+ uniform vec2 uRes;
10405
+ uniform float uWaveFrame;
10406
+ uniform float uBaseFrame;
10407
+ uniform float uAmp;
10408
+ uniform float uFlowFrame;
10409
+ uniform float uEdge;
10410
+ uniform float uNoiseScale;
10411
+ uniform float uSurface;
10412
+ uniform float uMic;
10413
+ uniform float uUser;
10414
+ uniform float uFlat;
10415
+ uniform vec2 uOff0;
10416
+ uniform vec2 uOff1;
10417
+ uniform vec2 uOff2;
10418
+ uniform vec3 uAccent;
10419
+
10420
+ float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
10421
+ vec2 hash2(vec2 p){return vec2(hash(p),hash(p+vec2(19.19,73.73)));}
10422
+ float vnoise(vec2 p){
10423
+ vec2 i=floor(p),f=fract(p);f=f*f*(3.-2.*f);
10424
+ return mix(mix(hash(i),hash(i+vec2(1.,0.)),f.x),mix(hash(i+vec2(0.,1.)),hash(i+vec2(1.,1.)),f.x),f.y);
10425
+ }
10426
+ float fbm(vec2 p,float rough,float lac){
10427
+ float a=vnoise(p);
10428
+ float b=vnoise(p*lac+vec2(17.17,31.31));
10429
+ float c=vnoise(p*lac*lac+vec2(47.47,11.11));
10430
+ return (a+b*rough+c*rough*rough)/(1.+rough+rough*rough);
10431
+ }
10432
+ vec3 noise3(vec3 coord,float w,float scale,float rough,float lac,float unit){
10433
+ vec2 p=coord.xy*scale;
10434
+ float t=w*scale;
10435
+ vec2 d1=vec2(.17,.11),d2=vec2(-.13,.19),d3=vec2(.07,-.23);
10436
+ if(unit>.5){d1=normalize(d1);d2=normalize(d2);d3=normalize(d3);}
10437
+ return vec3(fbm(p+d1*t,rough,lac),fbm(p+vec2(41.41,17.17)+d2*t,rough,lac),fbm(p+vec2(23.23,59.59)+d3*t,rough,lac));
10438
+ }
10439
+
10440
+ // the painted-paper texture: soft pale ground, darker stains with ragged edges, a fine fibre. Reads like grey watercolour.
10441
+ float paint(vec2 uv){
10442
+ vec2 w=vec2(vnoise(uv*3.+vec2(4.2,1.7)),vnoise(uv*3.+vec2(9.1,6.3)))-.5;
10443
+ vec2 q=uv+w*.3;
10444
+ float body=vnoise(q*2.+vec2(3.1,8.3))*.5+vnoise(q*4.3+vec2(9.7,1.9))*.3+vnoise(q*9.+vec2(2.3,5.5))*.2;
10445
+ float stain=smoothstep(.5,.64,body);
10446
+ float wash=smoothstep(.28,.6,vnoise(q*2.6+vec2(14.2,4.4)));
10447
+ float mott=vnoise(q*vec2(18.,16.)+vec2(5.,5.))*.55+vnoise(q*vec2(42.,36.))*.45;
10448
+ float ragged=stain*(1.-stain)*4.;
10449
+ float v=.92-.2*wash-.5*stain+(mott-.5)*(.1+.2*ragged);
10450
+ return clamp(v,.16,1.);
10451
+ }
10452
+
10453
+ vec3 warp3(vec3 c,float frame){
10454
+ vec2 pt=c.xy*8.;
10455
+ vec2 cell=floor(pt),local=fract(pt);
10456
+ vec2 nearest=vec2(0.);
10457
+ float best=3.4e38;
10458
+ float phase=frame*6.2831853/240.;
10459
+ for(int y=-1;y<=1;y++){for(int x=-1;x<=1;x++){
10460
+ vec2 off=vec2(float(x),float(y));
10461
+ vec2 r=hash2(cell+off);
10462
+ vec2 pos=off+r*.7185189723968506+sin(vec2(phase)+r*6.2831853)*.04;
10463
+ vec2 d=pos-local;
10464
+ float dd=dot(d,d);
10465
+ if(dd<best){best=dd;nearest=pos;}
10466
+ }}
10467
+ vec3 vor=vec3((nearest+cell)/8.,c.z);
10468
+ return (c-vec3(.5))*.26+(vor-c)*.06;
10469
+ }
10470
+
10471
+ vec3 rotY(vec3 v,float a){float c=cos(a),s=sin(a);return vec3(c*v.x+s*v.z,v.y,-s*v.x+c*v.z);}
10472
+ float fac(vec3 v,float lift){vec3 r=rotY(v,.7853981852531433)+vec3(0.,lift,0.);return (r.x+r.y+r.z)/3.;}
10473
+ // 1 below a, easing to 0 at b
10474
+ float ramp(float f,float a,float b){float t=clamp((f-a)/max(b-a,1e-6),0.,1.);t=t*t*(3.-2.*t);return 1.-t;}
10475
+
10476
+ vec3 interior(vec3 gen){
10477
+ vec3 a=uAccent;
10478
+ vec3 soft=mix(((16.*a-12.)*a+4.)*a,sqrt(a),step(.25,a));
10479
+ vec3 cShadow=max(vec3(0.),a+.9465623-1.);
10480
+ vec3 cMidLow=a+(2.*.89882153-1.)*(soft-a);
10481
+ vec3 cMidHigh=vec3(1.);
10482
+ vec3 cHigh=1.-(1.-a)*(1.-.63704175);
10483
+
10484
+ vec3 m=gen+vec3(0.,-.14,0.);
10485
+ vec2 cen=gen.xy-.5;
10486
+ float rad=length(cen);
10487
+ m.xy+=(rad>1e-6?cen/rad:vec2(0.))*smoothstep(.015,.44,rad)*uEdge;
10488
+ vec3 wave=noise3(m,uWaveFrame/100.,1.,.4,2.,1.);
10489
+ vec3 m2=m+(wave-.5)*(.8*uAmp);
10490
+ vec3 grain=noise3(m2,uFlowFrame/10.,4000.,.5,2.,0.);
10491
+ vec3 g2=(grain-.5)*uNoiseScale*.06;
10492
+ vec3 base=m2*.8+g2;
10493
+ float blend=clamp(.5-.5*cos(uBaseFrame*6.2831853/120.),0.,1.);
10494
+ vec3 cw=warp3(m2,uBaseFrame);
10495
+
10496
+ // tone 3: highlight
10497
+ vec2 p0=base.xy+uOff0;
10498
+ vec2 f0=vec2(base.x,1.-base.y)+uOff0;
10499
+ float s0=mix(paint(p0)-.5,paint(f0)-.5,blend)*(.2-.06*cos((uBaseFrame-1.)*6.2831853/120.));
10500
+ vec3 v6=m2+vec3(s0,s0,0.)+cw;
10501
+ float rHigh=ramp(fac(v6,.56),.3431819975376129,.6200000047683716);
10502
+
10503
+ // tone 2: white
10504
+ vec3 b1=m2*.8+vec3(.04,.02,0.)+g2;
10505
+ vec3 b2=m2*.8+vec3(-.04,-.02,0.)+g2;
10506
+ float s1=mix(paint(b1.xy+uOff1)-.5,paint(vec2(b2.x,1.-b2.y)+uOff1)-.5,blend)*(.2-.06*cos((uBaseFrame-1.)*6.2831853/80.));
10507
+ vec3 v5=m2+vec3(s1,s1,0.)+cw;
10508
+ float rMidHigh=ramp(fac(v5,.26),.4113636016845703,.5586364269256592);
10509
+
10510
+ // tone 1: mid-low, with the left-to-right fade the original uses
10511
+ float gx=clamp(gen.y-.32,0.,1.);
10512
+ float gradient=gx*gx*(3.-2.*gx);
10513
+ vec3 c1=m2*.8+vec3(-.08,-.04,0.)+g2;
10514
+ vec3 c2=m2*.8+vec3(-.96,.06,0.)+g2;
10515
+ float t1=mix(paint(c1.xy+uOff2),1.,gradient)-.5;
10516
+ float t2=mix(paint(vec2(c2.x,1.-c2.y)+uOff2),1.,gradient)-.5;
10517
+ float s2=mix(t1,t2,blend)*(.14-.06*cos((uBaseFrame-1.)*6.2831853/100.));
10518
+ vec3 v9=m2+vec3(s2,s2,0.)+cw;
10519
+ float rMidLow=ramp(fac(v9,.12),.3522728383541107,.7400000095367432);
10520
+
10521
+ vec3 col=cShadow;
10522
+ col=mix(col,cMidLow,rMidLow);
10523
+ col=mix(col,cMidHigh,rMidHigh);
10524
+ col=mix(col,cHigh,rHigh);
10525
+ return col;
10526
+ }
10527
+
10528
+ void main(){
10529
+ if(uFlat>.5){gl_FragColor=vec4(interior(vec3(gl_FragCoord.xy/uRes,.5)),1.);return;}
10530
+ vec2 frame=(gl_FragCoord.xy/uRes)*2.-1.;
10531
+ float user=clamp(uUser,0.,1.);
10532
+ float voice=max(1.-user*.14+user*clamp(uMic,0.,1.)*.08,.75);
10533
+ float scale=voice*uSurface;
10534
+ vec2 sdf=frame/scale;
10535
+ float dist=length(sdf)-1.;
10536
+ float px=2./uRes.x/scale;
10537
+ float shape=1.-smoothstep(-px,px,dist+px*.5);
10538
+ if(shape<=0.){gl_FragColor=vec4(0.);return;}
10539
+ vec3 col=interior(vec3(sdf*.5+.5,.5));
10540
+ gl_FragColor=vec4(col*shape,shape);
10541
+ }`;
10542
+ const parseColor = (text) => {
10543
+ const match = /(\d+(?:\.\d+)?)[ ,]+(\d+(?:\.\d+)?)[ ,]+(\d+(?:\.\d+)?)/u.exec(String(text));
10544
+ return match ? [
10545
+ Number(match[1]) / 255,
10546
+ Number(match[2]) / 255,
10547
+ Number(match[3]) / 255
10548
+ ] : [
10549
+ .25,
10550
+ .46,
10551
+ .9
10552
+ ];
10553
+ };
10554
+ const clamp = (value, low = 0, high = 1) => Math.min(Math.max(value, low), high);
10555
+ const smooth = (low, high, value) => {
10556
+ const t = clamp((value - low) / (high - low));
10557
+ return t * t * (3 - 2 * t);
10558
+ };
10559
+ const ease = (current, target, dt, attack, release) => current + (target - current) * (1 - Math.exp(-dt / (target > current ? attack : release)));
10560
+ const spring = (value, velocity, target, dt, frequency, damping) => {
10561
+ const omega = 2 * Math.PI * frequency;
10562
+ const steps = Math.max(1, Math.ceil(dt * 120));
10563
+ const h = dt / steps;
10564
+ let v = value;
10565
+ let w = velocity;
10566
+ for (let i = 0; i < steps; i += 1) {
10567
+ w += ((target - v) * omega * omega - w * 2 * damping * omega) * h;
10568
+ v += w * h;
10569
+ }
10570
+ return {
10571
+ value: v,
10572
+ velocity: w
10573
+ };
10574
+ };
10575
+ const TUNE = {
10576
+ fps: 24,
10577
+ startFrame: 350,
10578
+ surface: .34,
10579
+ fit: 1.4,
10580
+ speakSpeedScale: .56,
10581
+ speakSpeedBase: 1.8,
10582
+ speakSlowEnergy: 4.6,
10583
+ speakAmp: 1.5,
10584
+ minWaveSpeed: .72,
10585
+ edgeDistance: .102,
10586
+ edgeFrequency: 7,
10587
+ edgeDamping: .36,
10588
+ listenNoise: 2.2,
10589
+ flowScale: .96,
10590
+ driftScale: .1,
10591
+ windScale: .3,
10592
+ windSpeed: .65
10593
+ };
10594
+ const WIND = [
10595
+ .72,
10596
+ 1,
10597
+ 1.28
10598
+ ];
10599
+ /** Returns a renderer for the canvas, or undefined when this browser cannot draw it. */
10600
+ function createOrb(canvas, accent, options = {}) {
10601
+ const gl = canvas.getContext("webgl", {
10602
+ alpha: true,
10603
+ premultipliedAlpha: true,
10604
+ antialias: false
10605
+ });
10606
+ if (!gl) return void 0;
10607
+ const compile = (type, source) => {
10608
+ const shader = gl.createShader(type);
10609
+ gl.shaderSource(shader, source);
10610
+ gl.compileShader(shader);
10611
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(shader) || "shader");
10612
+ return shader;
10613
+ };
10614
+ let program;
10615
+ try {
10616
+ program = gl.createProgram();
10617
+ gl.attachShader(program, compile(gl.VERTEX_SHADER, VERTEX));
10618
+ gl.attachShader(program, compile(gl.FRAGMENT_SHADER, FRAGMENT));
10619
+ gl.linkProgram(program);
10620
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error("link");
10621
+ } catch (error) {
10622
+ options.onError?.(error);
10623
+ return;
10624
+ }
10625
+ gl.useProgram(program);
10626
+ const buffer = gl.createBuffer();
10627
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
10628
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
10629
+ -1,
10630
+ -1,
10631
+ 1,
10632
+ -1,
10633
+ -1,
10634
+ 1,
10635
+ 1,
10636
+ 1
10637
+ ]), gl.STATIC_DRAW);
10638
+ const position = gl.getAttribLocation(program, "a");
10639
+ gl.enableVertexAttribArray(position);
10640
+ gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
10641
+ const at = (name) => gl.getUniformLocation(program, name);
10642
+ const u = {};
10643
+ for (const name of [
10644
+ "uRes",
10645
+ "uWaveFrame",
10646
+ "uBaseFrame",
10647
+ "uAmp",
10648
+ "uFlowFrame",
10649
+ "uEdge",
10650
+ "uNoiseScale",
10651
+ "uSurface",
10652
+ "uMic",
10653
+ "uUser",
10654
+ "uFlat",
10655
+ "uOff0",
10656
+ "uOff1",
10657
+ "uOff2",
10658
+ "uAccent"
10659
+ ]) u[name] = at(name);
10660
+ const color = parseColor(accent);
10661
+ const flat = options.flat ? Number(options.flat) : 0;
10662
+ const s = {
10663
+ last: void 0,
10664
+ base: TUNE.startFrame,
10665
+ wave: TUNE.startFrame,
10666
+ flow: TUNE.startFrame,
10667
+ listen: 1,
10668
+ speak: 0,
10669
+ think: 0,
10670
+ level: 0,
10671
+ motion: 0,
10672
+ prevOverall: 0,
10673
+ accent: 0,
10674
+ cumulative: [
10675
+ 0,
10676
+ 0,
10677
+ 0
10678
+ ],
10679
+ drive: 0,
10680
+ boost: 0,
10681
+ boostV: 0,
10682
+ amp: 1,
10683
+ edge: 0,
10684
+ edgeV: 0,
10685
+ flowBoost: 0,
10686
+ colour: [
10687
+ 0,
10688
+ 0,
10689
+ 0
10690
+ ],
10691
+ phase: [
10692
+ 0,
10693
+ 2.1,
10694
+ 4.2
10695
+ ],
10696
+ user: 0,
10697
+ userOn: false,
10698
+ userFrom: 0,
10699
+ userAt: 0
10700
+ };
10701
+ return {
10702
+ /** Draw one frame. `now` in seconds; `input` = { mic, assistant, working, reduce } with levels 0..1. */
10703
+ draw(now, input) {
10704
+ const dt = s.last === void 0 ? .016 : clamp(now - s.last, 0, .1);
10705
+ s.last = now;
10706
+ const assistant = clamp(input.assistant ?? 0);
10707
+ const mic = clamp((input.mic ?? 0) * 1.75);
10708
+ const speaking = assistant > .05;
10709
+ const thinking = !speaking && !!input.working;
10710
+ const pace = input.reduce ? .5 : 1;
10711
+ const target = speaking ? {
10712
+ listen: .2,
10713
+ speak: 1,
10714
+ think: 0
10715
+ } : thinking ? {
10716
+ listen: .65,
10717
+ speak: 0,
10718
+ think: 1
10719
+ } : {
10720
+ listen: 1,
10721
+ speak: 0,
10722
+ think: 0
10723
+ };
10724
+ const k = 1 - Math.exp(-dt / .28);
10725
+ s.listen += (target.listen - s.listen) * k;
10726
+ s.speak += (target.speak - s.speak) * k;
10727
+ s.think += (target.think - s.think) * k;
10728
+ const overall = speaking ? assistant : 0;
10729
+ s.motion += (overall - s.motion) * (1 - Math.exp(-dt / .2));
10730
+ s.accent = Math.max(overall - s.prevOverall, 0) * 3.6;
10731
+ s.prevOverall = overall;
10732
+ for (let i = 0; i < 3; i += 1) s.cumulative[i] += overall * (i === 0 ? .82 : i === 1 ? .76 : .68) * dt * 60 * 40 * (1 - Math.exp(-dt / 2));
10733
+ const energy = clamp(Math.max(...s.cumulative) / 12);
10734
+ const waveLevel = overall > .024 ? (1 - Math.exp(-overall * 4)) ** 1.15 : 0;
10735
+ s.drive = ease(s.drive, waveLevel, dt, .045, .14);
10736
+ const speedTarget = s.drive * TUNE.speakSpeedScale * (TUNE.speakSpeedBase + Math.max(s.motion, energy) * TUNE.speakSlowEnergy + s.accent * .5);
10737
+ const boost = spring(s.boost, s.boostV, speedTarget, dt, 6, .76);
10738
+ s.boost = boost.value;
10739
+ s.boostV = boost.velocity;
10740
+ const waveSpeed = Math.max(TUNE.minWaveSpeed, 1 + s.boost);
10741
+ s.amp = ease(s.amp, 1 - waveLevel * (1 - TUNE.speakAmp), dt, .1, .28);
10742
+ const userDrive = clamp(s.user) * clamp(input.mic ?? 0);
10743
+ const edge = spring(s.edge, s.edgeV, userDrive * TUNE.edgeDistance, dt, TUNE.edgeFrequency, TUNE.edgeDamping);
10744
+ s.edge = edge.value;
10745
+ s.edgeV = edge.velocity;
10746
+ const noiseScale = 1 + userDrive * (TUNE.listenNoise - 1);
10747
+ s.flowBoost = ease(s.flowBoost, Math.max(s.motion, energy) * TUNE.flowScale, dt, .055, .16);
10748
+ const talking = !speaking && !thinking && (s.userOn ? mic > .32 : mic >= .48);
10749
+ if (talking !== s.userOn) {
10750
+ s.userFrom = s.user;
10751
+ s.userOn = talking;
10752
+ s.userAt = now;
10753
+ }
10754
+ s.user = s.userFrom + ((s.userOn ? 1 : 0) - s.userFrom) * clamp((now - s.userAt) / .14);
10755
+ const offsets = [
10756
+ [0, 0],
10757
+ [0, 0],
10758
+ [0, 0]
10759
+ ];
10760
+ const audio = s.cumulative.map((value) => clamp(value / 12));
10761
+ for (let i = 0; i < 3; i += 1) s.colour[i] = ease(s.colour[i], audio[i] * (speaking ? 1 : 0) * 1, dt, .07, .22);
10762
+ if (speaking) for (let i = 0; i < 3; i += 1) s.phase[i] += dt * TUNE.windSpeed * WIND[i] * (1 + s.colour[i] * 1.5);
10763
+ const [r, g, b] = s.colour;
10764
+ const [p0, p1, p2] = s.phase;
10765
+ const d = TUNE.driftScale;
10766
+ const w = TUNE.windScale;
10767
+ offsets[0] = [r * d + Math.sin(p0) * r * w, -g * d * .55 + Math.cos(p0 * .82) * g * w * .65];
10768
+ offsets[1] = [-g * d * .75 + Math.cos(p1 * .91) * g * w * .8, b * d + Math.sin(p1) * b * w];
10769
+ offsets[2] = [b * d * 1.1 + Math.sin(p2 * 1.07) * b * w * 1.1, r * d * .7 + Math.cos(p2) * r * w * .75];
10770
+ s.base += dt * pace * TUNE.fps;
10771
+ s.wave += dt * pace * TUNE.fps * waveSpeed;
10772
+ s.flow += dt * pace * TUNE.fps * (1 + s.flowBoost);
10773
+ s.level = ease(s.level, speaking ? assistant : 0, dt, .09, .3);
10774
+ const out = smooth(.04, .46, s.level);
10775
+ const breath = Math.sin(now * Math.PI * .34) * .5 + .5;
10776
+ const state = Math.max(s.listen, s.think, s.speak);
10777
+ let size = TUNE.surface * (.88 + .06 * s.think) * (.82 + .18 * smooth(0, .9, state));
10778
+ size += out * TUNE.surface * .12;
10779
+ size += (1 - s.think) * (1 - out) * breath * TUNE.surface * .01;
10780
+ const surface = Math.min(TUNE.surface, size) * 2 * TUNE.fit;
10781
+ gl.viewport(0, 0, canvas.width, canvas.height);
10782
+ gl.clearColor(0, 0, 0, 0);
10783
+ gl.clear(gl.COLOR_BUFFER_BIT);
10784
+ gl.uniform2f(u.uRes, canvas.width, canvas.height);
10785
+ gl.uniform1f(u.uWaveFrame, s.wave);
10786
+ gl.uniform1f(u.uBaseFrame, s.base);
10787
+ gl.uniform1f(u.uAmp, s.amp);
10788
+ gl.uniform1f(u.uFlowFrame, s.flow);
10789
+ gl.uniform1f(u.uEdge, s.edge);
10790
+ gl.uniform1f(u.uNoiseScale, noiseScale);
10791
+ gl.uniform1f(u.uSurface, surface);
10792
+ gl.uniform1f(u.uMic, mic);
10793
+ gl.uniform1f(u.uUser, s.user);
10794
+ gl.uniform1f(u.uFlat, flat);
10795
+ gl.uniform2f(u.uOff0, offsets[0][0], offsets[0][1]);
10796
+ gl.uniform2f(u.uOff1, offsets[1][0], offsets[1][1]);
10797
+ gl.uniform2f(u.uOff2, offsets[2][0], offsets[2][1]);
10798
+ gl.uniform3f(u.uAccent, color[0], color[1], color[2]);
10799
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
10800
+ },
10801
+ dispose() {
10802
+ try {
10803
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
10804
+ } catch {}
10805
+ }
10806
+ };
10807
+ }
10808
+ //#endregion
10809
+ //#region src/client-voice-call.jsx
10810
+ const Bars = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
10811
+ width: "20",
10812
+ height: "20",
10813
+ viewBox: "0 0 20 20",
10814
+ fill: "none",
10815
+ "aria-hidden": "true",
10816
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
10817
+ d: "M4 8.5v3M7 5.5v9M10 3v14M13 6v8M16 8.5v3",
10818
+ stroke: "currentColor",
10819
+ strokeWidth: "2.2",
10820
+ strokeLinecap: "round"
10821
+ })
10822
+ });
10823
+ const Cross = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
10824
+ width: "14",
10825
+ height: "14",
10826
+ viewBox: "0 0 14 14",
10827
+ fill: "none",
10828
+ "aria-hidden": "true",
10829
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
10830
+ d: "M3 3l8 8M11 3l-8 8",
10831
+ stroke: "currentColor",
10832
+ strokeWidth: "2",
10833
+ strokeLinecap: "round"
10834
+ })
10835
+ });
10836
+ const Mic = ({ off }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
10837
+ width: "18",
10838
+ height: "18",
10839
+ viewBox: "0 0 18 18",
10840
+ fill: "none",
10841
+ "aria-hidden": "true",
10842
+ children: [
10843
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
10844
+ x: "6.2",
10845
+ y: "1.8",
10846
+ width: "5.6",
10847
+ height: "9",
10848
+ rx: "2.8",
10849
+ stroke: "currentColor",
10850
+ strokeWidth: "1.5"
10851
+ }),
10852
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
10853
+ d: "M3.5 8.4a5.5 5.5 0 0 0 11 0M9 13.9v2.3",
10854
+ stroke: "currentColor",
10855
+ strokeWidth: "1.5",
10856
+ strokeLinecap: "round"
10857
+ }),
10858
+ off ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
10859
+ d: "M2.8 15.2 15.2 2.8",
10860
+ stroke: "currentColor",
10861
+ strokeWidth: "1.7",
10862
+ strokeLinecap: "round"
10863
+ }) : null
10864
+ ]
10865
+ });
10866
+ const Speaker = ({ off }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
10867
+ width: "18",
10868
+ height: "18",
10869
+ viewBox: "0 0 18 18",
10870
+ fill: "none",
10871
+ "aria-hidden": "true",
10872
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
10873
+ d: "M3 7h2.6L9.5 3.8v10.4L5.6 11H3z",
10874
+ stroke: "currentColor",
10875
+ strokeWidth: "1.5",
10876
+ strokeLinejoin: "round"
10877
+ }), off ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
10878
+ d: "M12.6 6.6l3.2 4.8M15.8 6.6l-3.2 4.8",
10879
+ stroke: "currentColor",
10880
+ strokeWidth: "1.5",
10881
+ strokeLinecap: "round"
10882
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
10883
+ d: "M12.3 6.5a3.6 3.6 0 0 1 0 5M14.4 4.6a6.4 6.4 0 0 1 0 8.8",
10884
+ stroke: "currentColor",
10885
+ strokeWidth: "1.5",
10886
+ strokeLinecap: "round"
10887
+ })]
10888
+ });
10889
+ /**
10890
+ * What a call looks like while it runs, drawn like Codex's own voice chat: above a composer that sits at the bottom of the
10891
+ * conversation (below it on the empty start page) there is a small stage with the last spoken lines, what DSH is doing
10892
+ * and the orb, which swells with the person's voice and with the model's. While the call connects, the orb is a pulsing dot.
10893
+ */
10894
+ function VoiceStage({ call, connecting, rpc, sessionId, anchor, accent, t }) {
10895
+ const orb = (0, react.useRef)(null);
10896
+ const canvas = (0, react.useRef)(null);
10897
+ const working = (0, react.useRef)(false);
10898
+ const closing = (0, react.useRef)(false);
10899
+ const frame = (0, react.useRef)(null);
10900
+ const [status, setStatus] = (0, react.useState)({
10901
+ working: false,
10902
+ lines: []
10903
+ });
10904
+ working.current = status.working;
10905
+ closing.current = status.closing === true;
10906
+ const [mode, setMode] = (0, react.useState)("idle");
10907
+ const [box, setBox] = (0, react.useState)();
10908
+ (0, react.useEffect)(() => {
10909
+ let raf;
10910
+ const follow = () => {
10911
+ const rect = anchor.getBoundingClientRect();
10912
+ setBox((current) => current && Math.abs(current.left - rect.left) < .5 && Math.abs(current.top - rect.top) < .5 && Math.abs(current.width - rect.width) < .5 ? current : {
10913
+ left: rect.left,
10914
+ top: rect.top,
10915
+ bottom: rect.bottom,
10916
+ width: rect.width,
10917
+ docked: rect.bottom > window.innerHeight - 180
10918
+ });
10919
+ raf = requestAnimationFrame(follow);
10920
+ };
10921
+ follow();
10922
+ return () => cancelAnimationFrame(raf);
10923
+ }, [anchor]);
10924
+ (0, react.useEffect)(() => {
10925
+ if (!call) return void 0;
10926
+ const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
10927
+ const painter = canvas.current ? createOrb(canvas.current, accent) : void 0;
10928
+ const started = performance.now();
10929
+ let level = 0;
10930
+ let last = "";
10931
+ let closeSince = 0;
10932
+ let quietSince = 0;
10933
+ const tick = () => {
10934
+ const { mic, assistant } = call.levels();
10935
+ if (closing.current) {
10936
+ const now = performance.now();
10937
+ closeSince ||= now;
10938
+ if (assistant > .03) quietSince = 0;
10939
+ else quietSince ||= now;
10940
+ if (now - closeSince > 15e3 || now - closeSince > 1800 && now - quietSince > 1300) {
10941
+ closing.current = false;
10942
+ call.stop();
10943
+ return;
10944
+ }
10945
+ }
10946
+ const target = Math.max(mic, assistant);
10947
+ level += (target - level) * (target > level ? .35 : .12);
10948
+ orb.current?.style.setProperty("--level", reduce ? "0" : level.toFixed(3));
10949
+ painter?.draw((performance.now() - started) / 1e3, {
10950
+ mic,
10951
+ assistant,
10952
+ working: working.current,
10953
+ reduce
10954
+ });
10955
+ const next = assistant > .05 ? "speaking" : mic > .05 ? "listening" : "idle";
10956
+ if (next !== last) {
10957
+ last = next;
10958
+ setMode(next);
10959
+ }
10960
+ frame.current = requestAnimationFrame(tick);
10961
+ };
10962
+ tick();
10963
+ return () => {
10964
+ cancelAnimationFrame(frame.current);
10965
+ painter?.dispose();
10966
+ };
10967
+ }, [call]);
10968
+ (0, react.useEffect)(() => {
10969
+ if (!call) return void 0;
10970
+ let stopped = false;
10971
+ let seen = false;
10972
+ const poll = async () => {
10973
+ try {
10974
+ const result = await rpc.call(CHANNEL, "voice/status", { sessionId });
10975
+ if (!stopped && result.ok) {
10976
+ setStatus(result.value);
10977
+ if (result.value.active) seen = true;
10978
+ else if (seen) {
10979
+ stopped = true;
10980
+ call.stop();
10981
+ }
10982
+ }
10983
+ } catch {}
10984
+ };
10985
+ poll();
10986
+ const timer = setInterval(poll, 900);
10987
+ return () => {
10988
+ stopped = true;
10989
+ clearInterval(timer);
10990
+ };
10991
+ }, [
10992
+ call,
10993
+ rpc,
10994
+ sessionId
10995
+ ]);
10996
+ if (!box) return null;
10997
+ const label = connecting ? t("voiceConnecting") : status.working ? t("voiceWorking") : mode === "speaking" ? t("voiceSpeaking") : t("voiceListening");
10998
+ const lines = status.lines.slice(-2);
10999
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11000
+ className: "codexVoiceStage",
11001
+ role: "status",
11002
+ "aria-live": "polite",
11003
+ "data-side": box.docked ? "above" : "below",
11004
+ style: {
11005
+ left: box.left + box.width / 2,
11006
+ top: box.docked ? box.top : box.bottom,
11007
+ "--voice-accent": accent
11008
+ },
11009
+ children: [
11010
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
11011
+ className: "codexVoiceLines",
11012
+ children: lines.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
11013
+ "data-role": line.role,
11014
+ "data-old": index < lines.length - 1 || void 0,
11015
+ children: line.text
11016
+ }, `${index}-${line.text.slice(0, 12)}`))
11017
+ }),
11018
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
11019
+ className: "codexVoiceStatus",
11020
+ children: label
11021
+ }),
11022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11023
+ className: "codexVoiceOrbBox",
11024
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
11025
+ className: "codexVoiceDot",
11026
+ "data-gone": !connecting || void 0
11027
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
11028
+ className: "codexVoiceOrb",
11029
+ ref: orb,
11030
+ "data-ready": !connecting,
11031
+ "data-mode": status.working ? "working" : mode,
11032
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
11033
+ ref: canvas,
11034
+ width: 240,
11035
+ height: 240,
11036
+ "aria-hidden": "true"
11037
+ })
11038
+ })]
11039
+ })
11040
+ ]
11041
+ }), document.body);
11042
+ }
11043
+ /**
11044
+ * The composer's voice button. It sits exactly on the send button while there is nothing to send (the send button is
11045
+ * disabled) and gives it back as soon as there is text, so the one place always does the likely thing. During a call
11046
+ * it becomes the end button and, where DSH's dictation button is, a speaker and a microphone switch appear.
11047
+ */
11048
+ function VoiceCallButton({ sessionId, rpc, preference, t }) {
11049
+ const snapshot = usePreferenceSnapshot(preference);
11050
+ const marker = (0, react.useRef)(null);
11051
+ const call = (0, react.useRef)(null);
11052
+ const [liveCall, setLiveCall] = (0, react.useState)(null);
11053
+ const accent = (0, react.useRef)("");
11054
+ const button = (0, react.useRef)(null);
11055
+ const [place, setPlace] = (0, react.useState)();
11056
+ const [state, setState] = (0, react.useState)("idle");
11057
+ const [problem, setProblem] = (0, react.useState)("");
11058
+ const [micOff, setMicOff] = (0, react.useState)(false);
11059
+ const [speakerOff, setSpeakerOff] = (0, react.useState)(false);
11060
+ const enabled = snapshot.status === "ready" && snapshot.voiceCall === "on";
11061
+ (0, react.useLayoutEffect)(() => {
11062
+ if (!enabled) {
11063
+ setPlace(void 0);
11064
+ return;
11065
+ }
11066
+ const trailing = marker.current?.closest("[data-slot]")?.parentElement?.parentElement;
11067
+ if (!trailing) return void 0;
11068
+ const wasStatic = getComputedStyle(trailing).position === "static";
11069
+ if (wasStatic) trailing.style.position = "relative";
11070
+ const watcher = new MutationObserver(() => measure());
11071
+ const measure = () => {
11072
+ const send = [...trailing.children].filter((child) => child.tagName === "BUTTON" && !child.classList.contains("codexVoiceCall")).at(-1);
11073
+ if (!send) {
11074
+ setPlace(void 0);
11075
+ return;
11076
+ }
11077
+ const host = trailing.getBoundingClientRect();
11078
+ const box = send.getBoundingClientRect();
11079
+ const ratio = window.devicePixelRatio || 1;
11080
+ const snap = (value) => Math.round(value * ratio) / ratio;
11081
+ setPlace({
11082
+ host: trailing,
11083
+ activity: trailing.querySelector("[data-slot=\"conversation.input.activity\"]")?.parentElement ?? null,
11084
+ left: snap(box.left - host.left - trailing.clientLeft),
11085
+ top: snap(box.top - host.top - trailing.clientTop),
11086
+ width: snap(box.width),
11087
+ height: snap(box.width),
11088
+ idle: send.disabled,
11089
+ className: send.className
11090
+ });
11091
+ };
11092
+ measure();
11093
+ watcher.observe(trailing, {
11094
+ subtree: true,
11095
+ childList: true,
11096
+ attributes: true,
11097
+ attributeFilter: [
11098
+ "disabled",
11099
+ "aria-label",
11100
+ "class"
11101
+ ]
11102
+ });
11103
+ const resizer = new ResizeObserver(measure);
11104
+ resizer.observe(trailing);
11105
+ return () => {
11106
+ watcher.disconnect();
11107
+ resizer.disconnect();
11108
+ if (wasStatic) trailing.style.position = "";
11109
+ };
11110
+ }, [enabled]);
11111
+ const live = state === "live";
11112
+ (0, react.useEffect)(() => {
11113
+ const activity = place?.activity;
11114
+ if (!activity || !live) return void 0;
11115
+ activity.setAttribute("data-voice-live", "");
11116
+ return () => activity.removeAttribute("data-voice-live");
11117
+ }, [place?.activity, live]);
11118
+ (0, react.useEffect)(() => () => {
11119
+ call.current?.stop();
11120
+ }, []);
11121
+ (0, react.useEffect)(() => {
11122
+ if (!problem) return void 0;
11123
+ const timer = setTimeout(() => setProblem(""), 6e3);
11124
+ return () => clearTimeout(timer);
11125
+ }, [problem]);
11126
+ const start = async () => {
11127
+ setProblem("");
11128
+ accent.current = button.current ? getComputedStyle(button.current).backgroundColor : "";
11129
+ setMicOff(false);
11130
+ setSpeakerOff(false);
11131
+ setState("connecting");
11132
+ try {
11133
+ call.current = await openVoiceCall({
11134
+ rpc,
11135
+ sessionId,
11136
+ onEnd: () => {
11137
+ call.current = null;
11138
+ setLiveCall(null);
11139
+ setState("idle");
11140
+ }
11141
+ });
11142
+ setLiveCall(call.current);
11143
+ setState("live");
11144
+ } catch (error) {
11145
+ call.current = null;
11146
+ setLiveCall(null);
11147
+ setState("idle");
11148
+ setProblem(error?.name === "NotAllowedError" ? t("voiceMicBlocked") : t("voiceFailed"));
11149
+ }
11150
+ };
11151
+ const click = () => {
11152
+ if (state === "live") call.current?.stop();
11153
+ else if (state === "idle") start();
11154
+ };
11155
+ const anchor = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
11156
+ ref: marker,
11157
+ hidden: true
11158
+ });
11159
+ if (!enabled || !place) return anchor;
11160
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
11161
+ anchor,
11162
+ state !== "idle" || place.idle ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
11163
+ ref: button,
11164
+ type: "button",
11165
+ className: `${place.className} codexVoiceCall`,
11166
+ "data-state": state,
11167
+ onClick: click,
11168
+ disabled: state === "connecting",
11169
+ "aria-label": t(live ? "voiceEnd" : "voiceStart"),
11170
+ title: t(live ? "voiceEnd" : "voiceStart"),
11171
+ style: {
11172
+ left: place.left,
11173
+ top: place.top,
11174
+ width: place.width,
11175
+ height: place.height
11176
+ },
11177
+ children: live ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Cross, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Bars, {})
11178
+ }), problem ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
11179
+ className: "codexVoiceCallNote",
11180
+ role: "alert",
11181
+ style: { right: 0 },
11182
+ children: problem
11183
+ }) : null] }), place.host) : null,
11184
+ live && place.activity && liveCall ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
11185
+ className: "codexVoiceTools",
11186
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
11187
+ type: "button",
11188
+ className: "codexVoiceTool",
11189
+ "aria-pressed": speakerOff,
11190
+ "aria-label": t(speakerOff ? "voiceUnmuteSpeaker" : "voiceMuteSpeaker"),
11191
+ title: t(speakerOff ? "voiceUnmuteSpeaker" : "voiceMuteSpeaker"),
11192
+ onClick: () => {
11193
+ liveCall.setOutputMuted(!speakerOff);
11194
+ setSpeakerOff(!speakerOff);
11195
+ },
11196
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Speaker, { off: speakerOff })
11197
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
11198
+ type: "button",
11199
+ className: "codexVoiceTool",
11200
+ "aria-pressed": micOff,
11201
+ "aria-label": t(micOff ? "voiceUnmute" : "voiceMute"),
11202
+ title: t(micOff ? "voiceUnmute" : "voiceMute"),
11203
+ onClick: () => {
11204
+ liveCall.setMuted(!micOff);
11205
+ setMicOff(!micOff);
11206
+ },
11207
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Mic, { off: micOff })
11208
+ })]
11209
+ }), place.activity) : null,
11210
+ state !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceStage, {
11211
+ call: liveCall,
11212
+ connecting: state === "connecting",
11213
+ rpc,
11214
+ sessionId,
11215
+ anchor: place.host.parentElement?.parentElement ?? place.host,
11216
+ accent: accent.current,
11217
+ t
11218
+ }) : null
11219
+ ] });
11220
+ }
11221
+ /** One settings row: the label on the left, a menu in the same style as the context selector on the right. */
11222
+ function ChoiceRow({ label, items, selectedId, disabled, onSelect, trailing = null }) {
11223
+ const [open, setOpen] = (0, react.useState)(false);
11224
+ const selected = items.flatMap((item) => [item, ...item.submenu ?? []]).find((item) => item.id === selectedId)?.label ?? items[0].label;
11225
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11226
+ className: "codexSubscriptionPreference",
11227
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
11228
+ className: "codexSubscriptionPreferenceCopy",
11229
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
11230
+ className: "codexSubscriptionPreferenceLabel",
11231
+ children: label
11232
+ })
11233
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11234
+ className: "codexVoiceChoice",
11235
+ children: [trailing, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Menu, {
11236
+ open,
11237
+ items,
11238
+ selectedId,
11239
+ onSelect: (value) => {
11240
+ setOpen(false);
11241
+ onSelect(value);
11242
+ },
11243
+ onClose: () => setOpen(false),
11244
+ align: "end",
11245
+ side: "bottom",
11246
+ portal: true,
11247
+ compact: true,
11248
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
11249
+ className: "codexSubscriptionContextTrigger",
11250
+ type: "button",
11251
+ "aria-label": label,
11252
+ "aria-haspopup": "menu",
11253
+ "aria-expanded": open,
11254
+ disabled,
11255
+ onClick: () => setOpen((value) => !value),
11256
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: selected }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDownOutline14, {})]
11257
+ })
11258
+ })]
11259
+ })]
11260
+ });
11261
+ }
11262
+ /** Settings for the composer's voice button: on or off, the voice (with a short sample to listen to) and the answer language. */
11263
+ function VoiceCallCard({ rpc, preference, snapshot, t }) {
11264
+ const audio = (0, react.useRef)();
11265
+ const alive = (0, react.useRef)(false);
11266
+ const [offered, setOffered] = (0, react.useState)();
11267
+ const [sample, setSample] = (0, react.useState)({
11268
+ state: "idle",
11269
+ note: ""
11270
+ });
11271
+ const on = snapshot.voiceCall === "on";
11272
+ (0, react.useEffect)(() => {
11273
+ alive.current = true;
11274
+ return () => {
11275
+ alive.current = false;
11276
+ audio.current?.pause();
11277
+ };
11278
+ }, []);
11279
+ (0, react.useEffect)(() => {
11280
+ if (!on) return;
11281
+ (async () => {
11282
+ try {
11283
+ const result = await rpc.call(CHANNEL, "voice/catalog", {});
11284
+ if (alive.current && result.ok) setOffered(result.value.voices);
11285
+ } catch {}
11286
+ })();
11287
+ }, [on]);
11288
+ /** Play a short clip of a voice; picking a voice plays it too, so it can be heard before it is used. */
11289
+ const playVoice = async (voice) => {
11290
+ audio.current?.pause();
11291
+ setSample({
11292
+ state: "loading",
11293
+ note: ""
11294
+ });
11295
+ try {
11296
+ const result = await rpc.call(CHANNEL, "voice/preview", { voice });
11297
+ if (!alive.current) return;
11298
+ if (!result.ok) {
11299
+ setSample({
11300
+ state: "missing",
11301
+ note: ""
11302
+ });
11303
+ return;
11304
+ }
11305
+ const clip = new Audio(result.value.audio);
11306
+ audio.current = clip;
11307
+ clip.onended = () => {
11308
+ if (alive.current) setSample((current) => ({
11309
+ ...current,
11310
+ state: "idle"
11311
+ }));
11312
+ };
11313
+ setSample({
11314
+ state: "playing",
11315
+ note: [result.value.description, result.value.accent].filter(Boolean).join(" · ")
11316
+ });
11317
+ await clip.play();
11318
+ } catch {
11319
+ if (alive.current) setSample({
11320
+ state: "missing",
11321
+ note: ""
11322
+ });
11323
+ }
11324
+ };
11325
+ const listed = VOICE_VOICES.filter((value) => offered ? offered.includes(value) : VOICE_VOICES.indexOf(value) < 9);
11326
+ const others = VOICE_VOICES.filter((value) => !listed.includes(value));
11327
+ const voices = [...listed.map((value) => ({
11328
+ id: value,
11329
+ label: value
11330
+ })), ...others.length === 0 ? [] : [{
11331
+ type: "separator",
11332
+ id: "more-line"
11333
+ }, {
11334
+ id: "more",
11335
+ label: t("voiceMore"),
11336
+ submenu: others.map((value) => ({
11337
+ id: value,
11338
+ label: value
11339
+ }))
11340
+ }]];
11341
+ const languages = VOICE_LANGUAGES.map((value) => ({
11342
+ id: value,
11343
+ label: value === "auto" ? t("voiceAuto") : t(`voiceLang_${value}`)
11344
+ }));
11345
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
11346
+ className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
11347
+ "aria-label": t("voiceTitle"),
11348
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11349
+ className: "codexSubscriptionPreference",
11350
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11351
+ className: "codexSubscriptionPreferenceCopy",
11352
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
11353
+ className: "codexSubscriptionPreferenceLabel",
11354
+ children: [
11355
+ t("voiceTitle"),
11356
+ " ",
11357
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
11358
+ ]
11359
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
11360
+ t,
11361
+ children: t("voiceHint")
11362
+ })]
11363
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
11364
+ className: "codexSubscriptionQuotaModes",
11365
+ role: "radiogroup",
11366
+ "aria-label": t("voiceTitle"),
11367
+ "aria-busy": snapshot.saving || void 0,
11368
+ children: ["off", "on"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
11369
+ className: "codexSubscriptionQuotaMode",
11370
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
11371
+ type: "radio",
11372
+ name: "codex-voice-call",
11373
+ checked: snapshot.voiceCall === value,
11374
+ disabled: !snapshot.writable,
11375
+ onChange: () => {
11376
+ preference.set({ [VOICE_CALL_FIELD]: value });
11377
+ }
11378
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`remote_${value}`) })]
11379
+ }, value))
11380
+ })]
11381
+ }), on ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
11382
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
11383
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChoiceRow, {
11384
+ label: t("voiceVoice"),
11385
+ disabled: !snapshot.writable,
11386
+ selectedId: snapshot.voiceVoice,
11387
+ items: voices,
11388
+ onSelect: (value) => {
11389
+ if (value === "more") return;
11390
+ preference.set({ [VOICE_VOICE_FIELD]: value });
11391
+ playVoice(value);
11392
+ },
11393
+ trailing: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
11394
+ className: "codexSubscriptionContextTrigger",
11395
+ type: "button",
11396
+ disabled: sample.state === "loading",
11397
+ onClick: () => {
11398
+ playVoice(snapshot.voiceVoice);
11399
+ },
11400
+ children: sample.state === "loading" ? t("voiceLoading") : t("voicePlay")
11401
+ })
11402
+ }),
11403
+ sample.state === "playing" && sample.note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
11404
+ className: "codexSubscriptionHelp",
11405
+ role: "status",
11406
+ children: sample.note
11407
+ }) : null,
11408
+ sample.state === "missing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
11409
+ className: "codexSubscriptionHelp",
11410
+ role: "status",
11411
+ children: t("voiceNoSample")
11412
+ }) : null,
11413
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChoiceRow, {
11414
+ label: t("voiceLanguage"),
11415
+ disabled: !snapshot.writable,
11416
+ selectedId: snapshot.voiceLanguage,
11417
+ items: languages,
11418
+ onSelect: (value) => {
11419
+ preference.set({ [VOICE_LANGUAGE_FIELD]: value });
11420
+ }
11421
+ })
11422
+ ] }) : null]
11423
+ });
11424
+ }
11425
+ //#endregion
9982
11426
  //#region src/image-setting-groups.js
9983
11427
  const IMAGE_SETTING_GROUPS = Object.freeze({
9984
11428
  imageCapability: ["imageGeneration", "imageEditing"],
@@ -9998,7 +11442,7 @@ window.__ModuleLoader__.load({
9998
11442
  }
9999
11443
  //#endregion
10000
11444
  //#region src/image-preferences.jsx
10001
- function ImageChoice({ label, hint, value, text, items, disabled, onSelect }) {
11445
+ function ImageChoice({ t, label, hint, value, text, items, disabled, onSelect }) {
10002
11446
  const [open, setOpen] = (0, react.useState)(false);
10003
11447
  const anchor = (0, react.useRef)(null);
10004
11448
  (0, react.useEffect)(() => {
@@ -10017,8 +11461,8 @@ window.__ModuleLoader__.load({
10017
11461
  className: "codexSubscriptionPreference codexImagePreference",
10018
11462
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10019
11463
  className: "codexSubscriptionPreferenceCopy",
10020
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), hint ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10021
- className: "codexSubscriptionPreferenceHint",
11464
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), hint ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
11465
+ t,
10022
11466
  children: hint
10023
11467
  }) : null]
10024
11468
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Menu, {
@@ -10072,6 +11516,7 @@ window.__ModuleLoader__.load({
10072
11516
  Object.keys(IMAGE_SETTING_GROUPS).filter((group) => group.startsWith("sketch") === sketch).map((group) => {
10073
11517
  const value = imageGroupValue(snapshot, group);
10074
11518
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
11519
+ t,
10075
11520
  label: t(sketch ? `${group}Setting` : group),
10076
11521
  hint: t(`${group}Hint`),
10077
11522
  value,
@@ -10089,6 +11534,7 @@ window.__ModuleLoader__.load({
10089
11534
  !sketch ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10090
11535
  className: "codexImageDefaultsGroup",
10091
11536
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
11537
+ t,
10092
11538
  label: t("imageModel"),
10093
11539
  value: snapshot.imageModel,
10094
11540
  text: modelLabel(snapshot.imageModel),
@@ -10104,6 +11550,7 @@ window.__ModuleLoader__.load({
10104
11550
  });
10105
11551
  }
10106
11552
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
11553
+ t,
10107
11554
  label: t("imageQuality"),
10108
11555
  value: snapshot.imageQuality,
10109
11556
  text: t(`imageQuality_${snapshot.imageQuality}`),
@@ -10117,8 +11564,9 @@ window.__ModuleLoader__.load({
10117
11564
  }
10118
11565
  })]
10119
11566
  }) : null,
10120
- !sketch && snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10121
- className: "codexSubscriptionPreferenceHint",
11567
+ !sketch && snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
11568
+ t,
11569
+ as: "p",
10122
11570
  children: t("imageModelHint")
10123
11571
  }) : null
10124
11572
  ]
@@ -10176,8 +11624,8 @@ window.__ModuleLoader__.load({
10176
11624
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10177
11625
  className: "codexSubscriptionPreferenceLabel",
10178
11626
  children: t("quotaAlerts")
10179
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10180
- className: "codexSubscriptionPreferenceHint",
11627
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
11628
+ t,
10181
11629
  children: t("quotaAlertsHint")
10182
11630
  })]
10183
11631
  }), choices("quotaAlerts", QUOTA_ALERT_MODES)]
@@ -10211,8 +11659,9 @@ window.__ModuleLoader__.load({
10211
11659
  children: t("searchMode")
10212
11660
  }), choices("searchMode", SEARCH_MODES)]
10213
11661
  }),
10214
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10215
- className: "codexSubscriptionPreferenceHint",
11662
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
11663
+ t,
11664
+ as: "p",
10216
11665
  children: t("searchModeHint")
10217
11666
  }),
10218
11667
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -10231,8 +11680,8 @@ window.__ModuleLoader__.load({
10231
11680
  if (event.key === "Enter") event.currentTarget.blur();
10232
11681
  }
10233
11682
  }),
10234
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10235
- className: "codexSubscriptionPreferenceHint",
11683
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
11684
+ t,
10236
11685
  children: t("searchDomainsHint")
10237
11686
  })
10238
11687
  ]
@@ -10318,8 +11767,8 @@ window.__ModuleLoader__.load({
10318
11767
  className: "codexSubscriptionPreferenceLabel",
10319
11768
  children: t("defaultModelTitle")
10320
11769
  }),
10321
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10322
- className: "codexSubscriptionPreferenceHint",
11770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
11771
+ t,
10323
11772
  children: hint
10324
11773
  }),
10325
11774
  snapshot.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -12685,6 +14134,7 @@ window.__ModuleLoader__.load({
12685
14134
  const STATUS_KEYS = {
12686
14135
  connected: "remoteConnected",
12687
14136
  connecting: "remoteConnecting",
14137
+ waiting: "remoteWaiting",
12688
14138
  error: "remoteError",
12689
14139
  stopped: "remoteStopped"
12690
14140
  };
@@ -12838,8 +14288,8 @@ window.__ModuleLoader__.load({
12838
14288
  " ",
12839
14289
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
12840
14290
  ]
12841
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
12842
- className: "codexSubscriptionPreferenceHint",
14291
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
14292
+ t,
12843
14293
  children: t("remoteHint")
12844
14294
  })]
12845
14295
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -12867,9 +14317,9 @@ window.__ModuleLoader__.load({
12867
14317
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
12868
14318
  role: "status",
12869
14319
  children: [
12870
- t(STATUS_KEYS[status] ?? "remoteConnecting"),
14320
+ status === "connected" && state?.phones > 0 ? t("remotePhones").replace("{count}", String(state.phones)) : t(STATUS_KEYS[status] ?? "remoteConnecting"),
12871
14321
  status === "error" && state?.lastError ? ` · ${state.lastError}` : "",
12872
- state?.phones > 0 ? ` · ${t("remotePhones").replace("{count}", String(state.phones))}` : ""
14322
+ status !== "connected" && state?.phones > 0 ? ` · ${t("remotePhones").replace("{count}", String(state.phones))}` : ""
12873
14323
  ]
12874
14324
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
12875
14325
  type: "button",
@@ -12881,6 +14331,11 @@ window.__ModuleLoader__.load({
12881
14331
  children: t(pairing ? "remotePairAgain" : "remotePair")
12882
14332
  })]
12883
14333
  }),
14334
+ status === "waiting" && state?.lastError === "conflict" && state?.reconnects >= 6 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14335
+ role: "status",
14336
+ className: "codexSubscriptionHelp",
14337
+ children: t("remoteConflictLong")
14338
+ }) : null,
12884
14339
  failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
12885
14340
  role: "alert",
12886
14341
  className: "codexSubscriptionHelp",
@@ -13001,8 +14456,8 @@ window.__ModuleLoader__.load({
13001
14456
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13002
14457
  className: "codexSubscriptionPreferenceLabel",
13003
14458
  children: t("quickQuotaSetting")
13004
- }), snapshot.quickQuotaMode === "forecast" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13005
- className: "codexSubscriptionPreferenceHint",
14459
+ }), snapshot.quickQuotaMode === "forecast" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
14460
+ t,
13006
14461
  children: t("quickQuotaForecastHint")
13007
14462
  }) : null]
13008
14463
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -13034,8 +14489,8 @@ window.__ModuleLoader__.load({
13034
14489
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13035
14490
  className: "codexSubscriptionPreferenceLabel",
13036
14491
  children: t("autoQuotaRetry")
13037
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13038
- className: "codexSubscriptionPreferenceHint",
14492
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
14493
+ t,
13039
14494
  children: t("autoQuotaRetryHint")
13040
14495
  })]
13041
14496
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
@@ -13073,8 +14528,8 @@ window.__ModuleLoader__.load({
13073
14528
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13074
14529
  className: "codexSubscriptionPreferenceLabel",
13075
14530
  children: t("searchTitle")
13076
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13077
- className: "codexSubscriptionPreferenceHint",
14531
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
14532
+ t,
13078
14533
  children: t(hint)
13079
14534
  })]
13080
14535
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -13106,8 +14561,8 @@ window.__ModuleLoader__.load({
13106
14561
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13107
14562
  className: "codexSubscriptionPreferenceLabel",
13108
14563
  children: t("inputImageDetail")
13109
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13110
- className: "codexSubscriptionPreferenceHint",
14564
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
14565
+ t,
13111
14566
  children: t("inputImageDetailHint")
13112
14567
  })]
13113
14568
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -13141,8 +14596,8 @@ window.__ModuleLoader__.load({
13141
14596
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13142
14597
  className: "codexSubscriptionPreferenceLabel",
13143
14598
  children: t("reasoningSummary")
13144
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13145
- className: "codexSubscriptionPreferenceHint",
14599
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
14600
+ t,
13146
14601
  children: t("reasoningSummaryHint")
13147
14602
  })]
13148
14603
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -13176,8 +14631,8 @@ window.__ModuleLoader__.load({
13176
14631
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13177
14632
  className: "codexSubscriptionPreferenceLabel",
13178
14633
  children: t("retryPolicy")
13179
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13180
- className: "codexSubscriptionPreferenceHint",
14634
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
14635
+ t,
13181
14636
  children: t("retryPolicyHint")
13182
14637
  })]
13183
14638
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -13205,39 +14660,44 @@ window.__ModuleLoader__.load({
13205
14660
  const snapshot = usePreferenceSnapshot(preference);
13206
14661
  const writable = snapshot.status === "ready" && snapshot.writable === true;
13207
14662
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13208
- className: "codexSubscriptionPreference",
13209
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13210
- className: "codexSubscriptionPreferenceCopy",
13211
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
13212
- className: "codexSubscriptionPreferenceLabel",
13213
- children: [
13214
- t("connectors"),
13215
- " ",
13216
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
13217
- ]
13218
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13219
- className: "codexSubscriptionPreferenceHint",
13220
- children: t("connectorsHint")
13221
- })]
13222
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13223
- className: "codexSubscriptionQuotaModes",
13224
- "data-saving": snapshot.saving || void 0,
13225
- "aria-busy": snapshot.saving || void 0,
13226
- role: "radiogroup",
13227
- "aria-label": t("connectors"),
13228
- children: CONNECTOR_MODES.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
13229
- className: "codexSubscriptionQuotaMode",
13230
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
13231
- type: "radio",
13232
- name: "codex-connectors",
13233
- checked: snapshot.connectors === value,
13234
- disabled: !writable,
13235
- onChange: () => {
13236
- preference.set({ [CONNECTORS_FIELD]: value });
13237
- }
13238
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`connectors_${value}`) })]
13239
- }, value))
13240
- })]
14663
+ className: "codexSubscriptionPreference codexSubscriptionConnectors",
14664
+ children: [
14665
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14666
+ className: "codexSubscriptionPreferenceCopy",
14667
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14668
+ className: "codexSubscriptionPreferenceLabel",
14669
+ children: t("connectors")
14670
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
14671
+ t,
14672
+ children: t("connectorsHint")
14673
+ })]
14674
+ }),
14675
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14676
+ className: "codexSubscriptionQuotaModes",
14677
+ "data-saving": snapshot.saving || void 0,
14678
+ "aria-busy": snapshot.saving || void 0,
14679
+ role: "radiogroup",
14680
+ "aria-label": t("connectors"),
14681
+ children: CONNECTOR_MODES.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
14682
+ className: "codexSubscriptionQuotaMode",
14683
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
14684
+ type: "radio",
14685
+ name: "codex-connectors",
14686
+ checked: snapshot.connectors === value,
14687
+ disabled: !writable,
14688
+ onChange: () => {
14689
+ preference.set({ [CONNECTORS_FIELD]: value });
14690
+ }
14691
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`connectors_${value}`) })]
14692
+ }, value))
14693
+ }),
14694
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14695
+ className: "codexSubscriptionModeNote",
14696
+ "data-danger": snapshot.connectors === "write" || void 0,
14697
+ role: "status",
14698
+ children: t(`connectorsMode_${snapshot.connectors}`)
14699
+ })
14700
+ ]
13241
14701
  });
13242
14702
  }
13243
14703
  function StreamIdleTimeoutPreference({ preference, t }) {
@@ -13250,8 +14710,8 @@ window.__ModuleLoader__.load({
13250
14710
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13251
14711
  className: "codexSubscriptionPreferenceLabel",
13252
14712
  children: t("streamIdleTimeoutTitle")
13253
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13254
- className: "codexSubscriptionPreferenceHint",
14713
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
14714
+ t,
13255
14715
  children: t("streamIdleTimeoutHint")
13256
14716
  })]
13257
14717
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -13317,8 +14777,8 @@ window.__ModuleLoader__.load({
13317
14777
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13318
14778
  className: "codexSubscriptionPreferenceLabel",
13319
14779
  children: t("pickerModelsTitle")
13320
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13321
- className: "codexSubscriptionPreferenceHint",
14780
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
14781
+ t,
13322
14782
  children: t("pickerModelsHint")
13323
14783
  })]
13324
14784
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -13589,8 +15049,8 @@ window.__ModuleLoader__.load({
13589
15049
  " ",
13590
15050
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
13591
15051
  ]
13592
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13593
- className: "codexSubscriptionPreferenceHint",
15052
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
15053
+ t,
13594
15054
  children: t("connectionHint")
13595
15055
  })]
13596
15056
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -13633,8 +15093,8 @@ window.__ModuleLoader__.load({
13633
15093
  " ",
13634
15094
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
13635
15095
  ]
13636
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13637
- className: "codexSubscriptionPreferenceHint",
15096
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
15097
+ t,
13638
15098
  children: t("reviewModelHint")
13639
15099
  })]
13640
15100
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -13663,6 +15123,12 @@ window.__ModuleLoader__.load({
13663
15123
  snapshot,
13664
15124
  t
13665
15125
  }),
15126
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceCallCard, {
15127
+ rpc,
15128
+ preference,
15129
+ snapshot,
15130
+ t
15131
+ }),
13666
15132
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
13667
15133
  className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
13668
15134
  "aria-label": t("compactionTitle"),
@@ -13677,8 +15143,8 @@ window.__ModuleLoader__.load({
13677
15143
  " ",
13678
15144
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
13679
15145
  ]
13680
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13681
- className: "codexSubscriptionPreferenceHint",
15146
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
15147
+ t,
13682
15148
  children: t("compactionHint")
13683
15149
  })]
13684
15150
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -13715,8 +15181,8 @@ window.__ModuleLoader__.load({
13715
15181
  " ",
13716
15182
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "Beta" })
13717
15183
  ]
13718
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13719
- className: "codexSubscriptionPreferenceHint",
15184
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
15185
+ t,
13720
15186
  children: t(snapshot.subagentBackendAvailable ? "subagentBackendHint" : "subagentBackendUnavailable")
13721
15187
  })]
13722
15188
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -14327,8 +15793,9 @@ window.__ModuleLoader__.load({
14327
15793
  }) : null
14328
15794
  ]
14329
15795
  }),
14330
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14331
- className: "codexSubscriptionPreferenceHint",
15796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, {
15797
+ t,
15798
+ as: "p",
14332
15799
  children: t(confirm ? "recoveryClearHint" : "recoveryHint")
14333
15800
  }),
14334
15801
  failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -14422,6 +15889,13 @@ window.__ModuleLoader__.load({
14422
15889
  };
14423
15890
  }
14424
15891
  //#endregion
15892
+ //#region src/diagnostic-log-format.js
15893
+ function formatLogLine(entry) {
15894
+ const stamp = new Date(entry.time).toISOString().replace("T", " ").slice(0, 19);
15895
+ const repeat = entry.count > 1 ? ` (x${entry.count})` : "";
15896
+ return `${stamp} ${entry.level.toUpperCase().padEnd(5)} [${entry.area}] ${entry.message}${entry.detail ? ` — ${entry.detail}` : ""}${repeat}`;
15897
+ }
15898
+ //#endregion
14425
15899
  //#region src/diagnostic-compact.js
14426
15900
  function compactDiagnostic(report) {
14427
15901
  const summary = report.summary ?? { findings: [] };
@@ -14452,17 +15926,29 @@ window.__ModuleLoader__.load({
14452
15926
  reason: item.reason,
14453
15927
  ...item.latest ? { latest: `${item.latest.action} ${item.latest.status}` } : { latest: "not-verified" }
14454
15928
  })),
15929
+ log: (report.log?.entries ?? []).slice(-150).map(formatLogLine),
14455
15930
  ...report.client?.status === "unknown" ? { hostCollected: false } : {}
14456
15931
  };
14457
15932
  }
14458
15933
  //#endregion
14459
15934
  //#region src/client-diagnostics.jsx
15935
+ const LEVELS = {
15936
+ all: [
15937
+ "info",
15938
+ "warn",
15939
+ "error"
15940
+ ],
15941
+ warn: ["warn", "error"],
15942
+ error: ["error"]
15943
+ };
14460
15944
  function DiagnosticsCard({ rpc, t, diagnostics }) {
14461
15945
  const [report, setReport] = (0, react.useState)();
14462
15946
  const [busy, setBusy] = (0, react.useState)(false);
14463
15947
  const [copied, setCopied] = (0, react.useState)(false);
14464
15948
  const [error, setError] = (0, react.useState)(false);
14465
15949
  const [copyError, setCopyError] = (0, react.useState)(false);
15950
+ const [level, setLevel] = (0, react.useState)("all");
15951
+ const [logCopied, setLogCopied] = (0, react.useState)(false);
14466
15952
  const load = () => {
14467
15953
  setBusy(true);
14468
15954
  setError(false);
@@ -14485,6 +15971,10 @@ window.__ModuleLoader__.load({
14485
15971
  setCopyError(false);
14486
15972
  Promise.resolve().then(() => navigator.clipboard.writeText(JSON.stringify(full ? report : compactDiagnostic(report), null, 2))).then(() => setCopied(true)).catch(() => setCopyError(true));
14487
15973
  };
15974
+ const lines = (report?.log?.entries ?? []).filter((entry) => LEVELS[level].includes(entry.level));
15975
+ const copyLog = () => {
15976
+ Promise.resolve().then(() => navigator.clipboard.writeText(lines.map(formatLogLine).join(String.fromCharCode(10)))).then(() => setLogCopied(true)).catch(() => setCopyError(true));
15977
+ };
14488
15978
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14489
15979
  className: "codexSubscriptionCard codexSubscriptionDiagnostics",
14490
15980
  children: [
@@ -14523,10 +16013,6 @@ window.__ModuleLoader__.load({
14523
16013
  })]
14524
16014
  }),
14525
16015
  report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
14526
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14527
- className: "codexSubscriptionHelp",
14528
- children: t("diagnosticsCoverageHint")
14529
- }),
14530
16016
  report.generatedAt ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
14531
16017
  className: "codexSubscriptionHelp",
14532
16018
  children: [
@@ -14554,22 +16040,54 @@ window.__ModuleLoader__.load({
14554
16040
  ] }, `${event.source}:${event.action}`)) }) : null
14555
16041
  ] }, item.code)) }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("diagnosticsNoRecordedFailure") })
14556
16042
  }),
14557
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
14558
- style: {
14559
- width: "100%",
14560
- fontSize: 12,
14561
- textAlign: "left",
14562
- borderSpacing: "0 8px"
14563
- },
14564
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
14565
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsCapability") }),
14566
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsReadiness") }),
14567
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsEvidence") })
14568
- ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: (report.inspection?.capabilities ?? []).map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
14569
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: t(`diagnosticCapability_${item.id}`) }),
14570
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [t(`diagnosticReason_${item.reason}`), item.blockedBy ? ` · ${t(`diagnosticCapability_${item.blockedBy}`)}` : ""] }),
14571
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [item.latest ? `${item.latest.action} · ${t(`diagnosticSource_${item.latest.source}`)} · ${t(`diagnosticOutcome_${item.latest.status}`)} · ${new Date(item.latest.observedAt).toLocaleTimeString(uiLocale())}` : t("diagnosticsUnverified"), item.unresolved?.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("diagnosticsUnresolved") }) : null] })
14572
- ] }, item.id)) })]
16043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
16044
+ className: "codexSubscriptionLogHead",
16045
+ children: [
16046
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("diagnosticsLog") }),
16047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16048
+ className: "codexSubscriptionQuotaModes",
16049
+ role: "radiogroup",
16050
+ "aria-label": t("diagnosticsLog"),
16051
+ children: Object.keys(LEVELS).map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
16052
+ className: "codexSubscriptionQuotaMode",
16053
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
16054
+ type: "radio",
16055
+ name: "codex-log-level",
16056
+ checked: level === value,
16057
+ onChange: () => {
16058
+ setLevel(value);
16059
+ setLogCopied(false);
16060
+ }
16061
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`logLevel_${value}`) })]
16062
+ }, value))
16063
+ }),
16064
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
16065
+ type: "button",
16066
+ variant: "outline",
16067
+ disabled: lines.length === 0,
16068
+ onClick: copyLog,
16069
+ children: logCopied ? t("logCopied") : t("logCopy")
16070
+ })
16071
+ ]
16072
+ }),
16073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
16074
+ className: "codexSubscriptionHelp",
16075
+ children: t("diagnosticsLogHint")
16076
+ }),
16077
+ lines.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
16078
+ className: "codexSubscriptionHelp",
16079
+ children: t("logEmpty")
16080
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
16081
+ className: "codexSubscriptionLog",
16082
+ tabIndex: 0,
16083
+ children: lines.slice().reverse().map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
16084
+ className: `codexSubscriptionLogLine codexSubscriptionLog_${entry.level}`,
16085
+ children: [
16086
+ formatLogLine(entry),
16087
+ entry.earlier ? ` (${t("logEarlier")})` : "",
16088
+ String.fromCharCode(10)
16089
+ ]
16090
+ }, entry.sequence))
14573
16091
  }),
14574
16092
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14575
16093
  className: "codexSubscriptionHelp",
@@ -14577,6 +16095,28 @@ window.__ModuleLoader__.load({
14577
16095
  }),
14578
16096
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [
14579
16097
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("diagnosticsDetails") }),
16098
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
16099
+ className: "codexSubscriptionHelp",
16100
+ children: t("diagnosticsCoverageHint")
16101
+ }),
16102
+ " ",
16103
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
16104
+ style: {
16105
+ width: "100%",
16106
+ fontSize: 12,
16107
+ textAlign: "left",
16108
+ borderSpacing: "0 8px"
16109
+ },
16110
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
16111
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsCapability") }),
16112
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsReadiness") }),
16113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsEvidence") })
16114
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: (report.inspection?.capabilities ?? []).map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
16115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: t(`diagnosticCapability_${item.id}`) }),
16116
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [t(`diagnosticReason_${item.reason}`), item.blockedBy ? ` · ${t(`diagnosticCapability_${item.blockedBy}`)}` : ""] }),
16117
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [item.latest ? `${item.latest.action} · ${t(`diagnosticSource_${item.latest.source}`)} · ${t(`diagnosticOutcome_${item.latest.status}`)} · ${new Date(item.latest.observedAt).toLocaleTimeString(uiLocale())}` : t("diagnosticsUnverified"), item.unresolved?.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("diagnosticsUnresolved") }) : null] })
16118
+ ] }, item.id)) })]
16119
+ }),
14580
16120
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
14581
16121
  type: "button",
14582
16122
  variant: "outline",
@@ -15769,6 +17309,7 @@ window.__ModuleLoader__.load({
15769
17309
  "uiConversation",
15770
17310
  "sessions"
15771
17311
  ];
17312
+ const ComposerRight = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodexComposerQuota, { ...props }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceCallButton, { ...props })] });
15772
17313
  function apply(ctx) {
15773
17314
  const diagnostics = createHostDiagnostics();
15774
17315
  ctx.effect(() => diagnostics.register(ctx.slots), "codex-subscription: host inspection");
@@ -15846,12 +17387,13 @@ window.__ModuleLoader__.load({
15846
17387
  order: 15,
15847
17388
  locale: NS,
15848
17389
  inject: (sessionId) => ({
17390
+ sessionId,
15849
17391
  preference,
15850
17392
  rpc,
15851
17393
  t,
15852
17394
  directory: modelDirectories.directoryFor(sessionId).store
15853
17395
  })
15854
- }, CodexComposerQuota));
17396
+ }, ComposerRight));
15855
17397
  scope.slots.inject("conversation.input.model", () => scope.slots.register({
15856
17398
  name: "conversation.input.model",
15857
17399
  priority: -10,