@agentprojectcontext/apx 1.28.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).
|
|
@@ -389,17 +397,28 @@
|
|
|
389
397
|
<div class="bubble-user">${escapeHtml(m.text)}${viaIcon}</div>
|
|
390
398
|
`;
|
|
391
399
|
} else {
|
|
392
|
-
|
|
400
|
+
// Consecutive agent messages (intro + post-tool answer …) read as one
|
|
401
|
+
// continued reply: only the FIRST shows the "Roby" header — the rest skip
|
|
402
|
+
// it so a tool turn isn't a stack of repeated "Roby" labels. A new header
|
|
403
|
+
// only appears when something (a user message) breaks the run.
|
|
404
|
+
const idx = messages.indexOf(m);
|
|
405
|
+
const prevMsg = idx > 0 ? messages[idx - 1] : null;
|
|
406
|
+
const agentCont = !!(prevMsg && prevMsg.role === "agent");
|
|
407
|
+
if (agentCont) t.classList.add("cont");
|
|
408
|
+
const header = agentCont ? "" : `
|
|
393
409
|
<div class="role agent">
|
|
394
410
|
<span class="ava sa"><img src="assets/superagent.png" alt=""/></span>
|
|
395
411
|
<span class="who">${escapeHtml(agentName)}</span>
|
|
396
412
|
<span class="time">${m.t || ""}</span>
|
|
397
|
-
</div
|
|
398
|
-
|
|
399
|
-
|
|
413
|
+
</div>`;
|
|
414
|
+
// Copy is an inline icon at the end of the text, hover-only, so it never
|
|
415
|
+
// reserves an empty row. Regenerate lives in turn-actions and CSS shows it
|
|
416
|
+
// only on the last turn.
|
|
417
|
+
t.innerHTML = `
|
|
418
|
+
${header}
|
|
419
|
+
<div class="msg-agent">${formatWordsHtml(m.text)}<button class="btn-copy" aria-label="Copiar" title="Copiar">${ICON.copy()}</button></div>
|
|
400
420
|
<div class="turn-actions">
|
|
401
421
|
<button class="chip btn-regen">${ICON.refresh()} Regenerar</button>
|
|
402
|
-
<button class="chip btn-copy">${ICON.copy()} Copiar</button>
|
|
403
422
|
</div>
|
|
404
423
|
`;
|
|
405
424
|
if (m.audio && m.dur) {
|
|
@@ -409,13 +428,13 @@
|
|
|
409
428
|
actions.insertAdjacentHTML("beforebegin", scrubberHtml);
|
|
410
429
|
wireScrubber(t, m);
|
|
411
430
|
}
|
|
412
|
-
// copy
|
|
431
|
+
// copy (inline icon → swaps to a check briefly)
|
|
413
432
|
t.querySelector(".btn-copy")?.addEventListener("click", (e) => {
|
|
414
433
|
navigator.clipboard?.writeText(m.text).catch(() => {});
|
|
415
434
|
const btn = e.currentTarget;
|
|
416
435
|
btn.classList.add("done");
|
|
417
|
-
btn.innerHTML =
|
|
418
|
-
setTimeout(() => { btn.classList.remove("done"); btn.innerHTML =
|
|
436
|
+
btn.innerHTML = ICON.check();
|
|
437
|
+
setTimeout(() => { btn.classList.remove("done"); btn.innerHTML = ICON.copy(); }, 1400);
|
|
419
438
|
});
|
|
420
439
|
// regen: only the LAST agent turn can be regenerated. Past turns
|
|
421
440
|
// can't because we'd have to re-issue the user prompt that came right
|
|
@@ -749,6 +768,8 @@
|
|
|
749
768
|
micReady = false; // show "Cargando…" until the recorder is actually live
|
|
750
769
|
speechSeen = false;
|
|
751
770
|
lastVoiceTs = 0;
|
|
771
|
+
listenStartTs = 0;
|
|
772
|
+
micPeakRms = 0;
|
|
752
773
|
pausePreviewed = false;
|
|
753
774
|
reuseLiveOnStop = false;
|
|
754
775
|
livePromise = null;
|
|
@@ -869,13 +890,42 @@
|
|
|
869
890
|
// won't auto-send (speechSeen gates that).
|
|
870
891
|
micReady = true;
|
|
871
892
|
lastVoiceTs = Date.now();
|
|
893
|
+
listenStartTs = Date.now();
|
|
894
|
+
micPeakRms = 0;
|
|
872
895
|
if (mode === "listening") render();
|
|
873
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").
|
|
874
899
|
console.error("desktop renderer: mic error", e);
|
|
900
|
+
micReady = false;
|
|
875
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);
|
|
876
912
|
render();
|
|
877
913
|
}
|
|
878
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
|
+
}
|
|
879
929
|
function stopMic() {
|
|
880
930
|
try { mediaRecorder?.stop(); } catch {}
|
|
881
931
|
try { audioStream?.getTracks().forEach((t) => t.stop()); } catch {}
|
|
@@ -915,6 +965,19 @@
|
|
|
915
965
|
}
|
|
916
966
|
const rms = Math.sqrt(sumSq / timeData.length);
|
|
917
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
|
+
}
|
|
918
981
|
if (rms > VOICE_RMS) {
|
|
919
982
|
speechSeen = true;
|
|
920
983
|
lastVoiceTs = now;
|
|
@@ -328,19 +328,30 @@ button { font-family: inherit; }
|
|
|
328
328
|
.wavebar i.cur { transform: scaleY(1.25); }
|
|
329
329
|
.audio .dur { font-size: 11px; color: var(--ink-3); font-variant-numeric: tabular-nums; min-width: 30px; text-align: right; }
|
|
330
330
|
|
|
331
|
-
/*
|
|
331
|
+
/* Continued agent messages (no repeated "Roby" header) hug the previous one. */
|
|
332
|
+
.turn.cont { padding-top: 0; }
|
|
333
|
+
.turn.cont .msg-agent { margin-top: 1px; }
|
|
334
|
+
|
|
335
|
+
/* Inline copy icon at the end of an agent message — hover-only, so it never
|
|
336
|
+
reserves an empty row of vertical space. */
|
|
337
|
+
.msg-agent .btn-copy {
|
|
338
|
+
display: inline-flex; align-items: center; vertical-align: -3px;
|
|
339
|
+
margin-left: 6px; padding: 1px; border: none; background: transparent;
|
|
340
|
+
cursor: pointer; color: var(--ink-3); opacity: 0; transition: opacity .15s ease, color .15s ease;
|
|
341
|
+
}
|
|
342
|
+
.turn:hover .msg-agent .btn-copy { opacity: .55; }
|
|
343
|
+
.msg-agent .btn-copy:hover { opacity: 1; color: var(--ink); }
|
|
344
|
+
.msg-agent .btn-copy.done { opacity: 1; color: oklch(0.6 0.15 150); }
|
|
345
|
+
|
|
346
|
+
/* per-turn actions — only Regenerate now, and only on the LAST agent turn.
|
|
347
|
+
Regenerating a past turn would replay the most-recent user prompt (not the
|
|
348
|
+
one that produced that reply) and silently break the flow, so it's hidden
|
|
349
|
+
everywhere else and takes no space. */
|
|
332
350
|
.turn-actions {
|
|
333
|
-
margin: 7px 0 0 24px; display:
|
|
351
|
+
margin: 7px 0 0 24px; display: none; gap: 4px;
|
|
334
352
|
opacity: 0; transition: opacity .2s ease;
|
|
335
353
|
}
|
|
336
|
-
.turn
|
|
337
|
-
|
|
338
|
-
/* Regenerate is only meaningful on the LAST agent turn — regenerating a
|
|
339
|
-
past one would replay the most-recent user prompt (not the one that
|
|
340
|
-
produced this reply) and silently break the conversation flow. Copy
|
|
341
|
-
stays available on every turn so users can grab old replies. */
|
|
342
|
-
.turn .btn-regen { display: none; }
|
|
343
|
-
.turn.last .btn-regen { display: inline-flex; }
|
|
354
|
+
.turn.last .turn-actions { display: flex; opacity: 1; }
|
|
344
355
|
.chip {
|
|
345
356
|
display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
|
|
346
357
|
padding: 4px 8px; border-radius: 9px; border: 1px solid var(--glass-hairline);
|
|
@@ -350,6 +361,16 @@ button { font-family: inherit; }
|
|
|
350
361
|
.chip:hover { background: var(--glass-hairline); color: var(--ink); border-color: transparent; }
|
|
351
362
|
.chip.done { color: oklch(0.6 0.15 150); }
|
|
352
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
|
+
|
|
353
374
|
/* tool pill (when the agent runs a tool) */
|
|
354
375
|
.tool-pill {
|
|
355
376
|
display: inline-flex; align-items: center; gap: 6px;
|