dsh-codex-subscription 3.0.0 → 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",
@@ -2672,7 +2677,7 @@ window.__ModuleLoader__.load({
2672
2677
  }
2673
2678
  return matrix;
2674
2679
  }
2675
- function parseColor(value) {
2680
+ function parseColor$1(value) {
2676
2681
  const text = String(value ?? "").trim().toLowerCase();
2677
2682
  if (!text || text === "none") return "none";
2678
2683
  if (text === "currentcolor") return "#000000";
@@ -3034,7 +3039,7 @@ window.__ModuleLoader__.load({
3034
3039
  if (!target || !/Gradient$/.test(target.name)) throw Error(`SVG paint ${reference} does not reference a gradient defined in the same SVG`);
3035
3040
  const stops = target.children.filter((child) => child.name === "stop");
3036
3041
  if (!stops.length) throw Error(`Gradient ${target.attrs.id} has no stops`);
3037
- 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) => [
3038
3043
  1,
3039
3044
  3,
3040
3045
  5
@@ -3047,7 +3052,7 @@ window.__ModuleLoader__.load({
3047
3052
  warnings.add(`Gradient ${target.attrs.id} was flattened to its average color; stack several bands for a real gradient.`);
3048
3053
  return `#${mean.map((value) => value.toString(16).padStart(2, "0")).join("")}`;
3049
3054
  };
3050
- const paint = (value) => String(value).startsWith("url(") ? gradientColor(value) : parseColor(value);
3055
+ const paint = (value) => String(value).startsWith("url(") ? gradientColor(value) : parseColor$1(value);
3051
3056
  const emit = (subpath, matrix, style, id) => {
3052
3057
  [subpath.start, ...subpath.curves.flatMap((curve) => [
3053
3058
  [curve[0], curve[1]],
@@ -5708,13 +5713,54 @@ window.__ModuleLoader__.load({
5708
5713
  subagentBackend_dsh: "DSH",
5709
5714
  subagentBackend_codex: "Codex",
5710
5715
  connectionTitle: "连接方式",
5716
+ diagnosticsLog: "运行日志",
5717
+ diagnosticsLogHint: "按时间记录运行中发生的事,错误和警告在最上面的筛选里。内容已去掉令牌、邮箱和路径。",
5718
+ logLevel_all: "全部",
5719
+ logLevel_warn: "警告和错误",
5720
+ logLevel_error: "仅错误",
5721
+ logEmpty: "这个级别暂时没有记录。",
5722
+ logCopy: "复制日志",
5723
+ logCopied: "已复制日志",
5724
+ logEarlier: "上次运行",
5711
5725
  remoteTitle: "手机远程控制",
5712
5726
  remote_off: "关闭",
5713
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: "Русский",
5714
5759
  remoteHint: "实验功能,默认关闭。开启后,这台电脑上的 DSH 会出现在 ChatGPT 手机 App 的 Codex 主机列表里,可用手机查看对话、发消息、切换模型和审批。需要订阅账号登录;电脑要保持开机和联网。",
5715
- remoteConnected: "已连接",
5760
+ remoteConnected: "电脑已连上 ChatGPT,还没有手机连接",
5716
5761
  remoteConnecting: "正在连接",
5717
5762
  remoteWaiting: "正在等待上一次连接释放,通常几分钟内自动连上",
5763
+ remoteConflictLong: "长时间连不上,通常是另一台电脑、另一份 DSH(或另一个窗口)也在用同一个 ChatGPT 账号开着手机远程,彼此在抢连接。请关闭其他地方的手机远程,再回到这里点“关闭”后重新“开启”。",
5718
5764
  remoteError: "连接失败,会自动重试",
5719
5765
  remoteStopped: "已停止",
5720
5766
  remotePaired: "已配对的手机",
@@ -5722,7 +5768,7 @@ window.__ModuleLoader__.load({
5722
5768
  remoteRevoke: "移除",
5723
5769
  remoteLastSeen: "最近 {date}",
5724
5770
  remotePhoneUnknown: "未命名手机",
5725
- remotePhones: "{count} 个手机连接",
5771
+ remotePhones: "手机已连接 {count} 部",
5726
5772
  remotePair: "显示配对码",
5727
5773
  remotePairAgain: "重新生成配对码",
5728
5774
  remotePairFailed: "无法生成配对码,请确认已登录订阅账号。",
@@ -6024,9 +6070,9 @@ window.__ModuleLoader__.load({
6024
6070
  searchTitle: "搜索来源",
6025
6071
  searchScope: "自动按当前会话模型分流;手动选择会覆盖所有模型和会话。",
6026
6072
  searchAuto: "自动",
6027
- searchAutoHint: "Codex 模型用订阅搜索,其他模型用 DSH",
6073
+ searchAutoHint: "Codex 模型用订阅搜索,其他模型用 DSH;一方未登录或没有 Key 时自动改用另一方",
6028
6074
  searchDsh: "DSH 默认",
6029
- searchDshHint: "所有模型使用 DSH 当前搜索服务",
6075
+ searchDshHint: "所有模型使用 DSH 当前搜索服务;没有 API Key 时用已登录的 DeepSeek 账号",
6030
6076
  searchCodex: "Codex 订阅",
6031
6077
  searchCodexHint: "所有模型通过已登录的 ChatGPT 订阅搜索",
6032
6078
  preferenceFailed: "设置未保存。",
@@ -6394,13 +6440,54 @@ window.__ModuleLoader__.load({
6394
6440
  subagentBackend_dsh: "DSH",
6395
6441
  subagentBackend_codex: "Codex",
6396
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",
6397
6452
  remoteTitle: "Phone remote control",
6398
6453
  remote_off: "Off",
6399
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: "Русский",
6400
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.",
6401
- remoteConnected: "Connected",
6487
+ remoteConnected: "This computer is online with ChatGPT; no phone connected yet",
6402
6488
  remoteConnecting: "Connecting",
6403
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.",
6404
6491
  remoteError: "Connection failed, retrying",
6405
6492
  remoteStopped: "Stopped",
6406
6493
  remotePaired: "Paired phones",
@@ -6710,9 +6797,9 @@ window.__ModuleLoader__.load({
6710
6797
  searchTitle: "Search source",
6711
6798
  searchScope: "Auto follows the current session model; an explicit choice overrides every model and session.",
6712
6799
  searchAuto: "Auto",
6713
- 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",
6714
6801
  searchDsh: "DSH default",
6715
- 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",
6716
6803
  searchCodex: "Codex subscription",
6717
6804
  searchCodexHint: "Search through the signed-in ChatGPT subscription for every model",
6718
6805
  preferenceFailed: "The setting was not saved.",
@@ -7255,6 +7342,59 @@ window.__ModuleLoader__.load({
7255
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}}
7256
7343
  @container (max-width:340px){.codexSettingsTabs{grid-template-columns:repeat(2,minmax(0,1fr))}}
7257
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)}
7258
7398
  `;
7259
7399
  //#endregion
7260
7400
  //#region src/image-edit-reference.js
@@ -7263,10 +7403,10 @@ window.__ModuleLoader__.load({
7263
7403
  const PIN_OUTER_STROKE = "#111827";
7264
7404
  const PIN_INNER_STROKE = "#ffffff";
7265
7405
  const finiteDimension = (value) => Number.isSafeInteger(value) && value > 0;
7266
- 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));
7267
7407
  const clampCenter = (value, size, edge) => {
7268
7408
  const margin = Math.min(edge, size / 2);
7269
- return clamp$2(value, margin, size - margin);
7409
+ return clamp$3(value, margin, size - margin);
7270
7410
  };
7271
7411
  function getBitmapFactory(options) {
7272
7412
  const factory = options?.createImageBitmap ?? options?.bitmapFactory ?? globalThis.createImageBitmap;
@@ -7403,7 +7543,7 @@ window.__ModuleLoader__.load({
7403
7543
  `;
7404
7544
  //#endregion
7405
7545
  //#region src/subscription-image-transform.js
7406
- 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));
7407
7547
  const fitted = () => ({
7408
7548
  zoom: 1,
7409
7549
  x: 0,
@@ -7445,15 +7585,15 @@ window.__ModuleLoader__.load({
7445
7585
  const limitX = Math.max(0, (surface.offsetWidth * zoom - stage.clientWidth) / 2);
7446
7586
  const limitY = Math.max(0, (surface.offsetHeight * zoom - stage.clientHeight) / 2);
7447
7587
  return {
7448
- x: clamp$1(x, -limitX, limitX),
7449
- y: clamp$1(y, -limitY, limitY)
7588
+ x: clamp$2(x, -limitX, limitX),
7589
+ y: clamp$2(y, -limitY, limitY)
7450
7590
  };
7451
7591
  }, []);
7452
7592
  const setZoomAt = (0, react.useCallback)((zoom, clientX, clientY) => {
7453
7593
  const stage = stageRef.current;
7454
7594
  if (!stage) return;
7455
7595
  setTransform((current) => {
7456
- const next = clamp$1(zoom, .5, Math.max(8, actualScale()));
7596
+ const next = clamp$2(zoom, .5, Math.max(8, actualScale()));
7457
7597
  const box = stage.getBoundingClientRect();
7458
7598
  const px = clientX - box.left - box.width / 2, py = clientY - box.top - box.height / 2;
7459
7599
  const ratio = next / current.zoom;
@@ -7594,7 +7734,7 @@ window.__ModuleLoader__.load({
7594
7734
  }) : null;
7595
7735
  };
7596
7736
  const fill$1 = (value, variables) => Object.entries(variables).reduce((text, [key, replacement]) => text.replaceAll(`{${key}}`, String(replacement)), value);
7597
- 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));
7598
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`;
7599
7739
  const downloadName = (name) => {
7600
7740
  const cleaned = String(name || "image.png").replace(/[<>:"/\\|?*\u0000-\u001f]/gu, "-").replace(/[. ]+$/u, "").trim();
@@ -7879,7 +8019,7 @@ window.__ModuleLoader__.load({
7879
8019
  }
7880
8020
  if (event.key === "[" || event.key === "]") {
7881
8021
  event.preventDefault();
7882
- 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);
7883
8023
  return;
7884
8024
  }
7885
8025
  if (event.key === "ArrowLeft" || event.key === "ArrowRight") return;
@@ -7965,8 +8105,8 @@ window.__ModuleLoader__.load({
7965
8105
  const bounds = maskRef.current?.getBoundingClientRect();
7966
8106
  if (bounds === void 0) return void 0;
7967
8107
  return {
7968
- x: clamp((event.clientX - bounds.left) / bounds.width, 0, 1),
7969
- 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)
7970
8110
  };
7971
8111
  };
7972
8112
  const maskHandlers = {
@@ -8046,8 +8186,8 @@ window.__ModuleLoader__.load({
8046
8186
  if (bounds === void 0) return;
8047
8187
  const annotation = {
8048
8188
  id: crypto.randomUUID(),
8049
- x: clamp((event.clientX - bounds.left) / bounds.width, 0, 1),
8050
- 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),
8051
8191
  note: ""
8052
8192
  };
8053
8193
  setAnnotations((current) => [...current, annotation]);
@@ -8613,6 +8753,51 @@ window.__ModuleLoader__.load({
8613
8753
  const OUTPUT_VERBOSITY_MEDIUM = "medium";
8614
8754
  const OUTPUT_VERBOSITY_HIGH = "high";
8615
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";
8616
8801
  const REASONING_SUMMARY_FIELD = "reasoningSummary";
8617
8802
  const REASONING_SUMMARIES = Object.freeze([
8618
8803
  "auto",
@@ -8779,6 +8964,9 @@ window.__ModuleLoader__.load({
8779
8964
  outputVerbosity: normalizeOutputVerbosity(value?.[OUTPUT_VERBOSITY_FIELD]),
8780
8965
  inputImageDetail: normalizeInputImageDetail(value?.[INPUT_IMAGE_DETAIL_FIELD]),
8781
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]),
8782
8970
  retryPolicy: normalizeRetryPolicy(value?.[RETRY_POLICY_FIELD]),
8783
8971
  connectors: normalizeConnectorMode(value?.[CONNECTORS_FIELD]),
8784
8972
  streamIdleTimeoutMinutes: normalizeStreamIdleTimeoutMinutes(value?.[STREAM_IDLE_TIMEOUT_MINUTES_FIELD]),
@@ -8838,6 +9026,9 @@ window.__ModuleLoader__.load({
8838
9026
  [OUTPUT_VERBOSITY_FIELD]: normalizeOutputVerbosity(value?.[OUTPUT_VERBOSITY_FIELD]),
8839
9027
  [INPUT_IMAGE_DETAIL_FIELD]: normalizeInputImageDetail(value?.[INPUT_IMAGE_DETAIL_FIELD]),
8840
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]),
8841
9032
  [RETRY_POLICY_FIELD]: normalizeRetryPolicy(value?.[RETRY_POLICY_FIELD]),
8842
9033
  [CONNECTORS_FIELD]: normalizeConnectorMode(value?.[CONNECTORS_FIELD]),
8843
9034
  [STREAM_IDLE_TIMEOUT_MINUTES_FIELD]: normalizeStreamIdleTimeoutMinutes(value?.[STREAM_IDLE_TIMEOUT_MINUTES_FIELD]),
@@ -9325,7 +9516,7 @@ window.__ModuleLoader__.load({
9325
9516
  }
9326
9517
  //#endregion
9327
9518
  //#region src/version.js
9328
- const PACKAGE_VERSION = "3.0.0";
9519
+ const PACKAGE_VERSION = "3.0.1";
9329
9520
  //#endregion
9330
9521
  //#region src/client-recovery.js
9331
9522
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -10049,6 +10240,1189 @@ window.__ModuleLoader__.load({
10049
10240
  });
10050
10241
  }
10051
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
10052
11426
  //#region src/image-setting-groups.js
10053
11427
  const IMAGE_SETTING_GROUPS = Object.freeze({
10054
11428
  imageCapability: ["imageGeneration", "imageEditing"],
@@ -12943,9 +14317,9 @@ window.__ModuleLoader__.load({
12943
14317
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
12944
14318
  role: "status",
12945
14319
  children: [
12946
- t(STATUS_KEYS[status] ?? "remoteConnecting"),
14320
+ status === "connected" && state?.phones > 0 ? t("remotePhones").replace("{count}", String(state.phones)) : t(STATUS_KEYS[status] ?? "remoteConnecting"),
12947
14321
  status === "error" && state?.lastError ? ` · ${state.lastError}` : "",
12948
- state?.phones > 0 ? ` · ${t("remotePhones").replace("{count}", String(state.phones))}` : ""
14322
+ status !== "connected" && state?.phones > 0 ? ` · ${t("remotePhones").replace("{count}", String(state.phones))}` : ""
12949
14323
  ]
12950
14324
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
12951
14325
  type: "button",
@@ -12957,6 +14331,11 @@ window.__ModuleLoader__.load({
12957
14331
  children: t(pairing ? "remotePairAgain" : "remotePair")
12958
14332
  })]
12959
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,
12960
14339
  failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
12961
14340
  role: "alert",
12962
14341
  className: "codexSubscriptionHelp",
@@ -13281,7 +14660,7 @@ window.__ModuleLoader__.load({
13281
14660
  const snapshot = usePreferenceSnapshot(preference);
13282
14661
  const writable = snapshot.status === "ready" && snapshot.writable === true;
13283
14662
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13284
- className: "codexSubscriptionPreference codexSubscriptionPreferenceStack",
14663
+ className: "codexSubscriptionPreference codexSubscriptionConnectors",
13285
14664
  children: [
13286
14665
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13287
14666
  className: "codexSubscriptionPreferenceCopy",
@@ -13744,6 +15123,12 @@ window.__ModuleLoader__.load({
13744
15123
  snapshot,
13745
15124
  t
13746
15125
  }),
15126
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceCallCard, {
15127
+ rpc,
15128
+ preference,
15129
+ snapshot,
15130
+ t
15131
+ }),
13747
15132
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
13748
15133
  className: "codexSubscriptionCard codexSubscriptionPreferencesCard",
13749
15134
  "aria-label": t("compactionTitle"),
@@ -14504,6 +15889,13 @@ window.__ModuleLoader__.load({
14504
15889
  };
14505
15890
  }
14506
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
14507
15899
  //#region src/diagnostic-compact.js
14508
15900
  function compactDiagnostic(report) {
14509
15901
  const summary = report.summary ?? { findings: [] };
@@ -14534,17 +15926,29 @@ window.__ModuleLoader__.load({
14534
15926
  reason: item.reason,
14535
15927
  ...item.latest ? { latest: `${item.latest.action} ${item.latest.status}` } : { latest: "not-verified" }
14536
15928
  })),
15929
+ log: (report.log?.entries ?? []).slice(-150).map(formatLogLine),
14537
15930
  ...report.client?.status === "unknown" ? { hostCollected: false } : {}
14538
15931
  };
14539
15932
  }
14540
15933
  //#endregion
14541
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
+ };
14542
15944
  function DiagnosticsCard({ rpc, t, diagnostics }) {
14543
15945
  const [report, setReport] = (0, react.useState)();
14544
15946
  const [busy, setBusy] = (0, react.useState)(false);
14545
15947
  const [copied, setCopied] = (0, react.useState)(false);
14546
15948
  const [error, setError] = (0, react.useState)(false);
14547
15949
  const [copyError, setCopyError] = (0, react.useState)(false);
15950
+ const [level, setLevel] = (0, react.useState)("all");
15951
+ const [logCopied, setLogCopied] = (0, react.useState)(false);
14548
15952
  const load = () => {
14549
15953
  setBusy(true);
14550
15954
  setError(false);
@@ -14567,6 +15971,10 @@ window.__ModuleLoader__.load({
14567
15971
  setCopyError(false);
14568
15972
  Promise.resolve().then(() => navigator.clipboard.writeText(JSON.stringify(full ? report : compactDiagnostic(report), null, 2))).then(() => setCopied(true)).catch(() => setCopyError(true));
14569
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
+ };
14570
15978
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14571
15979
  className: "codexSubscriptionCard codexSubscriptionDiagnostics",
14572
15980
  children: [
@@ -14605,10 +16013,6 @@ window.__ModuleLoader__.load({
14605
16013
  })]
14606
16014
  }),
14607
16015
  report === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
14608
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14609
- className: "codexSubscriptionHelp",
14610
- children: t("diagnosticsCoverageHint")
14611
- }),
14612
16016
  report.generatedAt ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
14613
16017
  className: "codexSubscriptionHelp",
14614
16018
  children: [
@@ -14636,22 +16040,54 @@ window.__ModuleLoader__.load({
14636
16040
  ] }, `${event.source}:${event.action}`)) }) : null
14637
16041
  ] }, item.code)) }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("diagnosticsNoRecordedFailure") })
14638
16042
  }),
14639
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
14640
- style: {
14641
- width: "100%",
14642
- fontSize: 12,
14643
- textAlign: "left",
14644
- borderSpacing: "0 8px"
14645
- },
14646
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
14647
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsCapability") }),
14648
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsReadiness") }),
14649
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("diagnosticsEvidence") })
14650
- ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: (report.inspection?.capabilities ?? []).map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
14651
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: t(`diagnosticCapability_${item.id}`) }),
14652
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [t(`diagnosticReason_${item.reason}`), item.blockedBy ? ` · ${t(`diagnosticCapability_${item.blockedBy}`)}` : ""] }),
14653
- /* @__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] })
14654
- ] }, 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))
14655
16091
  }),
14656
16092
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
14657
16093
  className: "codexSubscriptionHelp",
@@ -14659,6 +16095,28 @@ window.__ModuleLoader__.load({
14659
16095
  }),
14660
16096
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [
14661
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
+ }),
14662
16120
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
14663
16121
  type: "button",
14664
16122
  variant: "outline",
@@ -15851,6 +17309,7 @@ window.__ModuleLoader__.load({
15851
17309
  "uiConversation",
15852
17310
  "sessions"
15853
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 })] });
15854
17313
  function apply(ctx) {
15855
17314
  const diagnostics = createHostDiagnostics();
15856
17315
  ctx.effect(() => diagnostics.register(ctx.slots), "codex-subscription: host inspection");
@@ -15928,12 +17387,13 @@ window.__ModuleLoader__.load({
15928
17387
  order: 15,
15929
17388
  locale: NS,
15930
17389
  inject: (sessionId) => ({
17390
+ sessionId,
15931
17391
  preference,
15932
17392
  rpc,
15933
17393
  t,
15934
17394
  directory: modelDirectories.directoryFor(sessionId).store
15935
17395
  })
15936
- }, CodexComposerQuota));
17396
+ }, ComposerRight));
15937
17397
  scope.slots.inject("conversation.input.model", () => scope.slots.register({
15938
17398
  name: "conversation.input.model",
15939
17399
  priority: -10,