From 3b532753b3999d984a8efe54a18a92109fd8e98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Fri, 15 May 2026 21:34:21 +0200 Subject: [PATCH 1/2] notifications: per-event tags + debug logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bug: all notifications used tag='hyperhive', so each new fire replaced the previous — operator only ever saw one at a time and might miss the fact that a second arrived. now per-event tags (hyperhive:approval:, hyperhive:question:, hyperhive:msg::) so distinct events stack in the OS notification center. dropped the bogus icon (was pointing at dashboard.css) — some browsers refuse to display a notification with an invalid icon. added console.debug at every block point (not supported, permission not granted, muted) and a 'shown' log on success, so the operator can see in the browser console exactly why a notification didn't fire. note for the operator: most browsers also suppress notifications while the originating tab is FOCUSED. that's a browser-level decision, not ours. --- hive-c0re/assets/app.js | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 2409710e..2da64596 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -82,15 +82,30 @@ unmute.addEventListener('click', () => { setMuted(false); renderControls(); }); renderControls(); } - function show(title, body) { - if (!supported || Notification.permission !== 'granted' || isMuted()) return; + function show(title, body, tag) { + if (!supported) { + console.debug('notify: Notification API not supported'); + return; + } + if (Notification.permission !== 'granted') { + console.debug('notify: permission not granted', Notification.permission); + return; + } + if (isMuted()) { + console.debug('notify: muted'); + return; + } try { + // Per-event tag so distinct messages stack instead of + // collapsing into one slot. Caller passes a unique tag per + // notification kind/id; we don't fall back to 'hyperhive' + // because that one tag would replace itself on every fire. const n = new Notification(title, { body, - tag: 'hyperhive', // collapse rapid bursts - icon: '/static/dashboard.css', // any same-origin asset works as a favicon stand-in + tag: tag || ('hyperhive:' + Date.now()), }); n.onclick = () => { window.focus(); n.close(); }; + console.debug('notify: shown', title, 'tag=', tag); } catch (err) { console.warn('notification show failed', err); } @@ -124,12 +139,14 @@ if (seenApprovals.has(a.id)) continue; seenApprovals.add(a.id); const verb = a.kind === 'spawn' ? 'spawn approval' : 'config commit'; - NOTIF.show('◆ approval #' + a.id, `${verb} for ${a.agent}`); + NOTIF.show('◆ approval #' + a.id, `${verb} for ${a.agent}`, + 'hyperhive:approval:' + a.id); } for (const q of questions) { if (seenQuestions.has(q.id)) continue; seenQuestions.add(q.id); - NOTIF.show('◆ manager asks', q.question.slice(0, 120)); + NOTIF.show('◆ manager asks', q.question.slice(0, 120), + 'hyperhive:question:' + q.id); } // operator_inbox: only notify on truly new ids — sse already // handles single-message notifications, but if the operator @@ -658,7 +675,13 @@ // the OS notification center. if (m.kind === 'sent' && m.to === 'operator') { refreshState(); - NOTIF.show('◆ ' + m.from + ' → operator', String(m.body || '').slice(0, 200)); + NOTIF.show( + '◆ ' + m.from + ' → operator', + String(m.body || '').slice(0, 200), + // Unique-per-arrival tag so a burst stacks instead of + // overwriting itself in the OS notification center. + 'hyperhive:msg:' + m.at + ':' + Math.random().toString(36).slice(2, 6), + ); } const row = document.createElement('div'); row.className = 'msgrow ' + m.kind; From fd0e493bf57c0f1a72eb83d82a2d12537e0d81c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Fri, 15 May 2026 21:35:48 +0200 Subject: [PATCH 2/2] agent terminal: show full body for send tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit send was truncating to 80 chars in the tool_use row, hiding anything past the first sentence. now renders as a collapsed
like Write/Edit — summary still shows the recipient + headline (so the operator can scan), expanding reveals the full body unchanged. recv side was already covered: the wake prompt shows the full incoming body, and explicit recv() tool_result rows expand to the full text via the existing collapsed-results path. --- hive-ag3nt/assets/app.js | 74 ++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/hive-ag3nt/assets/app.js b/hive-ag3nt/assets/app.js index f4670fd2..98ea546c 100644 --- a/hive-ag3nt/assets/app.js +++ b/hive-ag3nt/assets/app.js @@ -614,38 +614,51 @@ default: return name + ' ' + trim(JSON.stringify(input), 200); } } - // Build a tool_use row for Write/Edit as a collapsed
- // showing the actual change. Returns null for any other tool so - // the caller falls back to the flat-row path. + // Build a "rich" tool_use row for tools whose input has a body + // we want the operator to see in full. Returns null for any + // other tool so the caller falls back to the flat-row path. + // // Write: every input.content line is "+". // Edit: old_string lines as "-", new_string lines as "+". - // Not a true diff algorithm — claude's Edit blocks are already a - // contiguous old/new pair, so a literal -/+ rendering is honest. - function renderFileWriteEdit(c) { + // mcp__hyperhive__send: collapsed
, full body text + // inside. Truncating to 80 chars in the summary was hiding + // anything past the first sentence. + function renderRichToolUse(c) { const name = c.name || ''; const input = c.input || {}; - if (name !== 'Write' && name !== 'Edit') return null; - const path = input.file_path || '?'; - let body; - let plus = 0; - let minus = 0; - if (name === 'Write') { - const content = String(input.content || ''); - const lines = content.split('\n'); - plus = lines.length; - body = lines.map(l => '+ ' + l).join('\n'); - } else { - const oldLines = String(input.old_string || '').split('\n'); - const newLines = String(input.new_string || '').split('\n'); - minus = oldLines.length; - plus = newLines.length; - body = oldLines.map(l => '- ' + l).join('\n') - + '\n' - + newLines.map(l => '+ ' + l).join('\n'); + if (name === 'Write' || name === 'Edit') { + const path = input.file_path || '?'; + let body; + let plus = 0; + let minus = 0; + if (name === 'Write') { + const content = String(input.content || ''); + const lines = content.split('\n'); + plus = lines.length; + body = lines.map(l => '+ ' + l).join('\n'); + } else { + const oldLines = String(input.old_string || '').split('\n'); + const newLines = String(input.new_string || '').split('\n'); + minus = oldLines.length; + plus = newLines.length; + body = oldLines.map(l => '- ' + l).join('\n') + + '\n' + + newLines.map(l => '+ ' + l).join('\n'); + } + const summary = '→ ' + name + ' ' + path + ' · ' + + (minus ? '-' + minus + ' ' : '') + '+' + plus; + return detailsDiff('tool-use', summary, body); } - const summary = '→ ' + name + ' ' + path + ' · ' - + (minus ? '-' + minus + ' ' : '') + '+' + plus; - return detailsDiff('tool-use', summary, body); + if (name === 'mcp__hyperhive__send') { + const to = input.to || '?'; + const body = String(input.body || ''); + const headline = body.replace(/\s+/g, ' ').trim().slice(0, 80); + const lines = body.split('\n').length; + const summary = '→ send → ' + to + (lines > 1 ? ` · ${lines}L` : '') + + (headline ? ' · ' + headline + (body.length > 80 ? '…' : '') : ''); + return details('tool-use', summary, body); + } + return null; } function detailsDiff(cls, summary, body) { clearPlaceholder(); @@ -705,9 +718,10 @@ row('thinking', txt ? '· ' + txt : '· thinking …'); } else if (c.type === 'tool_use') { - // Write/Edit get a collapsed +/- diff body; everything - // else stays as the flat row produced by fmtToolUse. - if (!renderFileWriteEdit(c)) { + // Write/Edit get a +/- diff body; send gets a collapsed + //
with the full body text; everything else + // stays as the flat row produced by fmtToolUse. + if (!renderRichToolUse(c)) { row('tool-use', '→ ' + fmtToolUse(c)); } }