@double_geste/agent-standup 1.25.0 → 1.25.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@double_geste/agent-standup",
3
- "version": "1.25.0",
3
+ "version": "1.25.1",
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
@@ -280,6 +280,7 @@
280
280
  <select id="sessions" style="display:none"></select></div>
281
281
  <h4 data-fr="Personnages" data-en="Characters">Personnages</h4>
282
282
  <div class="box"><div id="charInfo" class="dim" style="font-size:11.5px;line-height:1.5"></div><div style="display:flex;gap:6px;margin-top:6px"><button class="btn" id="charReload" data-fr="Recharger" data-en="Reload">Recharger</button><button class="btn" id="charOpen" style="display:none" data-fr="Ouvrir le dossier" data-en="Open folder">Ouvrir le dossier</button></div></div>
283
+ <button class="mi" data-t="customLaptop"><span data-fr="Ordinateur devant vos personnages" data-en="Laptop in front of your own characters">Ordinateur devant vos personnages</span><i class="sw"></i></button>
283
284
  <h4 data-fr="Outils" data-en="Tools">Outils</h4>
284
285
  <button class="mi" id="mCoffee" style="display:none"><span id="mCoffeeT"></span></button>
285
286
  <button class="mi" id="mKeys"><span data-fr="⌨ Raccourcis clavier (?)" data-en="⌨ Keyboard shortcuts (?)">⌨ Raccourcis clavier (?)</span></button>
@@ -374,7 +375,12 @@ async function loadChars() {
374
375
  } catch {}
375
376
  CHARS = next;
376
377
  }
