Compare commits

..
2 changed files with 37 additions and 74 deletions

View file

@ -614,19 +614,17 @@
default: return name + ' ' + trim(JSON.stringify(input), 200); default: return name + ' ' + trim(JSON.stringify(input), 200);
} }
} }
// Build a "rich" tool_use row for tools whose input has a body // Build a tool_use row for Write/Edit as a collapsed <details>
// we want the operator to see in full. Returns null for any // showing the actual change. Returns null for any other tool so
// other tool so the caller falls back to the flat-row path. // the caller falls back to the flat-row path.
//
// Write: every input.content line is "+". // Write: every input.content line is "+".
// Edit: old_string lines as "-", new_string lines as "+". // Edit: old_string lines as "-", new_string lines as "+".
// mcp__hyperhive__send: collapsed <details>, full body text // Not a true diff algorithm — claude's Edit blocks are already a
// inside. Truncating to 80 chars in the summary was hiding // contiguous old/new pair, so a literal -/+ rendering is honest.
// anything past the first sentence. function renderFileWriteEdit(c) {
function renderRichToolUse(c) {
const name = c.name || ''; const name = c.name || '';
const input = c.input || {}; const input = c.input || {};
if (name === 'Write' || name === 'Edit') { if (name !== 'Write' && name !== 'Edit') return null;
const path = input.file_path || '?'; const path = input.file_path || '?';
let body; let body;
let plus = 0; let plus = 0;
@ -649,17 +647,6 @@
+ (minus ? '-' + minus + ' ' : '') + '+' + plus; + (minus ? '-' + minus + ' ' : '') + '+' + plus;
return detailsDiff('tool-use', summary, body); 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) { function detailsDiff(cls, summary, body) {
clearPlaceholder(); clearPlaceholder();
const d = document.createElement('details'); const d = document.createElement('details');
@ -718,10 +705,9 @@
row('thinking', txt ? '· ' + txt : '· thinking …'); row('thinking', txt ? '· ' + txt : '· thinking …');
} }
else if (c.type === 'tool_use') { else if (c.type === 'tool_use') {
// Write/Edit get a +/- diff body; send gets a collapsed // Write/Edit get a collapsed +/- diff body; everything
// <details> with the full body text; everything else // else stays as the flat row produced by fmtToolUse.
// stays as the flat row produced by fmtToolUse. if (!renderFileWriteEdit(c)) {
if (!renderRichToolUse(c)) {
row('tool-use', '→ ' + fmtToolUse(c)); row('tool-use', '→ ' + fmtToolUse(c));
} }
} }

View file

@ -82,30 +82,15 @@
unmute.addEventListener('click', () => { setMuted(false); renderControls(); }); unmute.addEventListener('click', () => { setMuted(false); renderControls(); });
renderControls(); renderControls();
} }
function show(title, body, tag) { function show(title, body) {
if (!supported) { if (!supported || Notification.permission !== 'granted' || isMuted()) return;
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 { 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, { const n = new Notification(title, {
body, body,
tag: tag || ('hyperhive:' + Date.now()), tag: 'hyperhive', // collapse rapid bursts
icon: '/static/dashboard.css', // any same-origin asset works as a favicon stand-in
}); });
n.onclick = () => { window.focus(); n.close(); }; n.onclick = () => { window.focus(); n.close(); };
console.debug('notify: shown', title, 'tag=', tag);
} catch (err) { } catch (err) {
console.warn('notification show failed', err); console.warn('notification show failed', err);
} }
@ -139,14 +124,12 @@
if (seenApprovals.has(a.id)) continue; if (seenApprovals.has(a.id)) continue;
seenApprovals.add(a.id); seenApprovals.add(a.id);
const verb = a.kind === 'spawn' ? 'spawn approval' : 'config commit'; 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) { for (const q of questions) {
if (seenQuestions.has(q.id)) continue; if (seenQuestions.has(q.id)) continue;
seenQuestions.add(q.id); 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 // operator_inbox: only notify on truly new ids — sse already
// handles single-message notifications, but if the operator // handles single-message notifications, but if the operator
@ -675,13 +658,7 @@
// the OS notification center. // the OS notification center.
if (m.kind === 'sent' && m.to === 'operator') { if (m.kind === 'sent' && m.to === 'operator') {
refreshState(); refreshState();
NOTIF.show( NOTIF.show('◆ ' + m.from + ' → operator', String(m.body || '').slice(0, 200));
'◆ ' + 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'); const row = document.createElement('div');
row.className = 'msgrow ' + m.kind; row.className = 'msgrow ' + m.kind;