@agentprojectcontext/apx 1.29.0 → 1.30.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.
package/package.json
CHANGED
|
@@ -36,6 +36,14 @@
|
|
|
36
36
|
// doesn't talk into the dead gap before the recorder starts.
|
|
37
37
|
let micReady = false;
|
|
38
38
|
|
|
39
|
+
// Dead-mic detection: track the loudest RMS seen this session. If it stays
|
|
40
|
+
// near zero for DEAD_MIC_MS the stream is silent (no permission / muted /
|
|
41
|
+
// wrong device) and we surface a notice instead of hanging in "listening".
|
|
42
|
+
let listenStartTs = 0;
|
|
43
|
+
let micPeakRms = 0;
|
|
44
|
+
const DEAD_MIC_MS = 3500;
|
|
45
|
+
const DEAD_MIC_RMS = 0.004;
|
|
46
|
+
|
|
39
47
|
// Silence auto-send: once speech has been heard, SILENCE_MS of quiet
|
|
40
48
|
// auto-commits the recording. RMS (time-domain) is the voice/silence gate.
|
|
41
49
|
// Both are overridable from config.json (desktop.silence_ms / voice_rms).
|
|
@@ -760,6 +768,8 @@
|
|
|
760
768
|
micReady = false; // show "Cargando…" until the recorder is actually live
|
|
761
769
|
speechSeen = false;
|
|
762
770
|
lastVoiceTs = 0;
|
|
771
|
+
listenStartTs = 0;
|
|
772
|
+
micPeakRms = 0;
|
|
763
773
|
pausePreviewed = false;
|
|
764
774
|
reuseLiveOnStop = false;
|
|
765
775
|
livePromise = null;
|
|
@@ -880,13 +890,42 @@
|
|
|
880
890
|
// won't auto-send (speechSeen gates that).
|
|
881
891
|
micReady = true;
|
|
882
892
|
lastVoiceTs = Date.now();
|
|
893
|
+
listenStartTs = Date.now();
|
|
894
|
+
micPeakRms = 0;
|
|
883
895
|
if (mode === "listening") render();
|
|
884
896
|
} catch (e) {
|
|
897
|
+
// getUserMedia failed → classify and tell the user, instead of silently
|
|
898
|
+
// bailing to idle (which looks like "it just doesn't work").
|
|
885
899
|
console.error("desktop renderer: mic error", e);
|
|
900
|
+
micReady = false;
|
|
886
901
|
mode = "idle";
|
|
902
|
+
const name = e?.name || "";
|
|
903
|
+
let notice;
|
|
904
|
+
if (name === "NotAllowedError" || name === "SecurityError" || /permission|denied/i.test(e?.message || "")) {
|
|
905
|
+
notice = "Roby no tiene permiso para el micrófono. Activalo en Ajustes del sistema › Privacidad y seguridad › Micrófono, y volvé a intentar.";
|
|
906
|
+
} else if (name === "NotFoundError" || name === "DevicesNotFoundError" || name === "OverconstrainedError") {
|
|
907
|
+
notice = "No encontré ningún micrófono conectado. Revisá el dispositivo de entrada y reintentá.";
|
|
908
|
+
} else {
|
|
909
|
+
notice = "No pude abrir el micrófono (" + (name || e?.message || "error") + "). Reintentá o revisá los permisos.";
|
|
910
|
+
}
|
|
911
|
+
showMicNotice(notice);
|
|
887
912
|
render();
|
|
888
913
|
}
|
|
889
914
|
}
|
|
915
|
+
// Visual-only system notice in the conversation (not a message, not history).
|
|
916
|
+
function showMicNotice(text) {
|
|
917
|
+
ensureConv();
|
|
918
|
+
if ($convScroll) {
|
|
919
|
+
// Replace any prior notice so they don't stack.
|
|
920
|
+
$convScroll.querySelector(".sys-notice")?.remove();
|
|
921
|
+
const el = document.createElement("div");
|
|
922
|
+
el.className = "sys-notice";
|
|
923
|
+
el.innerHTML = `<span class="ic">${ICON.mic()}</span><span>${escapeHtml(text)}</span>`;
|
|
924
|
+
$convScroll.appendChild(el);
|
|
925
|
+
scrollConvToBottom();
|
|
926
|
+
}
|
|
927
|
+
requestWindowResize();
|
|
928
|
+
}
|
|
890
929
|
function stopMic() {
|
|
891
930
|
try { mediaRecorder?.stop(); } catch {}
|
|
892
931
|
try { audioStream?.getTracks().forEach((t) => t.stop()); } catch {}
|
|
@@ -926,6 +965,19 @@
|
|
|
926
965
|
}
|
|
927
966
|
const rms = Math.sqrt(sumSq / timeData.length);
|
|
928
967
|
const now = Date.now();
|
|
968
|
+
if (rms > micPeakRms) micPeakRms = rms;
|
|
969
|
+
// Dead-mic guard: a real mic always has a noise floor (rms ≳ 0.005). If
|
|
970
|
+
// after DEAD_MIC_MS the signal is essentially flat zero, the stream is
|
|
971
|
+
// muted / wrong device / OS-blocked — tell the user instead of sitting
|
|
972
|
+
// in "listening" forever waiting for speech that can't arrive.
|
|
973
|
+
if (!speechSeen && listenStartTs && now - listenStartTs > DEAD_MIC_MS && micPeakRms < DEAD_MIC_RMS) {
|
|
974
|
+
waveRaf = null;
|
|
975
|
+
stopMic();
|
|
976
|
+
mode = "idle";
|
|
977
|
+
showMicNotice("No me está llegando audio del micrófono. Revisá que Roby tenga permiso (Ajustes del sistema › Privacidad y seguridad › Micrófono) y que esté seleccionado el micrófono correcto.");
|
|
978
|
+
render();
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
929
981
|
if (rms > VOICE_RMS) {
|
|
930
982
|
speechSeen = true;
|
|
931
983
|
lastVoiceTs = now;
|
|
@@ -361,6 +361,16 @@ button { font-family: inherit; }
|
|
|
361
361
|
.chip:hover { background: var(--glass-hairline); color: var(--ink); border-color: transparent; }
|
|
362
362
|
.chip.done { color: oklch(0.6 0.15 150); }
|
|
363
363
|
|
|
364
|
+
/* system notice (e.g. no mic permission / dead mic) — not a chat message */
|
|
365
|
+
.sys-notice {
|
|
366
|
+
margin: 8px 0 4px; padding: 9px 11px; border-radius: 11px;
|
|
367
|
+
display: flex; align-items: flex-start; gap: 8px;
|
|
368
|
+
background: color-mix(in oklch, oklch(0.72 0.17 55) 12%, transparent);
|
|
369
|
+
border: 1px solid color-mix(in oklch, oklch(0.72 0.17 55) 35%, transparent);
|
|
370
|
+
color: var(--ink); font-size: 12.5px; line-height: 1.4;
|
|
371
|
+
}
|
|
372
|
+
.sys-notice .ic { flex: none; color: oklch(0.62 0.18 45); display: inline-flex; margin-top: 1px; }
|
|
373
|
+
|
|
364
374
|
/* tool pill (when the agent runs a tool) */
|
|
365
375
|
.tool-pill {
|
|
366
376
|
display: inline-flex; align-items: center; gap: 6px;
|