@phenx-inc/ctlsurf 0.6.0 → 0.8.0

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.
Files changed (34) hide show
  1. package/out/headless/index.mjs +26 -10
  2. package/out/headless/index.mjs.map +2 -2
  3. package/out/main/index.js +31 -9
  4. package/out/preload/index.js +8 -0
  5. package/out/renderer/assets/{cssMode-DbMmcl1h.js → cssMode-BQN8v2ok.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-CvaHiy92.js → freemarker2-DbxGYYVp.js} +1 -1
  7. package/out/renderer/assets/{handlebars-D58lUIOu.js → handlebars-3auU1CAd.js} +1 -1
  8. package/out/renderer/assets/{html-D1h1aJbM.js → html-D8xFiRmI.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-BdkAp9qr.js → htmlMode-M3MApZ4n.js} +3 -3
  10. package/out/renderer/assets/{index-B60JU1yI.js → index---H6cxNl.js} +854 -38
  11. package/out/renderer/assets/{index-DJFYmHjz.css → index-B-iM7dFC.css} +269 -0
  12. package/out/renderer/assets/{javascript-CXqZcnvb.js → javascript-BO_ViZM5.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-BuVr-eSl.js → jsonMode-CKp2zvZu.js} +3 -3
  14. package/out/renderer/assets/{liquid-LKu0Wd0B.js → liquid-C1eHcrht.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-Cjr_4HGs.js → lspLanguageFeatures-CHWJx_Tl.js} +1 -1
  16. package/out/renderer/assets/{mdx-Bl84ILla.js → mdx-Qqdtk7fL.js} +1 -1
  17. package/out/renderer/assets/{python-0sFd9G1k.js → python-DKu7rNbs.js} +1 -1
  18. package/out/renderer/assets/{razor-Cqcu1rLJ.js → razor-BOMpCo6z.js} +1 -1
  19. package/out/renderer/assets/{tsMode-CYd3NUkW.js → tsMode-yAjlPR-D.js} +1 -1
  20. package/out/renderer/assets/{typescript-rkc9lhpi.js → typescript-BiJRCUcL.js} +1 -1
  21. package/out/renderer/assets/{xml-EsHEUps1.js → xml-D4PvYeQq.js} +1 -1
  22. package/out/renderer/assets/{yaml-B9-nQ_s2.js → yaml-BeHVkmnS.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/src/main/index.ts +7 -0
  26. package/src/main/orchestrator.ts +38 -9
  27. package/src/preload/index.ts +11 -0
  28. package/src/renderer/App.tsx +39 -6
  29. package/src/renderer/components/FloatingMic.tsx +128 -0
  30. package/src/renderer/components/SpeakControls.tsx +235 -0
  31. package/src/renderer/components/VoiceInput.tsx +170 -6
  32. package/src/renderer/lib/localWhisper.ts +48 -4
  33. package/src/renderer/lib/speech.ts +299 -0
  34. package/src/renderer/styles.css +269 -0
@@ -1,4 +1,4 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./cssMode-DbMmcl1h.js","./lspLanguageFeatures-Cjr_4HGs.js","./htmlMode-BdkAp9qr.js","./jsonMode-BuVr-eSl.js","./javascript-CXqZcnvb.js","./typescript-rkc9lhpi.js"])))=>i.map(i=>d[i]);
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./cssMode-BQN8v2ok.js","./lspLanguageFeatures-CHWJx_Tl.js","./htmlMode-M3MApZ4n.js","./jsonMode-CKp2zvZu.js","./javascript-BO_ViZM5.js","./typescript-BiJRCUcL.js"])))=>i.map(i=>d[i]);
2
2
  function getDefaultExportFromCjs(x) {
3
3
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
4
4
  }
@@ -19163,6 +19163,26 @@ const __vitePreload = function preload(baseModule, deps, importerUrl) {
19163
19163
  };
19164
19164
  const MODEL = "Xenova/whisper-base";
19165
19165
  const TARGET_SAMPLE_RATE = 16e3;
19166
+ const SILENCE_RMS = 8e-3;
19167
+ const LOW_CONFIDENCE_RMS = 0.02;
19168
+ const FILLER_PHRASES = /* @__PURE__ */ new Set([
19169
+ "you",
19170
+ "thank you",
19171
+ "thank you very much",
19172
+ "thank you for watching",
19173
+ "thanks for watching",
19174
+ "please subscribe"
19175
+ ]);
19176
+ function computeRms(pcm) {
19177
+ if (pcm.length === 0) return 0;
19178
+ let sum2 = 0;
19179
+ for (let i2 = 0; i2 < pcm.length; i2++) sum2 += pcm[i2] * pcm[i2];
19180
+ return Math.sqrt(sum2 / pcm.length);
19181
+ }
19182
+ function isFillerOnly(text2) {
19183
+ const norm = text2.toLowerCase().replace(/[.!?,…"']/g, "").replace(/\s+/g, " ").trim();
19184
+ return FILLER_PHRASES.has(norm);
19185
+ }
19166
19186
  let transcriberPromise = null;
19167
19187
  async function loadTranscriber(onProgress) {
19168
19188
  if (!transcriberPromise) {
@@ -19213,25 +19233,42 @@ async function blobToPcm16k(blob) {
19213
19233
  return rendered.getChannelData(0);
19214
19234
  }
19215
19235
  async function transcribeBlob(blob, onProgress) {
19216
- const transcriber = await loadTranscriber(onProgress);
19217
19236
  const pcm = await blobToPcm16k(blob);
19218
19237
  if (!pcm) return "";
19238
+ const rms = computeRms(pcm);
19239
+ if (rms < SILENCE_RMS) {
19240
+ console.info(`[voice] near-silent clip (rms=${rms.toFixed(4)}); skipping transcription`);
19241
+ return "";
19242
+ }
19243
+ const transcriber = await loadTranscriber(onProgress);
19219
19244
  const result = await transcriber(pcm);
19220
- const text2 = Array.isArray(result) ? result.map((r) => r.text).join(" ") : result?.text;
19221
- return (text2 || "").trim();
19245
+ const text2 = (Array.isArray(result) ? result.map((r) => r.text).join(" ") : result?.text || "").trim();
19246
+ if (text2 && rms < LOW_CONFIDENCE_RMS && isFillerOnly(text2)) {
19247
+ console.info(`[voice] dropping filler-only output "${text2}" from quiet clip (rms=${rms.toFixed(4)})`);
19248
+ return "";
19249
+ }
19250
+ return text2;
19222
19251
  }
19223
19252
  function getRecognitionCtor() {
19224
19253
  const w = window;
19225
19254
  return w.SpeechRecognition || w.webkitSpeechRecognition || null;
19226
19255
  }
19227
- const ENGINE_KEY = "ctlsurf.voiceEngine";
19256
+ const ENGINE_KEY$1 = "ctlsurf.voiceEngine";
19257
+ const DEVICE_KEY = "ctlsurf.voiceDeviceId";
19258
+ function loadDeviceId() {
19259
+ try {
19260
+ return localStorage.getItem(DEVICE_KEY) || null;
19261
+ } catch {
19262
+ return null;
19263
+ }
19264
+ }
19228
19265
  const WEB_SPEECH_SUPPORTED = getRecognitionCtor() !== null;
19229
19266
  const LOCAL_SUPPORTED = typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof MediaRecorder !== "undefined" && typeof OfflineAudioContext !== "undefined";
19230
19267
  const ANY_SUPPORTED = WEB_SPEECH_SUPPORTED || LOCAL_SUPPORTED;
19231
19268
  function loadInitialEngine() {
19232
19269
  if (!WEB_SPEECH_SUPPORTED && LOCAL_SUPPORTED) return "local";
19233
19270
  try {
19234
- if (localStorage.getItem(ENGINE_KEY) === "local" && LOCAL_SUPPORTED) return "local";
19271
+ if (localStorage.getItem(ENGINE_KEY$1) === "local" && LOCAL_SUPPORTED) return "local";
19235
19272
  } catch {
19236
19273
  }
19237
19274
  return WEB_SPEECH_SUPPORTED ? "web-speech" : "local";
@@ -19245,13 +19282,21 @@ function describeMicError(err) {
19245
19282
  if (name === "NotFoundError") return "No microphone found";
19246
19283
  return "Could not start microphone";
19247
19284
  }
19248
- function VoiceInput({ onTranscript }) {
19249
- const [engine, setEngine] = reactExports.useState(loadInitialEngine);
19285
+ function VoiceInput({ onTranscript, variant = "titlebar" }) {
19286
+ const [engine, setEngine2] = reactExports.useState(loadInitialEngine);
19250
19287
  const [phase, setPhase] = reactExports.useState("idle");
19251
19288
  const [interim, setInterim] = reactExports.useState("");
19252
19289
  const [modelPct, setModelPct] = reactExports.useState(null);
19253
19290
  const [error, setError] = reactExports.useState(null);
19254
19291
  const [notice, setNotice] = reactExports.useState(null);
19292
+ const [devices, setDevices] = reactExports.useState([]);
19293
+ const [selectedDeviceId, setSelectedDeviceId] = reactExports.useState(loadDeviceId);
19294
+ const [showDevicePicker, setShowDevicePicker] = reactExports.useState(false);
19295
+ const selectedDeviceIdRef = reactExports.useRef(selectedDeviceId);
19296
+ reactExports.useEffect(() => {
19297
+ selectedDeviceIdRef.current = selectedDeviceId;
19298
+ }, [selectedDeviceId]);
19299
+ const wrapRef = reactExports.useRef(null);
19255
19300
  const recognitionRef = reactExports.useRef(null);
19256
19301
  const finalRef = reactExports.useRef("");
19257
19302
  const streamRef = reactExports.useRef(null);
@@ -19278,16 +19323,89 @@ function VoiceInput({ onTranscript }) {
19278
19323
  }, [notice]);
19279
19324
  const switchToLocal = reactExports.useCallback((reason) => {
19280
19325
  try {
19281
- localStorage.setItem(ENGINE_KEY, "local");
19326
+ localStorage.setItem(ENGINE_KEY$1, "local");
19282
19327
  } catch {
19283
19328
  }
19284
- setEngine("local");
19329
+ setEngine2("local");
19285
19330
  setNotice(reason);
19286
19331
  }, []);
19287
19332
  const stopStream = reactExports.useCallback(() => {
19288
19333
  streamRef.current?.getTracks().forEach((t) => t.stop());
19289
19334
  streamRef.current = null;
19290
19335
  }, []);
19336
+ const refreshDevices = reactExports.useCallback(async () => {
19337
+ if (!navigator.mediaDevices?.enumerateDevices) return;
19338
+ try {
19339
+ const all = await navigator.mediaDevices.enumerateDevices();
19340
+ setDevices(all.filter((d) => d.kind === "audioinput"));
19341
+ } catch {
19342
+ }
19343
+ }, []);
19344
+ const ensureDeviceLabels = reactExports.useCallback(async () => {
19345
+ if (!navigator.mediaDevices?.enumerateDevices) return;
19346
+ try {
19347
+ const all = await navigator.mediaDevices.enumerateDevices();
19348
+ const inputs = all.filter((d) => d.kind === "audioinput");
19349
+ if (inputs.length && inputs.every((d) => !d.label)) {
19350
+ const s = await navigator.mediaDevices.getUserMedia({ audio: true });
19351
+ s.getTracks().forEach((t) => t.stop());
19352
+ }
19353
+ } catch {
19354
+ }
19355
+ await refreshDevices();
19356
+ }, [refreshDevices]);
19357
+ const toggleDevicePicker = reactExports.useCallback(() => {
19358
+ setShowDevicePicker((open) => !open);
19359
+ }, []);
19360
+ reactExports.useEffect(() => {
19361
+ if (showDevicePicker) void ensureDeviceLabels();
19362
+ }, [showDevicePicker, ensureDeviceLabels]);
19363
+ const chooseDevice = reactExports.useCallback((id) => {
19364
+ setSelectedDeviceId(id);
19365
+ try {
19366
+ if (id) localStorage.setItem(DEVICE_KEY, id);
19367
+ else localStorage.removeItem(DEVICE_KEY);
19368
+ } catch {
19369
+ }
19370
+ setShowDevicePicker(false);
19371
+ }, []);
19372
+ const getStream = reactExports.useCallback(async () => {
19373
+ const id = selectedDeviceIdRef.current;
19374
+ try {
19375
+ return await navigator.mediaDevices.getUserMedia({
19376
+ audio: id ? { deviceId: { exact: id } } : true
19377
+ });
19378
+ } catch (err) {
19379
+ if (id && err?.name === "OverconstrainedError") {
19380
+ try {
19381
+ localStorage.removeItem(DEVICE_KEY);
19382
+ } catch {
19383
+ }
19384
+ setSelectedDeviceId(null);
19385
+ setNotice("Saved microphone unavailable — using system default.");
19386
+ return navigator.mediaDevices.getUserMedia({ audio: true });
19387
+ }
19388
+ throw err;
19389
+ }
19390
+ }, []);
19391
+ reactExports.useEffect(() => {
19392
+ const md = navigator.mediaDevices;
19393
+ if (!md?.addEventListener) return;
19394
+ const onChange = () => {
19395
+ void refreshDevices();
19396
+ };
19397
+ md.addEventListener("devicechange", onChange);
19398
+ void refreshDevices();
19399
+ return () => md.removeEventListener("devicechange", onChange);
19400
+ }, [refreshDevices]);
19401
+ reactExports.useEffect(() => {
19402
+ if (!showDevicePicker) return;
19403
+ const onDocDown = (e) => {
19404
+ if (!wrapRef.current?.contains(e.target)) setShowDevicePicker(false);
19405
+ };
19406
+ document.addEventListener("mousedown", onDocDown);
19407
+ return () => document.removeEventListener("mousedown", onDocDown);
19408
+ }, [showDevicePicker]);
19291
19409
  const startWebSpeech = reactExports.useCallback(() => {
19292
19410
  const Ctor = getRecognitionCtor();
19293
19411
  if (!Ctor || recognitionRef.current) return;
@@ -19361,6 +19479,7 @@ function VoiceInput({ onTranscript }) {
19361
19479
  try {
19362
19480
  const text2 = await transcribeBlob(blob, handleModelProgress);
19363
19481
  if (text2) onTranscriptRef.current(text2);
19482
+ else setNotice("No speech detected — check the mic source (▾).");
19364
19483
  } catch (err) {
19365
19484
  setError("On-device transcription failed");
19366
19485
  console.error("[voice] local transcription failed", err);
@@ -19375,7 +19494,7 @@ function VoiceInput({ onTranscript }) {
19375
19494
  setInterim("");
19376
19495
  cancelGestureRef.current = false;
19377
19496
  try {
19378
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
19497
+ const stream = await getStream();
19379
19498
  if (cancelGestureRef.current) {
19380
19499
  stream.getTracks().forEach((t) => t.stop());
19381
19500
  setPhase("idle");
@@ -19399,7 +19518,7 @@ function VoiceInput({ onTranscript }) {
19399
19518
  setError(describeMicError(err));
19400
19519
  console.error("[voice] getUserMedia failed", err);
19401
19520
  }
19402
- }, [runLocalTranscription, stopStream]);
19521
+ }, [runLocalTranscription, stopStream, getStream]);
19403
19522
  const stopLocal = reactExports.useCallback(() => {
19404
19523
  cancelGestureRef.current = true;
19405
19524
  const rec = recorderRef.current;
@@ -19441,12 +19560,21 @@ function VoiceInput({ onTranscript }) {
19441
19560
  else if (notice && phase === "idle") chip = { kind: "notice", text: notice };
19442
19561
  else if (listening) chip = { kind: "listening", text: interim || (engine === "local" ? "Recording…" : "Listening…") };
19443
19562
  else if (busy) chip = { kind: "busy", text: modelPct !== null ? `Downloading voice model… ${modelPct}%` : "Transcribing…" };
19444
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "voice-input-wrap", children: [
19563
+ const floating = variant === "floating";
19564
+ const btnClass = floating ? `voice-btn voice-btn-floating ${listening ? "listening" : ""} ${busy ? "busy" : ""}` : `titlebar-btn titlebar-icon-btn voice-btn ${listening ? "listening" : ""} ${busy ? "busy" : ""}`;
19565
+ const realDevices = devices.filter(
19566
+ (d) => d.deviceId && d.deviceId !== "default" && d.deviceId !== "communications"
19567
+ );
19568
+ const rawDefault = devices.find((d) => d.deviceId === "default")?.label;
19569
+ const defaultLabel = rawDefault ? `System default · ${rawDefault.replace(/^Default\s*-\s*/i, "")}` : "System default";
19570
+ const activeLabel = selectedDeviceId ? realDevices.find((d) => d.deviceId === selectedDeviceId)?.label || "Selected microphone" : defaultLabel;
19571
+ const showSourcePicker = floating && LOCAL_SUPPORTED;
19572
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "voice-input-wrap", ref: wrapRef, children: [
19445
19573
  /* @__PURE__ */ jsxRuntimeExports.jsxs(
19446
19574
  "button",
19447
19575
  {
19448
19576
  type: "button",
19449
- className: `titlebar-btn titlebar-icon-btn voice-btn ${listening ? "listening" : ""} ${busy ? "busy" : ""}`,
19577
+ className: btnClass,
19450
19578
  disabled: !ANY_SUPPORTED,
19451
19579
  onPointerDown: handlePointerDown,
19452
19580
  onPointerUp: handlePointerUp,
@@ -19460,7 +19588,645 @@ function VoiceInput({ onTranscript }) {
19460
19588
  ]
19461
19589
  }
19462
19590
  ),
19463
- chip && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `voice-chip ${chip.kind}`, children: chip.text })
19591
+ showSourcePicker && /* @__PURE__ */ jsxRuntimeExports.jsx(
19592
+ "button",
19593
+ {
19594
+ type: "button",
19595
+ className: "voice-source-btn",
19596
+ onPointerDown: (e) => e.stopPropagation(),
19597
+ onClick: toggleDevicePicker,
19598
+ title: `Mic source: ${activeLabel}`,
19599
+ "aria-label": "Choose microphone source",
19600
+ "aria-expanded": showDevicePicker,
19601
+ children: "▾"
19602
+ }
19603
+ ),
19604
+ showSourcePicker && showDevicePicker && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "voice-source-menu", role: "menu", children: [
19605
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "voice-source-head", children: "Microphone source" }),
19606
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
19607
+ "button",
19608
+ {
19609
+ type: "button",
19610
+ role: "menuitemradio",
19611
+ "aria-checked": selectedDeviceId === null,
19612
+ className: `voice-source-item ${selectedDeviceId === null ? "active" : ""}`,
19613
+ onClick: () => chooseDevice(null),
19614
+ children: [
19615
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "voice-source-check", children: selectedDeviceId === null ? "✓" : "" }),
19616
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "voice-source-label", children: defaultLabel })
19617
+ ]
19618
+ }
19619
+ ),
19620
+ realDevices.map((d, i2) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
19621
+ "button",
19622
+ {
19623
+ type: "button",
19624
+ role: "menuitemradio",
19625
+ "aria-checked": selectedDeviceId === d.deviceId,
19626
+ className: `voice-source-item ${selectedDeviceId === d.deviceId ? "active" : ""}`,
19627
+ onClick: () => chooseDevice(d.deviceId),
19628
+ children: [
19629
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "voice-source-check", children: selectedDeviceId === d.deviceId ? "✓" : "" }),
19630
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "voice-source-label", children: d.label || `Microphone ${i2 + 1}` })
19631
+ ]
19632
+ },
19633
+ d.deviceId
19634
+ )),
19635
+ realDevices.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "voice-source-empty", children: "No microphones found" })
19636
+ ] }),
19637
+ chip && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `voice-chip ${chip.kind} ${floating ? "voice-chip-floating" : ""}`, children: chip.text })
19638
+ ] });
19639
+ }
19640
+ const POS_KEY = "ctlsurf.floatingMicPos";
19641
+ const EDGE = 20;
19642
+ const TOP_MIN = 46;
19643
+ const BOTTOM_GAP = 36;
19644
+ function loadPos() {
19645
+ try {
19646
+ const raw = localStorage.getItem(POS_KEY);
19647
+ if (raw) {
19648
+ const p = JSON.parse(raw);
19649
+ if (typeof p.x === "number" && typeof p.y === "number") return { x: p.x, y: p.y };
19650
+ }
19651
+ } catch {
19652
+ }
19653
+ return null;
19654
+ }
19655
+ function FloatingMic({ onTranscript, onHide }) {
19656
+ const [pos, setPos] = reactExports.useState(loadPos);
19657
+ const elRef = reactExports.useRef(null);
19658
+ const dragRef = reactExports.useRef(null);
19659
+ const clamp2 = reactExports.useCallback((x, y) => {
19660
+ const el = elRef.current;
19661
+ const w = el?.offsetWidth ?? 64;
19662
+ const h2 = el?.offsetHeight ?? 90;
19663
+ return {
19664
+ x: Math.max(EDGE, Math.min(x, window.innerWidth - w - EDGE)),
19665
+ y: Math.max(TOP_MIN, Math.min(y, window.innerHeight - h2 - BOTTOM_GAP))
19666
+ };
19667
+ }, []);
19668
+ reactExports.useEffect(() => {
19669
+ if (pos) return;
19670
+ const el = elRef.current;
19671
+ const w = el?.offsetWidth ?? 64;
19672
+ const h2 = el?.offsetHeight ?? 90;
19673
+ setPos({
19674
+ x: window.innerWidth - w - EDGE,
19675
+ y: window.innerHeight - h2 - BOTTOM_GAP
19676
+ });
19677
+ }, [pos]);
19678
+ reactExports.useEffect(() => {
19679
+ const onResize = () => setPos((p) => p ? clamp2(p.x, p.y) : p);
19680
+ window.addEventListener("resize", onResize);
19681
+ return () => window.removeEventListener("resize", onResize);
19682
+ }, [clamp2]);
19683
+ const onHandleDown = reactExports.useCallback((e) => {
19684
+ const el = elRef.current;
19685
+ if (!el) return;
19686
+ e.preventDefault();
19687
+ const rect = el.getBoundingClientRect();
19688
+ dragRef.current = { dx: e.clientX - rect.left, dy: e.clientY - rect.top };
19689
+ e.currentTarget.setPointerCapture?.(e.pointerId);
19690
+ }, []);
19691
+ const onHandleMove = reactExports.useCallback((e) => {
19692
+ const d = dragRef.current;
19693
+ if (!d) return;
19694
+ setPos(clamp2(e.clientX - d.dx, e.clientY - d.dy));
19695
+ }, [clamp2]);
19696
+ const onHandleUp = reactExports.useCallback((e) => {
19697
+ if (!dragRef.current) return;
19698
+ dragRef.current = null;
19699
+ e.currentTarget.releasePointerCapture?.(e.pointerId);
19700
+ setPos((p) => {
19701
+ if (p) {
19702
+ try {
19703
+ localStorage.setItem(POS_KEY, JSON.stringify(p));
19704
+ } catch {
19705
+ }
19706
+ }
19707
+ return p;
19708
+ });
19709
+ }, []);
19710
+ const style = pos ? { left: pos.x, top: pos.y } : { left: -9999, top: -9999, visibility: "hidden" };
19711
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: elRef, className: "floating-mic", style, children: [
19712
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
19713
+ "div",
19714
+ {
19715
+ className: "floating-mic-handle",
19716
+ onPointerDown: onHandleDown,
19717
+ onPointerMove: onHandleMove,
19718
+ onPointerUp: onHandleUp,
19719
+ onPointerCancel: onHandleUp,
19720
+ title: "Drag to move",
19721
+ "aria-label": "Drag floating mic",
19722
+ children: [
19723
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "floating-mic-grip", "aria-hidden": "true", children: "⠿" }),
19724
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
19725
+ "button",
19726
+ {
19727
+ type: "button",
19728
+ className: "floating-mic-hide",
19729
+ onPointerDown: (e) => e.stopPropagation(),
19730
+ onClick: onHide,
19731
+ title: "Hide floating mic",
19732
+ "aria-label": "Hide floating mic",
19733
+ children: "×"
19734
+ }
19735
+ )
19736
+ ]
19737
+ }
19738
+ ),
19739
+ /* @__PURE__ */ jsxRuntimeExports.jsx(VoiceInput, { variant: "floating", onTranscript })
19740
+ ] });
19741
+ }
19742
+ const ENGINE_KEY = "ctlsurf.tts.engine";
19743
+ const VOICE_KEY = "ctlsurf.tts.voiceURI";
19744
+ const RATE_KEY = "ctlsurf.tts.rate";
19745
+ const MAX_SPEAK_CHARS = 1600;
19746
+ const MAX_CHUNK_CHARS = 280;
19747
+ function cleanForSpeech(input) {
19748
+ let t = input;
19749
+ t = t.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_m, body) => {
19750
+ const lines = body.replace(/\n+$/, "").split("\n").filter((l2) => l2.trim().length).length;
19751
+ return lines > 0 ? ` (code block, ${lines} ${lines === 1 ? "line" : "lines"}) ` : " (code) ";
19752
+ });
19753
+ t = t.replace(/```/g, " (code) ");
19754
+ t = t.replace(/`([^`]+)`/g, "$1");
19755
+ t = t.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
19756
+ t = t.replace(/https?:\/\/\S+/g, "link");
19757
+ t = t.replace(/(?:[\w.-]*\/){2,}([\w.-]+)/g, "$1");
19758
+ t = t.replace(/[*_#>]+/g, " ");
19759
+ t = t.replace(/^\s*[-•]\s+/gm, ", ");
19760
+ t = t.replace(/<[^>]+>/g, " ");
19761
+ t = t.replace(/\s+/g, " ").trim();
19762
+ if (t.length > MAX_SPEAK_CHARS) {
19763
+ t = t.slice(0, MAX_SPEAK_CHARS).replace(/\s+\S*$/, "") + "…";
19764
+ }
19765
+ return t;
19766
+ }
19767
+ function splitIntoChunks(text2) {
19768
+ if (text2.length <= MAX_CHUNK_CHARS) return text2 ? [text2] : [];
19769
+ const sentences = text2.match(/[^.!?]+[.!?]+|\S[^.!?]*$/g) || [text2];
19770
+ const chunks = [];
19771
+ let buf = "";
19772
+ for (const s of sentences) {
19773
+ if ((buf + s).length > MAX_CHUNK_CHARS && buf) {
19774
+ chunks.push(buf.trim());
19775
+ buf = "";
19776
+ }
19777
+ if (s.length > MAX_CHUNK_CHARS) {
19778
+ for (let i2 = 0; i2 < s.length; i2 += MAX_CHUNK_CHARS) chunks.push(s.slice(i2, i2 + MAX_CHUNK_CHARS).trim());
19779
+ } else {
19780
+ buf += s;
19781
+ }
19782
+ }
19783
+ if (buf.trim()) chunks.push(buf.trim());
19784
+ return chunks;
19785
+ }
19786
+ function getEngine() {
19787
+ try {
19788
+ return localStorage.getItem(ENGINE_KEY) === "neural" ? "neural" : "web";
19789
+ } catch {
19790
+ return "web";
19791
+ }
19792
+ }
19793
+ function setEngine(id) {
19794
+ try {
19795
+ localStorage.setItem(ENGINE_KEY, id);
19796
+ } catch {
19797
+ }
19798
+ }
19799
+ function getVoiceURI() {
19800
+ try {
19801
+ return localStorage.getItem(VOICE_KEY) || null;
19802
+ } catch {
19803
+ return null;
19804
+ }
19805
+ }
19806
+ function setVoiceURI(uri) {
19807
+ try {
19808
+ uri ? localStorage.setItem(VOICE_KEY, uri) : localStorage.removeItem(VOICE_KEY);
19809
+ } catch {
19810
+ }
19811
+ }
19812
+ function getRate() {
19813
+ try {
19814
+ const n2 = Number(localStorage.getItem(RATE_KEY));
19815
+ return Number.isFinite(n2) && n2 >= 0.5 && n2 <= 2 ? n2 : 1;
19816
+ } catch {
19817
+ return 1;
19818
+ }
19819
+ }
19820
+ function setRate(rate) {
19821
+ try {
19822
+ localStorage.setItem(RATE_KEY, String(rate));
19823
+ } catch {
19824
+ }
19825
+ }
19826
+ function listWebVoices() {
19827
+ if (typeof speechSynthesis === "undefined") return [];
19828
+ return speechSynthesis.getVoices();
19829
+ }
19830
+ const NEURAL_MODEL = "Xenova/mms-tts-eng";
19831
+ let synthPromise = null;
19832
+ function loadSynthesizer(onProgress) {
19833
+ if (!synthPromise) {
19834
+ synthPromise = (async () => {
19835
+ const { pipeline, env: env2 } = await __vitePreload(async () => {
19836
+ const { pipeline: pipeline2, env: env3 } = await import("./transformers.web-DtSCnG36.js");
19837
+ return { pipeline: pipeline2, env: env3 };
19838
+ }, true ? [] : void 0, import.meta.url);
19839
+ env2.allowLocalModels = false;
19840
+ const common = { progress_callback: onProgress };
19841
+ const hasWebGpu = typeof navigator !== "undefined" && "gpu" in navigator;
19842
+ if (hasWebGpu) {
19843
+ try {
19844
+ const s2 = await pipeline("text-to-speech", NEURAL_MODEL, { ...common, device: "webgpu" });
19845
+ console.info("[tts] neural backend: webgpu");
19846
+ return s2;
19847
+ } catch (err) {
19848
+ console.warn("[tts] WebGPU backend failed, falling back to WASM", err);
19849
+ }
19850
+ }
19851
+ const s = await pipeline("text-to-speech", NEURAL_MODEL, common);
19852
+ console.info("[tts] neural backend: wasm");
19853
+ return s;
19854
+ })();
19855
+ synthPromise.catch(() => {
19856
+ synthPromise = null;
19857
+ });
19858
+ }
19859
+ return synthPromise;
19860
+ }
19861
+ class SpeechController {
19862
+ queue = [];
19863
+ draining = false;
19864
+ audioCtx = null;
19865
+ currentSource = null;
19866
+ generation = 0;
19867
+ // bumped by stop() to abort in-flight work
19868
+ active = false;
19869
+ onModelProgress = null;
19870
+ // Fires true while a reply is being spoken/queued, false when idle — drives
19871
+ // the visible Stop button.
19872
+ onActivityChange = null;
19873
+ onError = null;
19874
+ // Prime/resume the AudioContext from a user gesture so neural playback isn't
19875
+ // blocked by the browser autoplay policy (the System engine is unaffected).
19876
+ unlock() {
19877
+ try {
19878
+ const ctx = this.ensureCtx();
19879
+ if (ctx.state === "suspended") void ctx.resume();
19880
+ } catch {
19881
+ }
19882
+ }
19883
+ // Start loading the neural model in the background (e.g. when the user picks
19884
+ // the neural engine) so the first utterance doesn't pay download/compile time.
19885
+ warmup() {
19886
+ if (getEngine() !== "neural") return;
19887
+ void loadSynthesizer((p) => this.onModelProgress?.(p)).then(() => this.onModelProgress?.(null)).catch(() => {
19888
+ this.onModelProgress?.(null);
19889
+ });
19890
+ }
19891
+ enqueue(rawText) {
19892
+ const text2 = cleanForSpeech(rawText);
19893
+ if (!text2) return;
19894
+ this.queue.push(...splitIntoChunks(text2));
19895
+ void this.drain();
19896
+ }
19897
+ stop() {
19898
+ this.generation++;
19899
+ this.queue = [];
19900
+ this.draining = false;
19901
+ if (typeof speechSynthesis !== "undefined") {
19902
+ try {
19903
+ speechSynthesis.cancel();
19904
+ } catch {
19905
+ }
19906
+ }
19907
+ if (this.currentSource) {
19908
+ try {
19909
+ this.currentSource.stop();
19910
+ } catch {
19911
+ }
19912
+ this.currentSource = null;
19913
+ }
19914
+ this.onModelProgress?.(null);
19915
+ this.setActive(false);
19916
+ }
19917
+ ensureCtx() {
19918
+ if (!this.audioCtx) {
19919
+ const Ctx = window.AudioContext || window.webkitAudioContext;
19920
+ this.audioCtx = new Ctx();
19921
+ }
19922
+ return this.audioCtx;
19923
+ }
19924
+ setActive(a) {
19925
+ if (this.active === a) return;
19926
+ this.active = a;
19927
+ this.onActivityChange?.(a);
19928
+ }
19929
+ async drain() {
19930
+ if (this.draining) return;
19931
+ this.draining = true;
19932
+ const gen = this.generation;
19933
+ this.setActive(true);
19934
+ while (this.queue.length && gen === this.generation) {
19935
+ const chunk = this.queue.shift();
19936
+ try {
19937
+ if (getEngine() === "neural") await this.speakNeural(chunk, gen);
19938
+ else await this.speakWeb(chunk, gen);
19939
+ } catch (err) {
19940
+ console.error("[tts] speak failed", err);
19941
+ this.onModelProgress?.(null);
19942
+ const detail = err instanceof Error ? err.message : String(err);
19943
+ this.onError?.(getEngine() === "neural" ? `Neural voice failed: ${detail}` : "Speech failed");
19944
+ }
19945
+ }
19946
+ if (gen === this.generation) {
19947
+ this.draining = false;
19948
+ this.setActive(false);
19949
+ }
19950
+ }
19951
+ speakWeb(text2, gen) {
19952
+ return new Promise((resolve2) => {
19953
+ if (typeof speechSynthesis === "undefined" || gen !== this.generation) return resolve2();
19954
+ const u = new SpeechSynthesisUtterance(text2);
19955
+ u.rate = getRate();
19956
+ const wantUri = getVoiceURI();
19957
+ if (wantUri) {
19958
+ const v2 = speechSynthesis.getVoices().find((vv) => vv.voiceURI === wantUri);
19959
+ if (v2) u.voice = v2;
19960
+ }
19961
+ u.onend = () => resolve2();
19962
+ u.onerror = () => resolve2();
19963
+ speechSynthesis.speak(u);
19964
+ });
19965
+ }
19966
+ async speakNeural(text2, gen) {
19967
+ console.info("[tts] neural: loading model…");
19968
+ this.onModelProgress?.({ status: "loading" });
19969
+ const synth = await loadSynthesizer((p) => this.onModelProgress?.(p));
19970
+ if (gen !== this.generation) return;
19971
+ console.info("[tts] neural: synthesizing", JSON.stringify(text2.slice(0, 60)));
19972
+ const out = await synth(text2);
19973
+ const raw = Array.isArray(out) ? out[0] : out;
19974
+ this.onModelProgress?.(null);
19975
+ if (gen !== this.generation) return;
19976
+ if (!raw?.audio?.length) throw new Error("neural synth returned no audio");
19977
+ console.info(`[tts] neural: playing ${raw.audio.length} samples @ ${raw.sampling_rate}Hz`);
19978
+ await this.playPcm(raw.audio, raw.sampling_rate, gen);
19979
+ }
19980
+ async playPcm(pcm, sampleRate, gen) {
19981
+ if (gen !== this.generation) return;
19982
+ const ctx = this.ensureCtx();
19983
+ if (ctx.state === "suspended") {
19984
+ try {
19985
+ await ctx.resume();
19986
+ } catch {
19987
+ }
19988
+ }
19989
+ if (gen !== this.generation) return;
19990
+ return new Promise((resolve2) => {
19991
+ const buffer = ctx.createBuffer(1, pcm.length, sampleRate);
19992
+ buffer.getChannelData(0).set(pcm);
19993
+ const source = ctx.createBufferSource();
19994
+ source.buffer = buffer;
19995
+ source.connect(ctx.destination);
19996
+ source.onended = () => {
19997
+ if (this.currentSource === source) this.currentSource = null;
19998
+ resolve2();
19999
+ };
20000
+ this.currentSource = source;
20001
+ source.start();
20002
+ });
20003
+ }
20004
+ }
20005
+ const speech = new SpeechController();
20006
+ const SAMPLE = "This is how spoken agent replies will sound.";
20007
+ function SpeakControls() {
20008
+ const [enabled, setEnabled] = reactExports.useState(false);
20009
+ const [engine, setEngineState] = reactExports.useState(getEngine);
20010
+ const [voices, setVoices] = reactExports.useState([]);
20011
+ const [voiceURI, setVoiceURIState] = reactExports.useState(getVoiceURI);
20012
+ const [rate, setRateState] = reactExports.useState(getRate);
20013
+ const [showMenu, setShowMenu] = reactExports.useState(false);
20014
+ const [modelPct, setModelPct] = reactExports.useState(null);
20015
+ const [speaking, setSpeaking] = reactExports.useState(false);
20016
+ const [error, setError] = reactExports.useState(null);
20017
+ const wrapRef = reactExports.useRef(null);
20018
+ reactExports.useEffect(() => {
20019
+ let alive = true;
20020
+ window.worker.getSpeakReplies().then((r) => {
20021
+ if (alive) setEnabled(!!r.enabled);
20022
+ }).catch(() => {
20023
+ });
20024
+ return () => {
20025
+ alive = false;
20026
+ };
20027
+ }, []);
20028
+ reactExports.useEffect(() => {
20029
+ const off = window.worker.onAgentMessage((text2) => speech.enqueue(text2));
20030
+ return off;
20031
+ }, []);
20032
+ reactExports.useEffect(() => {
20033
+ speech.onModelProgress = (p) => {
20034
+ if (p && p.status === "progress" && typeof p.progress === "number") {
20035
+ setModelPct(Math.min(100, Math.round(p.progress)));
20036
+ } else if (!p) {
20037
+ setModelPct(null);
20038
+ }
20039
+ };
20040
+ speech.onActivityChange = (a) => setSpeaking(a);
20041
+ speech.onError = (msg) => setError(msg);
20042
+ return () => {
20043
+ speech.onModelProgress = null;
20044
+ speech.onActivityChange = null;
20045
+ speech.onError = null;
20046
+ };
20047
+ }, []);
20048
+ reactExports.useEffect(() => {
20049
+ if (!error) return;
20050
+ const t = setTimeout(() => setError(null), 4e3);
20051
+ return () => clearTimeout(t);
20052
+ }, [error]);
20053
+ reactExports.useEffect(() => {
20054
+ const load = () => setVoices(listWebVoices());
20055
+ load();
20056
+ if (typeof speechSynthesis !== "undefined") {
20057
+ speechSynthesis.addEventListener("voiceschanged", load);
20058
+ return () => speechSynthesis.removeEventListener("voiceschanged", load);
20059
+ }
20060
+ }, []);
20061
+ reactExports.useEffect(() => {
20062
+ if (!showMenu) return;
20063
+ const onDown = (e) => {
20064
+ if (!wrapRef.current?.contains(e.target)) setShowMenu(false);
20065
+ };
20066
+ document.addEventListener("mousedown", onDown);
20067
+ return () => document.removeEventListener("mousedown", onDown);
20068
+ }, [showMenu]);
20069
+ const toggle = reactExports.useCallback(async () => {
20070
+ const next = !enabled;
20071
+ setEnabled(next);
20072
+ if (next) speech.unlock();
20073
+ else speech.stop();
20074
+ try {
20075
+ await window.worker.setSpeakReplies(next);
20076
+ } catch {
20077
+ }
20078
+ }, [enabled]);
20079
+ const chooseEngine = reactExports.useCallback((id) => {
20080
+ setEngine(id);
20081
+ setEngineState(id);
20082
+ speech.stop();
20083
+ if (id === "neural") speech.warmup();
20084
+ }, []);
20085
+ const chooseVoice = reactExports.useCallback((uri) => {
20086
+ const v2 = uri || null;
20087
+ setVoiceURI(v2);
20088
+ setVoiceURIState(v2);
20089
+ }, []);
20090
+ const changeRate = reactExports.useCallback((r) => {
20091
+ setRate(r);
20092
+ setRateState(r);
20093
+ }, []);
20094
+ const testVoice = reactExports.useCallback(() => {
20095
+ speech.unlock();
20096
+ speech.stop();
20097
+ speech.enqueue(SAMPLE);
20098
+ }, []);
20099
+ const title = enabled ? "Spoken replies on — click to mute" : "Speak agent replies (off)";
20100
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "speak-controls", ref: wrapRef, children: [
20101
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
20102
+ "button",
20103
+ {
20104
+ type: "button",
20105
+ className: `titlebar-btn titlebar-icon-btn speak-btn ${enabled ? "active" : ""}`,
20106
+ onClick: toggle,
20107
+ title,
20108
+ "aria-label": "Toggle spoken agent replies",
20109
+ "aria-pressed": enabled,
20110
+ children: [
20111
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
20112
+ "svg",
20113
+ {
20114
+ viewBox: "0 0 24 24",
20115
+ width: "13",
20116
+ height: "13",
20117
+ fill: "none",
20118
+ stroke: "currentColor",
20119
+ strokeWidth: "2",
20120
+ strokeLinecap: "round",
20121
+ strokeLinejoin: "round",
20122
+ "aria-hidden": "true",
20123
+ children: [
20124
+ /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5" }),
20125
+ enabled ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
20126
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M15.54 8.46a5 5 0 0 1 0 7.07" }),
20127
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M19.07 4.93a10 10 0 0 1 0 14.14" })
20128
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "23", y1: "9", x2: "17", y2: "15" })
20129
+ ]
20130
+ }
20131
+ ),
20132
+ modelPct !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "speak-pct", children: [
20133
+ modelPct,
20134
+ "%"
20135
+ ] })
20136
+ ]
20137
+ }
20138
+ ),
20139
+ speaking && /* @__PURE__ */ jsxRuntimeExports.jsx(
20140
+ "button",
20141
+ {
20142
+ type: "button",
20143
+ className: "titlebar-btn titlebar-icon-btn speak-stop",
20144
+ onClick: () => speech.stop(),
20145
+ title: "Stop speaking",
20146
+ "aria-label": "Stop speaking",
20147
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { viewBox: "0 0 24 24", width: "11", height: "11", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "5", y: "5", width: "14", height: "14", rx: "2" }) })
20148
+ }
20149
+ ),
20150
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
20151
+ "button",
20152
+ {
20153
+ type: "button",
20154
+ className: "titlebar-btn speak-caret",
20155
+ onClick: () => setShowMenu((v2) => !v2),
20156
+ title: "Voice options",
20157
+ "aria-label": "Voice options",
20158
+ "aria-expanded": showMenu,
20159
+ children: "▾"
20160
+ }
20161
+ ),
20162
+ showMenu && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "speak-menu", role: "menu", children: [
20163
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "speak-menu-head", children: "Engine" }),
20164
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
20165
+ "button",
20166
+ {
20167
+ type: "button",
20168
+ className: `speak-menu-item ${engine === "web" ? "active" : ""}`,
20169
+ onClick: () => chooseEngine("web"),
20170
+ children: [
20171
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "speak-menu-check", children: engine === "web" ? "✓" : "" }),
20172
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "System voice (instant)" })
20173
+ ]
20174
+ }
20175
+ ),
20176
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
20177
+ "button",
20178
+ {
20179
+ type: "button",
20180
+ className: `speak-menu-item ${engine === "neural" ? "active" : ""}`,
20181
+ onClick: () => chooseEngine("neural"),
20182
+ children: [
20183
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "speak-menu-check", children: engine === "neural" ? "✓" : "" }),
20184
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Neural voice (downloads)" })
20185
+ ]
20186
+ }
20187
+ ),
20188
+ engine === "web" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
20189
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "speak-menu-head", children: "Voice" }),
20190
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
20191
+ "select",
20192
+ {
20193
+ className: "speak-select",
20194
+ value: voiceURI || "",
20195
+ onChange: (e) => chooseVoice(e.target.value),
20196
+ children: [
20197
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: "System default" }),
20198
+ voices.map((v2) => /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: v2.voiceURI, children: [
20199
+ v2.name,
20200
+ " ",
20201
+ v2.lang ? `(${v2.lang})` : ""
20202
+ ] }, v2.voiceURI))
20203
+ ]
20204
+ }
20205
+ )
20206
+ ] }),
20207
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "speak-menu-head", children: [
20208
+ "Rate · ",
20209
+ rate.toFixed(1),
20210
+ "×"
20211
+ ] }),
20212
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
20213
+ "input",
20214
+ {
20215
+ className: "speak-rate",
20216
+ type: "range",
20217
+ min: 0.5,
20218
+ max: 2,
20219
+ step: 0.1,
20220
+ value: rate,
20221
+ onChange: (e) => changeRate(Number(e.target.value))
20222
+ }
20223
+ ),
20224
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "speak-menu-row", children: [
20225
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "speak-menu-btn", onClick: testVoice, children: "Test" }),
20226
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "speak-menu-btn", onClick: () => speech.stop(), children: "Stop" })
20227
+ ] })
20228
+ ] }),
20229
+ error && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "speak-error", children: error })
19464
20230
  ] });
