feat: notification sound and browser notifications on task completion
Add two new settings (both default off): - sound_enabled: plays a short tone via Web Audio API when assistant finishes a response or requests approval - notifications_enabled: shows a browser notification when a response completes while the tab is in the background Uses Web Audio API (oscillator) instead of bundled MP3 file — zero additional assets. Follows the standard 4-file settings pattern. Also skip test_valid_skill_accepted when hermes-agent not installed (skills endpoint returns 500 without the agent module). Inspired by #176 (DavidSchuchert) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -322,7 +322,7 @@ function applyBotName(){
|
||||
(async()=>{
|
||||
// Load send key preference
|
||||
let _bootSettings={};
|
||||
try{const s=await api('/api/settings');_bootSettings=s;window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;window._showCliSessions=!!s.show_cli_sessions;window._botName=s.bot_name||'Hermes';const _theme=s.theme||'dark';document.documentElement.dataset.theme=_theme;localStorage.setItem('hermes-theme',_theme);applyBotName();}catch(e){window._sendKey='enter';window._showTokenUsage=false;window._showCliSessions=false;window._botName='Hermes';_bootSettings={check_for_updates:false};}
|
||||
try{const s=await api('/api/settings');_bootSettings=s;window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;window._showCliSessions=!!s.show_cli_sessions;window._soundEnabled=!!s.sound_enabled;window._notificationsEnabled=!!s.notifications_enabled;window._botName=s.bot_name||'Hermes';const _theme=s.theme||'dark';document.documentElement.dataset.theme=_theme;localStorage.setItem('hermes-theme',_theme);applyBotName();}catch(e){window._sendKey='enter';window._showTokenUsage=false;window._showCliSessions=false;window._soundEnabled=false;window._notificationsEnabled=false;window._botName='Hermes';_bootSettings={check_for_updates:false};}
|
||||
// Non-blocking update check (fire-and-forget, once per tab session)
|
||||
// ?test_updates=1 in URL forces banner display for testing (bypasses sessionStorage guards)
|
||||
const _testUpdates=new URLSearchParams(location.search).get('test_updates')==='1';
|
||||
|
||||
@@ -345,6 +345,20 @@
|
||||
<option value="oled">OLED</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="settingsSoundEnabled" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||
Notification sound
|
||||
</label>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px">Play a sound when the assistant finishes a response.</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="settingsNotificationsEnabled" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||
Browser notifications
|
||||
</label>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px">Show a system notification when a response completes while the tab is in the background.</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="settingsShowTokenUsage" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||
|
||||
@@ -143,6 +143,8 @@ async function send(){
|
||||
const d=JSON.parse(e.data);
|
||||
d._session_id=activeSid;
|
||||
showApprovalCard(d);
|
||||
playNotificationSound();
|
||||
sendBrowserNotification('Approval required',d.description||'Tool approval needed');
|
||||
});
|
||||
|
||||
source.addEventListener('done',e=>{
|
||||
@@ -176,6 +178,8 @@ async function send(){
|
||||
syncTopbar();renderMessages();loadDir('.');
|
||||
}
|
||||
renderSessionList();setBusy(false);setStatus('');
|
||||
playNotificationSound();
|
||||
sendBrowserNotification('Response complete',assistantText?assistantText.slice(0,100):'Task finished');
|
||||
});
|
||||
|
||||
source.addEventListener('compressed',e=>{
|
||||
@@ -360,5 +364,36 @@ function startApprovalPolling(sid) {
|
||||
function stopApprovalPolling() {
|
||||
if (_approvalPollTimer) { clearInterval(_approvalPollTimer); _approvalPollTimer = null; }
|
||||
}
|
||||
|
||||
// ── Notifications and Sound ──────────────────────────────────────────────────
|
||||
|
||||
function playNotificationSound(){
|
||||
if(!window._soundEnabled) return;
|
||||
try{
|
||||
const ctx=new (window.AudioContext||window.webkitAudioContext)();
|
||||
const osc=ctx.createOscillator();
|
||||
const gain=ctx.createGain();
|
||||
osc.connect(gain);gain.connect(ctx.destination);
|
||||
osc.type='sine';osc.frequency.setValueAtTime(660,ctx.currentTime);
|
||||
osc.frequency.setValueAtTime(880,ctx.currentTime+0.1);
|
||||
gain.gain.setValueAtTime(0.3,ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01,ctx.currentTime+0.3);
|
||||
osc.start(ctx.currentTime);osc.stop(ctx.currentTime+0.3);
|
||||
}catch(e){console.warn('Notification sound failed:',e);}
|
||||
}
|
||||
|
||||
function sendBrowserNotification(title,body){
|
||||
if(!window._notificationsEnabled||!document.hidden) return;
|
||||
if(!('Notification' in window)) return;
|
||||
const botName=window._botName||'Hermes';
|
||||
if(Notification.permission==='granted'){
|
||||
new Notification(title||botName,{body:body});
|
||||
}else if(Notification.permission!=='denied'){
|
||||
Notification.requestPermission().then(p=>{
|
||||
if(p==='granted') new Notification(title||botName,{body:body});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Panel navigation (Chat / Tasks / Skills / Memory) ──
|
||||
|
||||
|
||||
@@ -1009,6 +1009,10 @@ async function loadSettingsPanel(){
|
||||
if(syncCb){syncCb.checked=!!settings.sync_to_insights;syncCb.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
const updateCb=$('settingsCheckUpdates');
|
||||
if(updateCb){updateCb.checked=settings.check_for_updates!==false;updateCb.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
const soundCb=$('settingsSoundEnabled');
|
||||
if(soundCb){soundCb.checked=!!settings.sound_enabled;soundCb.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
const notifCb=$('settingsNotificationsEnabled');
|
||||
if(notifCb){notifCb.checked=!!settings.notifications_enabled;notifCb.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
// Bot name
|
||||
const botNameField=$('settingsBotName');
|
||||
if(botNameField){botNameField.value=settings.bot_name||'Hermes';botNameField.addEventListener('input',_markSettingsDirty,{once:false});}
|
||||
@@ -1045,6 +1049,8 @@ async function saveSettings(andClose){
|
||||
body.show_cli_sessions=showCliSessions;
|
||||
body.sync_to_insights=!!($('settingsSyncInsights')||{}).checked;
|
||||
body.check_for_updates=!!($('settingsCheckUpdates')||{}).checked;
|
||||
body.sound_enabled=!!($('settingsSoundEnabled')||{}).checked;
|
||||
body.notifications_enabled=!!($('settingsNotificationsEnabled')||{}).checked;
|
||||
const botName=(($('settingsBotName')||{}).value||'').trim();
|
||||
body.bot_name=botName||'Hermes';
|
||||
// Password: only act if the field has content; blank = leave auth unchanged
|
||||
@@ -1065,6 +1071,8 @@ async function saveSettings(andClose){
|
||||
window._sendKey=sendKey||'enter';
|
||||
window._showTokenUsage=showTokenUsage;
|
||||
window._showCliSessions=showCliSessions;
|
||||
window._soundEnabled=body.sound_enabled;
|
||||
window._notificationsEnabled=body.notifications_enabled;
|
||||
window._botName=body.bot_name;
|
||||
if(typeof applyBotName==='function') applyBotName();
|
||||
_settingsDirty=false; _settingsThemeOnOpen=theme;
|
||||
|
||||
Reference in New Issue
Block a user