@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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;