Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd0e493bf5 | ||
|
|
3b532753b3 |
2 changed files with 74 additions and 37 deletions
|
|
@ -614,38 +614,51 @@
|
|||
default: return name + ' ' + trim(JSON.stringify(input), 200);
|
||||
}
|
||||
}
|
||||
// Build a tool_use row for Write/Edit as a collapsed <details>
|
||||
// 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 <details>, 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
|
||||
// <details> with the full body text; everything else
|
||||
// stays as the flat row produced by fmtToolUse.
|
||||
if (!renderRichToolUse(c)) {
|
||||
row('tool-use', '→ ' + fmtToolUse(c));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue