@double_geste/agent-standup 1.25.5 → 1.25.6
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 +1 -1
- package/public/index.html +34 -11
- package/server.js +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@double_geste/agent-standup",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.6",
|
|
4
4
|
"description": "Your Claude Code agents as a live video call, from the terminal: a local page in your browser, plus one-line status for status bars. Free, open source, zero token cost.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-standup": "bin/agent-standup.js"
|
package/public/index.html
CHANGED
|
@@ -519,6 +519,7 @@ const lagMs = (i = S.idx) => { // how much newer the newest event of the sessi
|
|
|
519
519
|
};
|
|
520
520
|
const lagFactor = () => { const l = lagMs() / 1000; return l < 6 ? 1 : l < 15 ? 1.15 : l < 30 ? 1.3 : 1.5; }; // steps, so the cached audio of a line is reused
|
|
521
521
|
const caughtUp = () => !S.live || lagMs() < 6000;
|
|
522
|
+
const nearLive = () => !S.live || lagMs() < 30000; // an answer to what was just said (a word after an order, a thank-you after a report) still fits here: reading the line took longer than 6 s and the session went on meanwhile
|
|
522
523
|
const spd = () => (S.live ? 1.12 * lagFactor() : +$('#speed').value) * (S.boost || 1);
|
|
523
524
|
const nameOf = (id) => (S.agents[id] || {}).name || id;
|
|
524
525
|
|
|
@@ -1543,11 +1544,11 @@ function talkLine(g) {
|
|
|
1543
1544
|
return t;
|
|
1544
1545
|
}
|
|
1545
1546
|
async function react(ev, token) {
|
|
1546
|
-
if ((!prefs.epi && !prefs.banter) || S.boost > 1 || S.hurry || !
|
|
1547
|
+
if ((!prefs.epi && !prefs.banter) || S.boost > 1 || S.hurry || !nearLive()) return; const seed = hash(String(ev.ts) + ev.agent), R = REACT[FR() ? 'fr' : 'en']; let who, line, thanks = false;
|
|
1547
1548
|
if (ev.kind === 'order') { if (!prefs.banter && seed % 100 > 55) return; who = ev.to; line = prefs.banter ? ackLine(ev) : R.ack[seed % R.ack.length]; }
|
|
1548
1549
|
else if (ev.kind === 'report') {
|
|
1549
1550
|
const r = seed % 100, joke = prefs.epi ? 60 : prefs.chat === 'quiet' ? -1 : 35; // a remark from someone else, or (a little less often) a word of thanks from whoever receives the report
|
|
1550
|
-
if (r <= joke) { const others = Object.keys(S.status).concat(['main']).filter((x) => x !== ev.agent && S.agents[x] && !S.gone.has(x)); who = others[(seed >>> 3) % others.length]; line = quipMem.get(baseText(ev, 160)) || R.done[seed % R.done.length]; }
|
|
1551
|
+
if (r <= joke) { if (!caughtUp()) return; const others = Object.keys(S.status).concat(['main']).filter((x) => x !== ev.agent && S.agents[x] && !S.gone.has(x)); who = others[(seed >>> 3) % others.length]; line = quipMem.get(baseText(ev, 160)) || R.done[seed % R.done.length]; }
|
|
1551
1552
|
else if (prefs.banter && r <= joke + 25 && ev.to !== 'user' && S.agents[ev.to] && !S.gone.has(ev.to)) { who = ev.to; line = varied('thanks', seed >>> 3); thanks = true; }
|
|
1552
1553
|
else return;
|
|
1553
1554
|
}
|
|
@@ -1578,6 +1579,18 @@ async function ponder(i, token) { // replay: an agent that really took a long
|
|
|
1578
1579
|
const ev = S.events[i], p = before(i); if (!prefs.banter || prefs.chat === 'quiet' || S.boost > 1 || S.hurry || !p || !S.agents[ev.agent] || thinkerOf(p) !== ev.agent || ev.ts - p.ts < 40000 || hash('t' + ev.ts) % 100 > 55) return;
|
|
1579
1580
|
const line = varied('think', hash(String(ev.ts))); startTalk(ev.agent, line, '💭'); await speakLine(line, S.agents[ev.agent], true, token); stopTalk(ev.agent);
|
|
1580
1581
|
}
|
|
1582
|
+
// Live: you gave an order and the lead starts on its tools without a word (about one time in three): it says what it is starting with, from the tool call itself, so you know it heard you and what it is on
|
|
1583
|
+
const LEAD = {
|
|
1584
|
+
en: { in: ['Got it.', 'Okay.', 'On it.', 'Sure.', 'Right.'], read: 'Let me look at ', edit: 'I’m editing ', write: 'I’m writing ', cmd: 'I’m running ', note: 'I’m running a command: ', grep: 'Let me search the code.', glob: 'Let me see which files we have.', todo: 'Let me lay out a plan.', web: 'Let me check the web.', question: 'I have a question for you.', any: 'Let me get started.', and: ' and ' },
|
|
1585
|
+
fr: { in: ['D’accord.', 'Compris.', 'Très bien.', 'Entendu.', 'Ça marche.'], read: 'Je regarde ', edit: 'Je modifie ', write: 'J’écris ', cmd: 'Je lance ', note: 'Je lance une commande : ', grep: 'Je cherche dans le code.', glob: 'Je regarde quels fichiers on a.', todo: 'Je fais mon plan.', web: 'Je vais voir sur le web.', question: 'J’ai une question pour vous.', any: 'Je m’y mets.', and: ' et ' },
|
|
1586
|
+
};
|
|
1587
|
+
async function leadAck(ev, token) {
|
|
1588
|
+
S.owed = false; if (!prefs.banter || S.hurry || S.boost > 1 || !caughtUp() || S.speakers.size || !S.agents.main) return;
|
|
1589
|
+
const W = LEAD[FR() ? 'fr' : 'en'], d = ev.tools[0].d || {}, k = d.kind === 'text' ? d.what : d.kind; let what = W[k] || W.any;
|
|
1590
|
+
if (k === 'read' || k === 'edit' || k === 'write') { const files = [...new Set(ev.tools.filter((t) => (t.d || {}).kind === k).map((t) => base(t.d.file)).filter(Boolean))].slice(0, 2); what = files.length ? what + files.join(W.and) + '.' : W.any; }
|
|
1591
|
+
else if (k === 'cmd') { const note = (d.note || '').trim().replace(/[.\s]+$/, ''), cmd = String(d.text || '').trim().split(/\s+/).slice(0, 2).join(' ').slice(0, 26); what = note ? W.note + note + '.' : cmd ? what + cmd + '.' : W.any; }
|
|
1592
|
+
const line = `${W.in[hash('k' + ev.ts) % W.in.length]} ${what}`; startTalk('main', line, '💬'); await speakLine(line, S.agents.main, true, token); stopTalk('main');
|
|
1593
|
+
}
|
|
1581
1594
|
async function wrapUp(token) { // live, once the lead is done: the part of its last message that the clipped reading left out, so you know what was done
|
|
1582
1595
|
if (!S.wrap || S.speakers.size || !prefs.voice && !prefs.banter) return false; S.wrap = false;
|
|
1583
1596
|
let k = S.events.length - 1; while (k >= 0 && !(S.events[k].agent === 'main' && S.events[k].kind === 'say')) k--; if (k < 0) return false; const e = S.events[k], m = S.mainSaid;
|
|
@@ -1609,7 +1622,7 @@ const SPK = new Set(['say', 'report', 'order', 'user']);
|
|
|
1609
1622
|
function aheadSpeakable(i, limit) { let n = 0; for (let k = i, m = Math.min(S.events.length, i + 800); k < m; k++) if (SPK.has(S.events[k].kind) && ++n > limit) break; return n; }
|
|
1610
1623
|
const isSub = (id) => id !== 'main' && id !== 'user';
|
|
1611
1624
|
async function run(i) {
|
|
1612
|
-
const token = ++S.token; S.playing = true; S.idle = false; S.hurry = false; S.boost = 1; S.spokeAt = performance.now(); S.fill = 0; S.exQ = []; S.errRun = {}; S.wrap = false; S.mainSaid = null; S.syncTs = (S.events[i] && S.events[i].ts) || Date.now(); syncUI(); AUD.init();
|
|
1625
|
+
const token = ++S.token; S.playing = true; S.idle = false; S.hurry = false; S.boost = 1; S.spokeAt = performance.now(); S.fill = 0; S.exQ = []; S.errRun = {}; S.wrap = false; S.mainSaid = null; S.owed = false; S.syncTs = (S.events[i] && S.events[i].ts) || Date.now(); syncUI(); AUD.init();
|
|
1613
1626
|
if (i === 0 && prefs.epi && !S.live) { await card(L('ÉPISODE 1', 'EPISODE 1'), S.title); if (token !== S.token) return; }
|
|
1614
1627
|
while (token === S.token) {
|
|
1615
1628
|
const lim = S.stopAt || S.events.length;
|
|
@@ -1625,13 +1638,13 @@ async function run(i) {
|
|
|
1625
1638
|
const q = S.live ? aheadSpeakable(i, 14) : 0; // lines waiting to be said: the more of them, the shorter each one is read, but it is always read
|
|
1626
1639
|
const lg = lagMs(i) / 1000; S.pace = !S.live ? 0 : Math.min(q > 7 ? 70 : q > 3 ? 130 : 260, lg > 30 ? 70 : lg > 15 ? 130 : lg > 6 ? 200 : 260); S.hurry = q > 12 || lg > 45; // voices are only skipped when more than a dozen lines are queued
|
|
1627
1640
|
S.boost = S.hurry || (S.live && S.events.length - i > 60) ? 3 : 1;
|
|
1628
|
-
const ev = S.events[i]; S.idx = i; tally(ev);
|
|
1641
|
+
const ev = S.events[i]; S.idx = i; tally(ev); if (S.live && ev.ts > S.since) S.since = ev.ts; // said once: a reload or a return to this session does not read it again
|
|
1629
1642
|
if (!S.live && (ev.kind === 'say' || ev.kind === 'report' || ev.kind === 'order' || ev.kind === 'tools')) { await ponder(i, token); if (token !== S.token) return; }
|
|
1630
1643
|
if (ev.kind === 'usage' || ev.kind === 'compact') { if (ev.kind === 'compact' && S.live && prefs.banter) toast(`${nameOf(ev.agent)}: ${L('contexte compacté 🗜', 'context compacted 🗜')}`); i++; continue; }
|
|
1631
1644
|
if (ev.kind === 'finish') { applyStatus(ev); paintStatus(); i++; continue; }
|
|
1632
1645
|
const backlog = S.live ? S.events.length - i : 0, quick = S.live ? Math.min(1, 40 / Math.max(1, backlog)) : 1; // live: hundreds of tool events from parallel sub-agents must not take minutes to scroll past
|
|
1633
1646
|
if (ev.kind === 'results') { applyResults(ev, S.boost > 1); await sleep((S.boost === 1 && S.present === ev.agent && ev.items.some((it) => it.img) ? (S.live ? 900 : 1800) : S.demo ? 900 : S.live ? 30 * quick : 80) / spd()); i++; continue; } // a picture gets a moment on screen
|
|
1634
|
-
if (ev.kind === 'tools') { applyStatus(ev); tickTools(ev); AUD.click(); OFFICE.kick(ev.tools[ev.tools.length - 1].name); paintStatus(); if (quick === 1) await smallTalk(token); await sleep((S.demo ? 1100 : S.live ? 110 * quick : 250) / spd()); i++; continue; }
|
|
1647
|
+
if (ev.kind === 'tools') { applyStatus(ev); tickTools(ev); AUD.click(); OFFICE.kick(ev.tools[ev.tools.length - 1].name); paintStatus(); if (S.live && S.owed && ev.agent === 'main') { await leadAck(ev, token); if (token !== S.token) return; } if (quick === 1) await smallTalk(token); await sleep((S.demo ? 1100 : S.live ? 110 * quick : 250) / spd()); i++; continue; }
|
|
1635
1648
|
const group = [ev]; let j = i + 1; // sub-agents talking back-to-back were running in parallel: they talk over each other
|
|
1636
1649
|
if (ev.kind === 'order') { // several orders in a row (a parallel launch): the new agents all appear now, the first order is read, the others are named in one line
|
|
1637
1650
|
while (j < Math.min(S.events.length, i + 40) && group.length < 8) { const e2 = S.events[j]; if (e2.kind === 'usage' || e2.kind === 'compact') { tally(e2); j++; continue; } if (e2.kind === 'order' && e2.agent === ev.agent) { group.push(e2); j++; } else break; }
|
|
@@ -1650,7 +1663,7 @@ async function run(i) {
|
|
|
1650
1663
|
if (FR() && !S.hurry) { prefetchTr(i); await Promise.race([ensureTr(group.map((g) => baseText(g))), sleep(S.live ? 7000 : 14000)]); if (token !== S.token) return; }
|
|
1651
1664
|
prefetchQuips(i); warmTTS(i);
|
|
1652
1665
|
const playOne = (g) => {
|
|
1653
|
-
applyStatus(g);
|
|
1666
|
+
applyStatus(g); if (g.agent === 'user') S.owed = true; else if (g.agent === 'main') S.owed = false; // your order waits for a word from the lead
|
|
1654
1667
|
const label = g.kind === 'order' ? `${L('pour', 'to')} ${nameOf(g.to)}` : g.kind === 'report' ? `${L('rapport à', 'report to')} ${nameOf(g.to)}` : '', line = talkLine(g);
|
|
1655
1668
|
startTalk(g.agent, line, label);
|
|
1656
1669
|
if (g.kind === 'order') { S.status[g.to] = 'working'; AUD.join(); setTimeout(() => { paintStatus(); fly(g.agent, g.to, '📨'); popTile(g.to); }, 30); }
|
|
@@ -1666,7 +1679,7 @@ async function run(i) {
|
|
|
1666
1679
|
}
|
|
1667
1680
|
else if (S.live) { plays = []; let chain = Promise.resolve(); for (const g of group) { chain = chain.then(() => (token === S.token ? playOne(g) : null)); plays.push(chain); } } else plays = group.map(playOne); // live: one voice at a time so each can be heard; replay keeps the overlapping chatter
|
|
1668
1681
|
S.idx = j - 1; paintStatus(); await Promise.all(plays); if (token !== S.token) return;
|
|
1669
|
-
for (const g of (ev.kind === 'order'
|
|
1682
|
+
for (const g of (ev.kind === 'order' ? group.slice(0, 3) : group)) { await converse(g, token); if (token !== S.token) return; } // a parallel launch: the first three answer, as long as the call keeps up
|
|
1670
1683
|
i = j;
|
|
1671
1684
|
}
|
|
1672
1685
|
if (token === S.token) {
|
|
@@ -1724,8 +1737,18 @@ async function followCurrent() {
|
|
|
1724
1737
|
if (S.pin && cur.id !== S.pin) S.pin = ''; // the pinned session is gone: follow the latest one instead of staying silently pinned
|
|
1725
1738
|
if (S.cur && S.cur.id === cur.id && S.cur.proj === cur.proj) return false;
|
|
1726
1739
|
if (!(await loadSession(cur.proj, cur.id))) return false;
|
|
1727
|
-
const start =
|
|
1728
|
-
|
|
1740
|
+
const start = liveStart(); if (!S.locked && !S.userPaused) run(start); return true;
|
|
1741
|
+
}
|
|
1742
|
+
// Live: a call starts when you join it. Only what happened since then, and was not said yet, is said (the last 16 events at most, when a session is picked up on the way);
|
|
1743
|
+
// the end of a session that was already over is on screen (states, transcript) but is not read again. Joining while the agents are at work, the last few seconds are said too, to know what they are on
|
|
1744
|
+
const JOIN_BACK_MS = 10000;
|
|
1745
|
+
function midWork() { // someone is in the middle of something: the session does not end on a message of the lead (its turn is over then, or about to be)
|
|
1746
|
+
let k = S.events.length - 1; while (k >= 0 && (S.events[k].kind === 'usage' || S.events[k].kind === 'compact')) k--; const e = S.events[k]; return !!e && !(e.agent === 'main' && e.kind === 'say');
|
|
1747
|
+
}
|
|
1748
|
+
function liveStart() {
|
|
1749
|
+
const n = S.events.length; if (!S.locked && !S.since) S.since = Date.now() - (midWork() ? JOIN_BACK_MS : 0); // S.since 0: joining now
|
|
1750
|
+
const since = S.locked ? Date.now() : S.since; let i = n; while (i > Math.max(0, n - 16) && S.events[i - 1].ts > since) i--;
|
|
1751
|
+
recomputeStatus(i); S.idx = i; trNew(Math.max(0, n - 16)); paintStatus(); return i;
|
|
1729
1752
|
}
|
|
1730
1753
|
let polling = false;
|
|
1731
1754
|
setInterval(async () => {
|
|
@@ -1818,7 +1841,7 @@ async function setMode(m) {
|
|
|
1818
1841
|
const live = m === 'live'; if (live === S.live && S.cur) return; pause(); S.live = live; S.userPaused = false;
|
|
1819
1842
|
document.querySelectorAll('#modeSeg button').forEach((b) => b.classList.toggle('on', b.dataset.m === m)); $('#sessions').style.display = live ? 'none' : '';
|
|
1820
1843
|
S.cur = null; syncUI();
|
|
1821
|
-
if (live) { fillProjects(); await followCurrent(); return; }
|
|
1844
|
+
if (live) { S.since = 0; fillProjects(); await followCurrent(); return; }
|
|
1822
1845
|
$('#liveProj').style.display = 'none';
|
|
1823
1846
|
const rows = await (await fetch('/api/sessions')).json();
|
|
1824
1847
|
$('#sessions').innerHTML = rows.map((r) => { const d = new Date(r.mtime).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
@@ -2004,7 +2027,7 @@ $('#eDemo').onclick = () => setMode('demo');
|
|
|
2004
2027
|
$('#gB').onclick = () => {
|
|
2005
2028
|
claimAudio(); AUD.init(); AUD.apply(); S.locked = false; $('#gate').classList.remove('on'); syncUI();
|
|
2006
2029
|
if (synth && !EMBED) { try { synth.speak(new SpeechSynthesisUtterance('')); } catch {} } // unlock browser speech
|
|
2007
|
-
if (S.live && S.events.length) run(
|
|
2030
|
+
S.since = 0; if (S.live && S.events.length) run(liveStart()); // what arrived while the door was closed is past too
|
|
2008
2031
|
};
|
|
2009
2032
|
// J / K (or a click on the “need you” chip) jump to the agents that need you or are stalled, one after the other
|
|
2010
2033
|
function jumpAttn(dir = 1) {
|
package/server.js
CHANGED
|
@@ -251,6 +251,12 @@ function parseRecord(st, r) {
|
|
|
251
251
|
if (id && status && status !== 'running') st.raw.push({ ts, agent, kind: 'agentdone', toolId: id });
|
|
252
252
|
return;
|
|
253
253
|
}
|
|
254
|
+
if (r.type === 'attachment' && r.attachment && r.attachment.type === 'queued_command') { // an order typed while the lead was working: Claude Code hands it over in the middle of the turn, as an attachment and not as a user message
|
|
255
|
+
const q = r.attachment, human = q.origin ? q.origin.kind === 'human' : q.commandMode === 'prompt'; if (agent !== 'main' || !human) return; // the same record also carries task notifications
|
|
256
|
+
const txt = (Array.isArray(q.prompt) ? q.prompt.filter((b) => b && b.type === 'text').map((b) => b.text).join('\n') : typeof q.prompt === 'string' ? q.prompt : '').replace(HARNESS, '').trim();
|
|
257
|
+
if (txt && !/^<(system-reminder|command-|local-command|task-notification)/.test(txt)) st.raw.push({ ts, agent: 'user', kind: 'user', text: txt });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
254
260
|
if (r.type === 'user') {
|
|
255
261
|
const items = []; // tool results: the real output that appeared on the agent's screen
|
|
256
262
|
for (const b of blocks(r)) if (b.type === 'tool_result' && (st.toolName[b.tool_use_id] === 'Agent' || st.toolName[b.tool_use_id] === 'Task') && !(st.bg && st.bg[b.tool_use_id]) && !/^\s*Async agent launched/i.test(resultText(b.content))) st.raw.push({ ts, agent, kind: 'agentdone', toolId: b.tool_use_id }); // a synchronous call: the lead got the sub-agent's answer back, so it has finished (async launches answer immediately and are not a completion)
|