377
- const customFor = (id) => (id === 'main' ? CHARS.lead : id === 'user' ? CHARS.you : CHARS.pool.length ? CHARS.pool.reduce((b, c) => (hash(id + '|' + (c.idle || '')) > hash(id + '|' + (b.idle || '')) ? c : b)) : null);
378
+ const customFor = (id) => { // sub-agents: the best-hashed image no other agent holds; the whole pool again only once every image is taken
379
+ if (id === 'main') return CHARS.lead; if (id === 'user') return CHARS.you; if (!CHARS.pool.length) return null;
380
+ const used = new Set(Object.keys(S.agents).filter((k) => k !== id && S.agents[k].custom).map((k) => S.agents[k].custom.name));
381
+ const free = CHARS.pool.filter((c) => !used.has(c.name)), from = free.length ? free : CHARS.pool;
382
+ return from.reduce((b, c) => (hash(id + '|' + (c.idle || '')) > hash(id + '|' + (b.idle || '')) ? c : b));
383
+ };
378
384
  function renderCharInfo() {
379
385
  const el = $('#charInfo'); if (!el) return;
380
386
  el.innerHTML = CHARS.n ? `✓ ${CHARS.n} ${esc(L('personnage(s) personnalisé(s) chargé(s)', 'custom character(s) loaded'))}` : `${esc(L('Déposez vos images (PNG) dans ce dossier pour remplacer les personnages :', 'Drop your pixel art (PNG) in this folder to replace the characters:'))}<br><code style="word-break:break-all">${esc(CHARS.dir)}</code><br>${esc(L('lead.png = le chef · you.png = vous · les autres = les sous-agents · nom-talk.png = bouche ouverte', 'lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth'))}`;
@@ -470,6 +476,7 @@ function avatarSVG(p, o, uid = '') { // uid keeps SVG ids unique per instance:
470
476
  `<rect x="79" y="55.6" width="7.4" height="6.8" rx="1.6" fill="#f6f2ea"/><path d="M86.2 57q3.2 0 3.2 2.2t-3.2 2.2" fill="none" stroke="#f6f2ea" stroke-width="1.3"/><g class="steam" fill="none" stroke="#fff" stroke-width=".8" stroke-linecap="round" opacity=".7"><path d="M81.5 53q-1.4-1.8 0-3.4M84.3 53q-1.4-1.8 0-3.4"/></g>`,
471
477
  `<rect x="76" y="58.2" width="14" height="4.2" rx="1" fill="#ff7a6b"/><rect x="77.5" y="54.4" width="12" height="3.8" rx="1" fill="#4cc3ff"/><rect x="76.5" y="50.8" width="11" height="3.6" rx="1" fill="#ffd84a"/>`][p.prop];
472
478
  const lap = `<rect x="33" y="46" width="34" height="18" rx="2.8" fill="#d3d8e6"/><rect x="33" y="46" width="34" height="18" rx="2.8" fill="url(#${uid}lg)" opacity=".5"/><circle cx="50" cy="55" r="2.3" fill="${c1}"/><rect x="29.5" y="63.4" width="41" height="3" rx="1.5" fill="#aab1c2"/>`;
479
+ const lapDark = `<rect x="33" y="46" width="34" height="18" rx="2.8" fill="#3b4152"/><rect x="33" y="46" width="34" height="18" rx="2.8" fill="url(#${uid}lg)" opacity=".18"/><circle cx="50" cy="55" r="2.3" fill="${c1}"/><rect x="29.5" y="63.4" width="41" height="3" rx="1.5" fill="#262a36"/>`; // for custom characters: darker, so it still shows under a white or cream sprite
473
480
  const hands = `<circle class="hand l" cx="38" cy="65.6" r="3.1" fill="${c2}"/><circle class="hand r" cx="62" cy="65.6" r="3.1" fill="${c2}"/>`;
474
481
  const sparks = star(22, 28, 3) + star(78, 22, 2.6, '#ff9bb8') + star(70, 40, 1.9, '#9be7ff') + star(30, 46, 1.6, '#ffe58a');
475
482
  const custFig = (c, talking) => { const base = 'x="20" y="6" width="60" height="58" preserveAspectRatio="xMidYMax meet"', px = 'image-rendering:pixelated;';
@@ -484,7 +491,7 @@ function avatarSVG(p, o, uid = '') { // uid keeps SVG ids unique per instance:
484
491
  ${fig}
485
492
  </g>
486
493
  <rect y="62" width="100" height="13" fill="url(#${uid}dk${p.scene})"/><rect y="62" width="100" height="1.3" fill="#fff" opacity=".38"/><rect y="63.3" width="100" height="2.6" fill="#000" opacity=".1"/>
487
- ${prop}${p.custom ? '' : V('laptop', working, lap) + V('hands', working, hands)}${V('sparks', done, `<g class="sparks">${sparks}</g>`)}
494
+ ${prop}${p.custom ? (prefs.customLaptop ? V('laptop', working, lapDark) : '') : V('laptop', working, lap) + V('hands', working, hands)}${V('sparks', done, `<g class="sparks">${sparks}</g>`)}
488
495
  </svg>`;
489
496
  }
490
497
 
@@ -496,7 +503,10 @@ const S = { owner: null, errRun: {}, exQ: [], agents: {}, events: [], idx: 0, pl
496
503
  clock: 0, doneAt: {}, gone: new Set(), leaving: new Set(), demo: false, hold: false };
497
504
  const VANISH_MS = 120000; // a finished agent leaves the call after 2 minutes (playback time)
498
505
  // Live: how far the call is behind the real session, and what to do about it (speak faster, say less, skip decoration) so it keeps the rhythm of the real conversation
499
- const lagMs = () => { if (!S.live) return 0; const e = S.events[Math.min(S.idx, S.events.length - 1)]; return e && e.ts ? Math.max(0, Date.now() - e.ts) : 0; };
506
+ const lagMs = (i = S.idx) => { // how much newer the newest event of the session is than the one being played: 0 when the call is up to date, however long the session has been idle
507
+ if (!S.live || !S.events.length) return 0; const e = S.events[Math.max(0, Math.min(i, S.events.length - 1))]; let last = S.events.length - 1; while (last > 0 && !S.events[last].ts) last--;
508
+ return e && e.ts && S.events[last].ts ? Math.max(0, S.events[last].ts - e.ts) : 0;
509
+ };
500
510
  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
501
511
  const caughtUp = () => !S.live || lagMs() < 6000;
502
512
  const spd = () => (S.live ? 1.12 * lagFactor() : +$('#speed').value) * (S.boost || 1);
@@ -1583,13 +1593,13 @@ async function run(i) {
1583
1593
  if (S.idle) { S.idle = false; syncUI(); }
1584
1594
  if (S.live) { // way behind: skip ahead to stay current, but only count what is actually said: sub-agents produce thousands of silent tool events
1585
1595
  const rem = S.events.length - i;
1586
- if (rem > 300 || aheadSpeakable(i, 16) > 16 || lagMs() > 60000) { // only a really big backlog is skipped; before that, lines are shortened instead
1596
+ if (rem > 300 || aheadSpeakable(i, 16) > 16 || lagMs(i) > 60000) { // only a really big backlog is skipped; before that, lines are shortened instead
1587
1597
  let k = S.events.length, back = 0; while (k > i && back < 8) { k--; if (SPK.has(S.events[k].kind)) back++; }
1588
1598
  const to = rem > 300 ? Math.max(k, S.events.length - 14) : k; if (to > i) { i = to; recomputeStatus(i); paintStatus(); }
1589
1599
  }
1590
1600
  }
1591
1601
  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
1592
- const lg = lagMs() / 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
1602
+ 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
1593
1603
  S.boost = S.hurry || (S.live && S.events.length - i > 60) ? 3 : 1;
1594
1604
  const ev = S.events[i]; S.idx = i; tally(ev);
1595
1605
  if (!S.live && (ev.kind === 'say' || ev.kind === 'report' || ev.kind === 'order' || ev.kind === 'tools')) { await ponder(i, token); if (token !== S.token) return; }
@@ -1897,7 +1907,7 @@ function setView(v) { prefs.view = v; savePrefs(); document.body.classList.toggl
1897
1907
 
1898
1908
  $('#viewSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setView(b.dataset.v); };
1899
1909
  $('#langSel').onchange = () => setLang($('#langSel').value, true);
1900
- $('#charReload').onclick = async () => { await loadChars(); for (const k in imgCache) delete imgCache[k]; for (const id in S.agents) S.agents[id].custom = customFor(id); buildStage(); paintStatus(); renderCharInfo(); };
1910
+ $('#charReload').onclick = async () => { await loadChars(); for (const k in imgCache) delete imgCache[k]; for (const id in S.agents) S.agents[id].custom = null; for (const id in S.agents) S.agents[id].custom = customFor(id); buildStage(); paintStatus(); renderCharInfo(); };
1901
1911
  $('#charOpen').onclick = () => window.parent.postMessage({ agentStandup: 'openCharacters' }, '*');
1902
1912
  $('#modeSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setMode(b.dataset.m); };
1903
1913
  // ⚙ menu: sections that fold (remembered) and a search box
@@ -1914,7 +1924,7 @@ function menuInit() {
1914
1924
  if (!q) secs.forEach((sc, i) => sc.h.classList.toggle('closed', (prefs.menuClosed || []).includes(i)));
1915
1925
  };
1916
1926
  }
1917
- $('#menu').onclick = (e) => { const b = e.target.closest('.mi[data-t]'); if (!b) return; prefs[b.dataset.t] = !prefs[b.dataset.t]; savePrefs(); if (prefs[b.dataset.t] && ['voice', 'office', 'music', 'notify'].includes(b.dataset.t)) claimAudio(); b.querySelector('.sw').classList.toggle('on', prefs[b.dataset.t]); AUD.apply(); if (b.dataset.t === 'office') $('#officeBox').style.display = prefs.office ? '' : 'none'; if (b.dataset.t === 'reactions') document.body.classList.toggle('react', prefs.reactions); if (b.dataset.t === 'roster') { document.body.classList.toggle('rost', prefs.roster); paintStates(); } if (b.dataset.t === 'notify') { prefs.notifyChosen = true; savePrefs(); } if (b.dataset.t === 'notify' && prefs.notify) askNotify(); if (b.dataset.t === 'vanish' && !prefs.vanish) { S.gone.clear(); paintStatus(); }
1927
+ $('#menu').onclick = (e) => { const b = e.target.closest('.mi[data-t]'); if (!b) return; prefs[b.dataset.t] = !prefs[b.dataset.t]; savePrefs(); if (prefs[b.dataset.t] && ['voice', 'office', 'music', 'notify'].includes(b.dataset.t)) claimAudio(); b.querySelector('.sw').classList.toggle('on', prefs[b.dataset.t]); AUD.apply(); if (b.dataset.t === 'office') $('#officeBox').style.display = prefs.office ? '' : 'none'; if (b.dataset.t === 'customLaptop') { for (const k in imgCache) delete imgCache[k]; buildStage(); paintStatus(); } if (b.dataset.t === 'reactions') document.body.classList.toggle('react', prefs.reactions); if (b.dataset.t === 'roster') { document.body.classList.toggle('rost', prefs.roster); paintStates(); } if (b.dataset.t === 'notify') { prefs.notifyChosen = true; savePrefs(); } if (b.dataset.t === 'notify' && prefs.notify) askNotify(); if (b.dataset.t === 'vanish' && !prefs.vanish) { S.gone.clear(); paintStatus(); }
1918
1928
  if (b.dataset.t === 'music') { $('#musicBox').style.display = prefs.music ? '' : 'none'; if (prefs.music) { AUD.init(); AUD.apply(); } } };
1919
1929
  $('#station').innerHTML = STATIONS.map((s) => `<option value="${s.id}">📻 ${s.name}</option>`).join('');
1920
1930
  $('#station').value = prefs.station; $('#musicVol').value = prefs.musicVol; $('#musicBox').style.display = prefs.music ? '' : 'none';
@@ -205,5 +205,6 @@
205
205
  "Lively": "Livlig",
206
206
  "Normal": "Normal",
207
207
  "nothing to report": "intet at rapportere",
208
- "Quiet": "Stille"
208
+ "Quiet": "Stille",
209
+ "Laptop in front of your own characters": "Bærbar foran dine egne figurer"
209
210
  }
@@ -206,5 +206,6 @@
206
206
  "Lively": "Lebhaft",
207
207
  "Normal": "Normal",
208
208
  "nothing to report": "nichts zu melden",
209
- "Quiet": "Ruhig"
209
+ "Quiet": "Ruhig",
210
+ "Laptop in front of your own characters": "Laptop vor deinen eigenen Figuren"
210
211
  }
@@ -75,6 +75,7 @@
75
75
  "Join the call": "Join the call",
76
76
  "Keyboard shortcuts": "Keyboard shortcuts",
77
77
  "Language": "Language",
78
+ "Laptop in front of your own characters": "Laptop in front of your own characters",
78
79
  "Lead": "Lead",
79
80
  "Lead context": "Lead context",
80
81
  "lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth",
@@ -206,5 +206,6 @@
206
206
  "Lively": "Animado",
207
207
  "Normal": "Normal",
208
208
  "nothing to report": "nada que informar",
209
- "Quiet": "Tranquilo"
209
+ "Quiet": "Tranquilo",
210
+ "Laptop in front of your own characters": "Portátil delante de tus propios personajes"
210
211
  }
@@ -206,5 +206,6 @@
206
206
  "Lively": "にぎやか",
207
207
  "Normal": "ふつう",
208
208
  "nothing to report": "報告事項なし",
209
- "Quiet": "静か"
209
+ "Quiet": "静か",
210
+ "Laptop in front of your own characters": "自作キャラクターの前にノートパソコン"
210
211
  }
@@ -206,5 +206,6 @@
206
206
  "Lively": "Animado",
207
207
  "Normal": "Normal",
208
208
  "nothing to report": "nada a reportar",
209
- "Quiet": "Quieto"
209
+ "Quiet": "Quieto",
210
+ "Laptop in front of your own characters": "Notebook na frente dos seus personagens"
210
211
  }
@@ -206,5 +206,6 @@
206
206
  "Lively": "热闹",
207
207
  "Normal": "正常",
208
208
  "nothing to report": "没什么可报告的",
209
- "Quiet": "安静"
209
+ "Quiet": "安静",
210
+ "Laptop in front of your own characters": "在自定义角色前显示笔记本电脑"
210
211
  }
@@ -206,5 +206,6 @@
206
206
  "Lively": "熱鬧",
207
207
  "Normal": "正常",
208
208
  "nothing to report": "沒什麼好回報的",
209
- "Quiet": "安靜"
209
+ "Quiet": "安靜",
210
+ "Laptop in front of your own characters": "在自訂角色前顯示筆記型電腦"
210
211
  }