19465
20231
  }
19466
20232
  function CtlsurfPanel({ navigate }) {
@@ -207057,7 +207823,7 @@ const lessDefaults = new LanguageServiceDefaultsImpl$3(
207057
207823
  modeConfigurationDefault$2
207058
207824
  );
207059
207825
  function getMode$3() {
207060
- return __vitePreload(() => import("./cssMode-DbMmcl1h.js"), true ? __vite__mapDeps([0,1]) : void 0, import.meta.url);
207826
+ return __vitePreload(() => import("./cssMode-BQN8v2ok.js"), true ? __vite__mapDeps([0,1]) : void 0, import.meta.url);
207061
207827
  }
207062
207828
  languages.onLanguage("less", () => {
207063
207829
  getMode$3().then((mode2) => mode2.setupMode(lessDefaults));
@@ -207162,7 +207928,7 @@ const razorLanguageService = registerHTMLLanguageService(
207162
207928
  );
207163
207929
  const razorDefaults = razorLanguageService.defaults;
207164
207930
  function getMode$2() {
207165
- return __vitePreload(() => import("./htmlMode-BdkAp9qr.js"), true ? __vite__mapDeps([2,1]) : void 0, import.meta.url);
207931
+ return __vitePreload(() => import("./htmlMode-M3MApZ4n.js"), true ? __vite__mapDeps([2,1]) : void 0, import.meta.url);
207166
207932
  }
207167
207933
  function registerHTMLLanguageService(languageId, options = optionsDefault, modeConfiguration = getConfigurationDefault(languageId)) {
207168
207934
  const defaults = new LanguageServiceDefaultsImpl$2(languageId, options, modeConfiguration);
@@ -207246,7 +208012,7 @@ const jsonDefaults = new LanguageServiceDefaultsImpl$1(
207246
208012
  );
207247
208013
  const getWorker$1 = () => getMode$1().then((mode2) => mode2.getWorker());
207248
208014
  function getMode$1() {
207249
- return __vitePreload(() => import("./jsonMode-BuVr-eSl.js"), true ? __vite__mapDeps([3,1]) : void 0, import.meta.url);
208015
+ return __vitePreload(() => import("./jsonMode-CKp2zvZu.js"), true ? __vite__mapDeps([3,1]) : void 0, import.meta.url);
207250
208016
  }
207251
208017
  languages.register({
207252
208018
  id: "json",
@@ -207492,7 +208258,7 @@ const getJavaScriptWorker = () => {
207492
208258
  return getMode().then((mode) => mode.getJavaScriptWorker());
207493
208259
  };
207494
208260
  function getMode() {
207495
- return __vitePreload(() => import("./tsMode-CYd3NUkW.js"), true ? [] : void 0, import.meta.url);
208261
+ return __vitePreload(() => import("./tsMode-yAjlPR-D.js"), true ? [] : void 0, import.meta.url);
207496
208262
  }
207497
208263
  languages.onLanguage("typescript", () => {
207498
208264
  return getMode().then((mode) => mode.setupTypeScript(typescriptDefaults));
@@ -207687,49 +208453,49 @@ registerLanguage({
207687
208453
  extensions: [".ftl", ".ftlh", ".ftlx"],
207688
208454
  aliases: ["FreeMarker2", "Apache FreeMarker2"],
207689
208455
  loader: () => {
207690
- return __vitePreload(() => import("./freemarker2-CvaHiy92.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
208456
+ return __vitePreload(() => import("./freemarker2-DbxGYYVp.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
207691
208457
  }
207692
208458
  });
207693
208459
  registerLanguage({
207694
208460
  id: "freemarker2.tag-angle.interpolation-dollar",
207695
208461
  aliases: ["FreeMarker2 (Angle/Dollar)", "Apache FreeMarker2 (Angle/Dollar)"],
207696
208462
  loader: () => {
207697
- return __vitePreload(() => import("./freemarker2-CvaHiy92.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationDollar);
208463
+ return __vitePreload(() => import("./freemarker2-DbxGYYVp.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationDollar);
207698
208464
  }
207699
208465
  });
207700
208466
  registerLanguage({
207701
208467
  id: "freemarker2.tag-bracket.interpolation-dollar",
207702
208468
  aliases: ["FreeMarker2 (Bracket/Dollar)", "Apache FreeMarker2 (Bracket/Dollar)"],
207703
208469
  loader: () => {
207704
- return __vitePreload(() => import("./freemarker2-CvaHiy92.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationDollar);
208470
+ return __vitePreload(() => import("./freemarker2-DbxGYYVp.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationDollar);
207705
208471
  }
207706
208472
  });
207707
208473
  registerLanguage({
207708
208474
  id: "freemarker2.tag-angle.interpolation-bracket",
207709
208475
  aliases: ["FreeMarker2 (Angle/Bracket)", "Apache FreeMarker2 (Angle/Bracket)"],
207710
208476
  loader: () => {
207711
- return __vitePreload(() => import("./freemarker2-CvaHiy92.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationBracket);
208477
+ return __vitePreload(() => import("./freemarker2-DbxGYYVp.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAngleInterpolationBracket);
207712
208478
  }
207713
208479
  });
207714
208480
  registerLanguage({
207715
208481
  id: "freemarker2.tag-bracket.interpolation-bracket",
207716
208482
  aliases: ["FreeMarker2 (Bracket/Bracket)", "Apache FreeMarker2 (Bracket/Bracket)"],
207717
208483
  loader: () => {
207718
- return __vitePreload(() => import("./freemarker2-CvaHiy92.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationBracket);
208484
+ return __vitePreload(() => import("./freemarker2-DbxGYYVp.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagBracketInterpolationBracket);
207719
208485
  }
207720
208486
  });
207721
208487
  registerLanguage({
207722
208488
  id: "freemarker2.tag-auto.interpolation-dollar",
207723
208489
  aliases: ["FreeMarker2 (Auto/Dollar)", "Apache FreeMarker2 (Auto/Dollar)"],
207724
208490
  loader: () => {
207725
- return __vitePreload(() => import("./freemarker2-CvaHiy92.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
208491
+ return __vitePreload(() => import("./freemarker2-DbxGYYVp.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationDollar);
207726
208492
  }
207727
208493
  });
207728
208494
  registerLanguage({
207729
208495
  id: "freemarker2.tag-auto.interpolation-bracket",
207730
208496
  aliases: ["FreeMarker2 (Auto/Bracket)", "Apache FreeMarker2 (Auto/Bracket)"],
207731
208497
  loader: () => {
207732
- return __vitePreload(() => import("./freemarker2-CvaHiy92.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationBracket);
208498
+ return __vitePreload(() => import("./freemarker2-DbxGYYVp.js"), true ? [] : void 0, import.meta.url).then((m) => m.TagAutoInterpolationBracket);
207733
208499
  }
207734
208500
  });
207735
208501
  registerLanguage({
@@ -207750,7 +208516,7 @@ registerLanguage({
207750
208516
  extensions: [".handlebars", ".hbs"],
207751
208517
  aliases: ["Handlebars", "handlebars", "hbs"],
207752
208518
  mimetypes: ["text/x-handlebars-template"],
207753
- loader: () => __vitePreload(() => import("./handlebars-D58lUIOu.js"), true ? [] : void 0, import.meta.url)
208519
+ loader: () => __vitePreload(() => import("./handlebars-3auU1CAd.js"), true ? [] : void 0, import.meta.url)
207754
208520
  });
207755
208521
  registerLanguage({
207756
208522
  id: "hcl",
@@ -207763,7 +208529,7 @@ registerLanguage({
207763
208529
  extensions: [".html", ".htm", ".shtml", ".xhtml", ".mdoc", ".jsp", ".asp", ".aspx", ".jshtm"],
207764
208530
  aliases: ["HTML", "htm", "html", "xhtml"],
207765
208531
  mimetypes: ["text/html", "text/x-jshtm", "text/template", "text/ng-template"],
207766
- loader: () => __vitePreload(() => import("./html-D1h1aJbM.js"), true ? [] : void 0, import.meta.url)
208532
+ loader: () => __vitePreload(() => import("./html-D8xFiRmI.js"), true ? [] : void 0, import.meta.url)
207767
208533
  });
207768
208534
  registerLanguage({
207769
208535
  id: "ini",
@@ -207786,7 +208552,7 @@ registerLanguage({
207786
208552
  filenames: ["jakefile"],
207787
208553
  aliases: ["JavaScript", "javascript", "js"],
207788
208554
  mimetypes: ["text/javascript"],
207789
- loader: () => __vitePreload(() => import("./javascript-CXqZcnvb.js"), true ? __vite__mapDeps([4,5]) : void 0, import.meta.url)
208555
+ loader: () => __vitePreload(() => import("./javascript-BO_ViZM5.js"), true ? __vite__mapDeps([4,5]) : void 0, import.meta.url)
207790
208556
  });
207791
208557
  registerLanguage({
207792
208558
  id: "julia",
@@ -207825,7 +208591,7 @@ registerLanguage({
207825
208591
  extensions: [".liquid", ".html.liquid"],
207826
208592
  aliases: ["Liquid", "liquid"],
207827
208593
  mimetypes: ["application/liquid"],
207828
- loader: () => __vitePreload(() => import("./liquid-LKu0Wd0B.js"), true ? [] : void 0, import.meta.url)
208594
+ loader: () => __vitePreload(() => import("./liquid-C1eHcrht.js"), true ? [] : void 0, import.meta.url)
207829
208595
  });
207830
208596
  registerLanguage({
207831
208597
  id: "m3",
@@ -207843,7 +208609,7 @@ registerLanguage({
207843
208609
  id: "mdx",
207844
208610
  extensions: [".mdx"],
207845
208611
  aliases: ["MDX", "mdx"],
207846
- loader: () => __vitePreload(() => import("./mdx-Bl84ILla.js"), true ? [] : void 0, import.meta.url)
208612
+ loader: () => __vitePreload(() => import("./mdx-Qqdtk7fL.js"), true ? [] : void 0, import.meta.url)
207847
208613
  });
207848
208614
  registerLanguage({
207849
208615
  id: "mips",
@@ -207942,7 +208708,7 @@ registerLanguage({
207942
208708
  extensions: [".py", ".rpy", ".pyw", ".cpy", ".gyp", ".gypi"],
207943
208709
  aliases: ["Python", "py"],
207944
208710
  firstLine: "^#!/.*\\bpython[0-9.-]*\\b",
207945
- loader: () => __vitePreload(() => import("./python-0sFd9G1k.js"), true ? [] : void 0, import.meta.url)
208711
+ loader: () => __vitePreload(() => import("./python-DKu7rNbs.js"), true ? [] : void 0, import.meta.url)
207946
208712
  });
207947
208713
  registerLanguage({
207948
208714
  id: "qsharp",
@@ -207961,7 +208727,7 @@ registerLanguage({
207961
208727
  extensions: [".cshtml"],
207962
208728
  aliases: ["Razor", "razor"],
207963
208729
  mimetypes: ["text/x-cshtml"],
207964
- loader: () => __vitePreload(() => import("./razor-Cqcu1rLJ.js"), true ? [] : void 0, import.meta.url)
208730
+ loader: () => __vitePreload(() => import("./razor-BOMpCo6z.js"), true ? [] : void 0, import.meta.url)
207965
208731
  });
207966
208732
  registerLanguage({
207967
208733
  id: "redis",
@@ -208094,7 +208860,7 @@ registerLanguage({
208094
208860
  aliases: ["TypeScript", "ts", "typescript"],
208095
208861
  mimetypes: ["text/typescript"],
208096
208862
  loader: () => {
208097
- return __vitePreload(() => import("./typescript-rkc9lhpi.js"), true ? [] : void 0, import.meta.url);
208863
+ return __vitePreload(() => import("./typescript-BiJRCUcL.js"), true ? [] : void 0, import.meta.url);
208098
208864
  }
208099
208865
  });
208100
208866
  registerLanguage({
@@ -208139,14 +208905,14 @@ registerLanguage({
208139
208905
  firstLine: "(\\<\\?xml.*)|(\\<svg)|(\\<\\!doctype\\s+svg)",
208140
208906
  aliases: ["XML", "xml"],
208141
208907
  mimetypes: ["text/xml", "application/xml", "application/xaml+xml", "application/xml-dtd"],
208142
- loader: () => __vitePreload(() => import("./xml-EsHEUps1.js"), true ? [] : void 0, import.meta.url)
208908
+ loader: () => __vitePreload(() => import("./xml-D4PvYeQq.js"), true ? [] : void 0, import.meta.url)
208143
208909
  });
208144
208910
  registerLanguage({
208145
208911
  id: "yaml",
208146
208912
  extensions: [".yaml", ".yml"],
208147
208913
  aliases: ["YAML", "yaml", "YML", "yml"],
208148
208914
  mimetypes: ["application/x-yaml", "text/x-yaml"],
208149
- loader: () => __vitePreload(() => import("./yaml-B9-nQ_s2.js"), true ? [] : void 0, import.meta.url)
208915
+ loader: () => __vitePreload(() => import("./yaml-BeHVkmnS.js"), true ? [] : void 0, import.meta.url)
208150
208916
  });
208151
208917
  var __defProp = Object.defineProperty;
208152
208918
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -212549,6 +213315,20 @@ function App() {
212549
213315
  const [activeTabId, setActiveTabId] = reactExports.useState(tabs[0].id);
212550
213316
  const [trackingActive, setTrackingActive] = reactExports.useState(false);
212551
213317
  const [showTicketPanel, setShowTicketPanel] = reactExports.useState(false);
213318
+ const [showFloatingMic, setShowFloatingMic] = reactExports.useState(() => {
213319
+ try {
213320
+ return localStorage.getItem("ctlsurf.floatingMicVisible") !== "false";
213321
+ } catch {
213322
+ return true;
213323
+ }
213324
+ });
213325
+ const setFloatingMicVisible = reactExports.useCallback((v2) => {
213326
+ setShowFloatingMic(v2);
213327
+ try {
213328
+ localStorage.setItem("ctlsurf.floatingMicVisible", String(v2));
213329
+ } catch {
213330
+ }
213331
+ }, []);
212552
213332
  const [pickerTargetTabId, setPickerTargetTabId] = reactExports.useState(tabs[0].id);
212553
213333
  const [showAgentPicker, setShowAgentPicker] = reactExports.useState(true);
212554
213334
  const visiblePaneIds = findPaneIds(layout2);
@@ -212619,7 +213399,7 @@ function App() {
212619
213399
  const handleVoiceTranscript = reactExports.useCallback((text2) => {
212620
213400
  const trimmed = text2.trim();
212621
213401
  if (!trimmed) return;
212622
- window.worker.writePty(activeTabId, trimmed);
213402
+ window.worker.writePty(activeTabId, trimmed + "\r");
212623
213403
  focusTerminal(activeTabId);
212624
213404
  }, [activeTabId]);
212625
213405
  const cwdRef = reactExports.useRef(null);
@@ -212883,7 +213663,36 @@ function App() {
212883
213663
  ]
212884
213664
  }
212885
213665
  ),
212886
- /* @__PURE__ */ jsxRuntimeExports.jsx(VoiceInput, { onTranscript: handleVoiceTranscript }),
213666
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
213667
+ "button",
213668
+ {
213669
+ className: `titlebar-btn titlebar-icon-btn ${showFloatingMic ? "active" : ""}`,
213670
+ onClick: () => setFloatingMicVisible(!showFloatingMic),
213671
+ title: showFloatingMic ? "Hide floating mic" : "Show floating mic",
213672
+ "aria-label": "Toggle floating mic",
213673
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
213674
+ "svg",
213675
+ {
213676
+ viewBox: "0 0 24 24",
213677
+ width: "13",
213678
+ height: "13",
213679
+ fill: "none",
213680
+ stroke: "currentColor",
213681
+ strokeWidth: "2",
213682
+ strokeLinecap: "round",
213683
+ strokeLinejoin: "round",
213684
+ "aria-hidden": "true",
213685
+ children: [
213686
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }),
213687
+ /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }),
213688
+ /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "12", y1: "19", x2: "12", y2: "23" }),
213689
+ /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "8", y1: "23", x2: "16", y2: "23" })
213690
+ ]
213691
+ }
213692
+ )
213693
+ }
213694
+ ),
213695
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SpeakControls, {}),
212887
213696
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "titlebar-separator" }),
212888
213697
  agents.map((a) => {
212889
213698
  const activeTab = tabs.find((t) => t.id === activeTabId);
@@ -212937,6 +213746,13 @@ function App() {
212937
213746
  }
212938
213747
  }
212939
213748
  }
213749
+ ),
213750
+ showFloatingMic && /* @__PURE__ */ jsxRuntimeExports.jsx(
213751
+ FloatingMic,
213752
+ {
213753
+ onTranscript: handleVoiceTranscript,
213754
+ onHide: () => setFloatingMicVisible(false)
213755
+ }
212940
213756
  )
212941
213757
  ] });
212942
213758
  }