@double_geste/agent-standup 1.25.0 → 1.25.2
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 +52 -19
- package/public/locales/da.json +5 -1
- package/public/locales/de.json +5 -1
- package/public/locales/en.json +4 -0
- package/public/locales/es.json +5 -1
- package/public/locales/ja.json +5 -1
- package/public/locales/pt-BR.json +5 -1
- package/public/locales/zh-CN.json +5 -1
- package/public/locales/zh-TW.json +5 -1
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.2",
|
|
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) =>
|
|
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 = () => {
|
|
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);
|
|
@@ -703,7 +713,7 @@ const TTS = {
|
|
|
703
713
|
for (const l of ['fr', 'en']) this.pool[l] = rankVoices(d.voices.filter((v) => v.locale.startsWith(l + '_')).map((v) => v.name)); } catch {}
|
|
704
714
|
},
|
|
705
715
|
voiceFor(p, fr = FR()) { const pool = this.pool[fr ? 'fr' : 'en']; return pool.length ? (p.id === 'main' ? pool[0] : pool[(p.vi >>> 2) % pool.length]) : null; },
|
|
706
|
-
rate() { return Math.round(Math.max(120, Math.min(330, 178 * (S.live ? 1.12 * lagFactor() : +$('#speed').value))) / 10) * 10; },
|
|
716
|
+
rate() { return Math.round(Math.max(120, Math.min(330, 178 * (S.live ? 1.12 * lagFactor() * (S.boost > 1 ? 1.25 : 1) : +$('#speed').value))) / 10) * 10; },
|
|
707
717
|
buf(text, voice, rate) {
|
|
708
718
|
const key = this.engine + '|' + voice + '|' + rate + '|' + text;
|
|
709
719
|
if (!this.cache.has(key)) {
|
|
@@ -1240,6 +1250,7 @@ setInterval(() => { // now and then a busy agent has a little thought; never w
|
|
|
1240
1250
|
}, 7000);
|
|
1241
1251
|
function oops(id) { if (!prefs.banter || !S.playing || S.recomputing) return; const t = $('#t-' + id); if (!t || t.style.display === 'none' || (t._oops && Date.now() - t._oops < 4000)) return; t._oops = Date.now(); t.classList.remove('oops'); void t.offsetWidth; t.classList.add('oops'); thought(id, '😬'); }
|
|
1242
1252
|
function confetti() {
|
|
1253
|
+
S.wrap = true; // the lead is done: if the end of its last message was never read, it is read now (see wrapUp)
|
|
1243
1254
|
if (!prefs.banter || document.hidden) return; const box = $('#center'), cols = ['#f5c04a', '#7c9cff', '#ff7a6b', '#5ad19a', '#c58bff'];
|
|
1244
1255
|
for (let i = 0; i < 26; i++) { const c = document.createElement('i'); c.className = 'confetti'; c.style.left = Math.random() * 100 + '%'; c.style.background = cols[i % cols.length]; c.style.animationDelay = Math.random() * 0.6 + 's'; c.style.animationDuration = 1.8 + Math.random() * 1.4 + 's'; box.appendChild(c); setTimeout(() => c.remove(), 4200); }
|
|
1245
1256
|
}
|
|
@@ -1521,7 +1532,7 @@ async function react(ev, token) {
|
|
|
1521
1532
|
if ((!prefs.epi && !prefs.banter) || S.boost > 1 || S.hurry || !caughtUp()) return; const seed = hash(String(ev.ts) + ev.agent), R = REACT[FR() ? 'fr' : 'en']; let who, line, thanks = false;
|
|
1522
1533
|
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]; }
|
|
1523
1534
|
else if (ev.kind === 'report') {
|
|
1524
|
-
const r = seed % 100, joke = prefs.epi ? 60 : 35; // a remark from someone else, or (a little less often) a word of thanks from whoever receives the report
|
|
1535
|
+
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
|
|
1525
1536
|
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]; }
|
|
1526
1537
|
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; }
|
|
1527
1538
|
else return;
|
|
@@ -1550,10 +1561,18 @@ function varied(kind, seed) { const P = TALK[FR() ? 'fr' : 'en'][kind]; let line
|
|
|
1550
1561
|
const thinkerOf = (e) => (e.kind === 'results' ? e.agent : e.kind === 'user' ? 'main' : e.kind === 'report' || e.kind === 'finish' || e.kind === 'order' ? e.to : null);
|
|
1551
1562
|
const before = (i) => { let k = i - 1; while (k >= 0 && (S.events[k].kind === 'usage' || S.events[k].kind === 'compact')) k--; return S.events[k]; };
|
|
1552
1563
|
async function ponder(i, token) { // replay: an agent that really took a long time (40 s or more) before this line or call says so first, about every other time
|
|
1553
|
-
const ev = S.events[i], p = before(i); if (!prefs.banter || 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;
|
|
1564
|
+
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;
|
|
1554
1565
|
const line = varied('think', hash(String(ev.ts))); startTalk(ev.agent, line, '💭'); await speakLine(line, S.agents[ev.agent], true, token); stopTalk(ev.agent);
|
|
1555
1566
|
}
|
|
1567
|
+
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
|
|
1568
|
+
if (!S.wrap || S.speakers.size || !prefs.voice && !prefs.banter) return false; S.wrap = false;
|
|
1569
|
+
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;
|
|
1570
|
+
const full = plain(e.text); let rest = m && m.ev === e ? full.slice(m.said).trim() : full; if (m && m.ev === e && m.full) return false; if (!m || m.ev !== e) rest = full;
|
|
1571
|
+
rest = rest.replace(/^[^\w\p{L}]+/u, ''); if (rest.length < 25) return false; const line = (m && m.ev === e ? L('Pour résumer : ', 'To sum up: ') : '') + short(rest, 520);
|
|
1572
|
+
startTalk('main', line, '🎉'); await speakLine(line, S.agents.main, true, token); stopTalk('main'); return true;
|
|
1573
|
+
}
|
|
1556
1574
|
async function smallTalk(token) {
|
|
1575
|
+
if (prefs.chat === 'quiet') return; // quiet: nothing to fill the silences, only the real exchanges
|
|
1557
1576
|
const n = S.fill || 0, last = S.events[S.events.length - 1]; if (!prefs.banter || n > 2 || !last || S.speakers.size || S.hurry || S.boost > 1 || (document.hidden && !S.exporting)) return;
|
|
1558
1577
|
const quiet = performance.now() - S.spokeAt, here = Object.keys(S.agents).filter((id) => id !== 'user' && $('#t-' + id) && $('#t-' + id).style.display !== 'none'), pick = (a) => a[Math.floor(Math.random() * a.length)];
|
|
1559
1578
|
const p = before(S.events.length), muser = S.live && !n && !S.mused && p ? thinkerOf(p) : null, think = !!muser && here.includes(muser) && quiet > 6000 && Date.now() - p.ts > 20000 && Date.now() - p.ts < 180000; // live: 20 s without a word from whoever is thinking, once per silence
|
|
@@ -1575,30 +1594,35 @@ const SPK = new Set(['say', 'report', 'order', 'user']);
|
|
|
1575
1594
|
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; }
|
|
1576
1595
|
const isSub = (id) => id !== 'main' && id !== 'user';
|
|
1577
1596
|
async function run(i) {
|
|
1578
|
-
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.syncTs = (S.events[i] && S.events[i].ts) || Date.now(); syncUI(); AUD.init();
|
|
1597
|
+
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();
|
|
1579
1598
|
if (i === 0 && prefs.epi && !S.live) { await card(L('ÉPISODE 1', 'EPISODE 1'), S.title); if (token !== S.token) return; }
|
|
1580
1599
|
while (token === S.token) {
|
|
1581
1600
|
const lim = S.stopAt || S.events.length;
|
|
1582
|
-
if (i >= lim) { if (S.live && !S.stopAt) { if (!S.idle) { S.idle = true; syncUI(); } await smallTalk(token); await sleep(350); continue; } break; }
|
|
1601
|
+
if (i >= lim) { if (S.live && !S.stopAt) { if (!S.idle) { S.idle = true; syncUI(); } if (await wrapUp(token)) continue; await smallTalk(token); await sleep(350); continue; } break; }
|
|
1583
1602
|
if (S.idle) { S.idle = false; syncUI(); }
|
|
1584
1603
|
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
1604
|
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
|
|
1605
|
+
if (rem > 300 || aheadSpeakable(i, 16) > 16 || lagMs(i) > 60000) { // only a really big backlog is skipped; before that, lines are shortened instead
|
|
1587
1606
|
let k = S.events.length, back = 0; while (k > i && back < 8) { k--; if (SPK.has(S.events[k].kind)) back++; }
|
|
1588
1607
|
const to = rem > 300 ? Math.max(k, S.events.length - 14) : k; if (to > i) { i = to; recomputeStatus(i); paintStatus(); }
|
|
1589
1608
|
}
|
|
1590
1609
|
}
|
|
1591
1610
|
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
|
|
1611
|
+
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
1612
|
S.boost = S.hurry || (S.live && S.events.length - i > 60) ? 3 : 1;
|
|
1594
1613
|
const ev = S.events[i]; S.idx = i; tally(ev);
|
|
1595
1614
|
if (!S.live && (ev.kind === 'say' || ev.kind === 'report' || ev.kind === 'order' || ev.kind === 'tools')) { await ponder(i, token); if (token !== S.token) return; }
|
|
1596
1615
|
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; }
|
|
1597
1616
|
if (ev.kind === 'finish') { applyStatus(ev); paintStatus(); i++; continue; }
|
|
1598
|
-
|
|
1599
|
-
if (ev.kind === '
|
|
1617
|
+
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
|
|
1618
|
+
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
|
|
1619
|
+
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; }
|
|
1600
1620
|
const group = [ev]; let j = i + 1; // sub-agents talking back-to-back were running in parallel: they talk over each other
|
|
1601
|
-
if (
|
|
1621
|
+
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
|
|
1622
|
+
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; }
|
|
1623
|
+
if (group.length > 1) { for (const g of group) applyStatus(g); paintStatus(); }
|
|
1624
|
+
}
|
|
1625
|
+
else if (isSub(ev.agent) && ev.kind !== 'order') {
|
|
1602
1626
|
while (j < Math.min(S.events.length, i + 28) && group.length < 3) {
|
|
1603
1627
|
const e2 = S.events[j];
|
|
1604
1628
|
if (e2.kind === 'usage' || e2.kind === 'compact') { tally(e2); j++; continue; }
|
|
@@ -1616,11 +1640,18 @@ async function run(i) {
|
|
|
1616
1640
|
startTalk(g.agent, line, label);
|
|
1617
1641
|
if (g.kind === 'order') { S.status[g.to] = 'working'; AUD.join(); setTimeout(() => { paintStatus(); fly(g.agent, g.to, '📨'); popTile(g.to); }, 30); }
|
|
1618
1642
|
if (g.kind === 'report') { AUD.ding(); setTimeout(() => { fly(g.agent, g.to, '✅'); popTile(g.agent); }, 30); }
|
|
1619
|
-
|
|
1643
|
+
const said = clipSpeech(line, S.pace); if (g.agent === 'main' && g.kind === 'say') { const full = plain(g.text), key = said.replace(/…$/, '').trim().slice(-30), pos = key ? full.indexOf(key) : -1, n = pos >= 0 ? pos + key.length : said.length; S.mainSaid = { ev: g, said: n, full: n >= full.length - 5 }; }
|
|
1644
|
+
return speakLine(said, S.agents[g.agent], true, token, FR() && trMem.has(baseText(g))).then(() => stopTalk(g.agent));
|
|
1620
1645
|
};
|
|
1621
|
-
let plays;
|
|
1646
|
+
let plays;
|
|
1647
|
+
if (ev.kind === 'order' && group.length > 1) { // the first order in full, then "Also Remy on X, Iris on Y"
|
|
1648
|
+
const rest = group.slice(1), line = `${L('Et aussi ', 'Also ')}${rest.map((o) => `${nameOf(o.to)}${L(' sur ', ' on ')}${(o.title || short(o.text, 60)).replace(/[.。]\s*$/, '')}`).join(', ')}.`;
|
|
1649
|
+
setTimeout(() => rest.forEach((o, k) => setTimeout(() => { fly(o.agent, o.to, '📨'); popTile(o.to); }, k * 120)), 30);
|
|
1650
|
+
plays = [playOne(ev).then(() => { if (token !== S.token) return; startTalk(ev.agent, line, ''); return speakLine(clipSpeech(line, S.pace), S.agents[ev.agent], true, token).then(() => stopTalk(ev.agent)); })];
|
|
1651
|
+
}
|
|
1652
|
+
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
|
|
1622
1653
|
S.idx = j - 1; paintStatus(); await Promise.all(plays); if (token !== S.token) return;
|
|
1623
|
-
for (const g of group) { await converse(g, token); if (token !== S.token) return; }
|
|
1654
|
+
for (const g of (ev.kind === 'order' && group.length > 1 ? [ev] : group)) { await converse(g, token); if (token !== S.token) return; }
|
|
1624
1655
|
i = j;
|
|
1625
1656
|
}
|
|
1626
1657
|
if (token === S.token) {
|
|
@@ -1691,7 +1722,9 @@ setInterval(async () => {
|
|
|
1691
1722
|
if (d.error) return; if (d.total < S.events.length) { S.cur = null; return; } // transcript was rewritten: reload next tick
|
|
1692
1723
|
if (!d.events || !d.events.length) return;
|
|
1693
1724
|
for (const a of d.agents) if (!S.agents[a.id]) { S.agents[a.id] = persona(a); addTile(S.agents[a.id]); }
|
|
1694
|
-
const from = S.events.length; S.events = S.events.concat(d.events); $('#seek').max = S.events.length - 1; trNew(from); warmTTS(from); renderLog(false);
|
|
1725
|
+
const from = S.events.length; S.events = S.events.concat(d.events); $('#seek').max = S.events.length - 1; trNew(from); warmTTS(from); renderLog(false);
|
|
1726
|
+
if (S.playing && !S.recomputing) for (const e of d.events) if (e.kind === 'order' && !S.status[e.to]) { S.status[e.to] = 'working'; S.orderTs[e.to] = e.ts || 0; S.gone.delete(e.to); } // a new sub-agent shows up now, while the previous line is still being read
|
|
1727
|
+
paintStatus();
|
|
1695
1728
|
} catch { if (!S.offline) { S.offline = true; syncUI(); } } finally { polling = false; }
|
|
1696
1729
|
}, 400);
|
|
1697
1730
|
|
|
@@ -1897,7 +1930,7 @@ function setView(v) { prefs.view = v; savePrefs(); document.body.classList.toggl
|
|
|
1897
1930
|
|
|
1898
1931
|
$('#viewSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setView(b.dataset.v); };
|
|
1899
1932
|
$('#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(); };
|
|
1933
|
+
$('#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
1934
|
$('#charOpen').onclick = () => window.parent.postMessage({ agentStandup: 'openCharacters' }, '*');
|
|
1902
1935
|
$('#modeSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setMode(b.dataset.m); };
|
|
1903
1936
|
// ⚙ menu: sections that fold (remembered) and a search box
|
|
@@ -1914,7 +1947,7 @@ function menuInit() {
|
|
|
1914
1947
|
if (!q) secs.forEach((sc, i) => sc.h.classList.toggle('closed', (prefs.menuClosed || []).includes(i)));
|
|
1915
1948
|
};
|
|
1916
1949
|
}
|
|
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(); }
|
|
1950
|
+
$('#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
1951
|
if (b.dataset.t === 'music') { $('#musicBox').style.display = prefs.music ? '' : 'none'; if (prefs.music) { AUD.init(); AUD.apply(); } } };
|
|
1919
1952
|
$('#station').innerHTML = STATIONS.map((s) => `<option value="${s.id}">📻 ${s.name}</option>`).join('');
|
|
1920
1953
|
$('#station').value = prefs.station; $('#musicVol').value = prefs.musicVol; $('#musicBox').style.display = prefs.music ? '' : 'none';
|
package/public/locales/da.json
CHANGED
|
@@ -205,5 +205,9 @@
|
|
|
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",
|
|
210
|
+
"Also ": "Også ",
|
|
211
|
+
" on ": " på ",
|
|
212
|
+
"To sum up: ": "Kort sagt: "
|
|
209
213
|
}
|
package/public/locales/de.json
CHANGED
|
@@ -206,5 +206,9 @@
|
|
|
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",
|
|
211
|
+
"Also ": "Außerdem ",
|
|
212
|
+
" on ": " an ",
|
|
213
|
+
"To sum up: ": "Zusammengefasst: "
|
|
210
214
|
}
|
package/public/locales/en.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
" full — compaction soon": " full — compaction soon",
|
|
3
|
+
" on ": " on ",
|
|
3
4
|
"”": "”",
|
|
4
5
|
"↗ Open in browser": "↗ Open in browser",
|
|
5
6
|
"⌨ Keyboard shortcuts (?)": "⌨ Keyboard shortcuts (?)",
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
"Agents": "Agents",
|
|
15
16
|
"Agents list (left)": "Agents list (left)",
|
|
16
17
|
"All projects (most recent)": "All projects (most recent)",
|
|
18
|
+
"Also ": "Also ",
|
|
17
19
|
"Ambience volume": "Ambience volume",
|
|
18
20
|
"An agent may be waiting for you ✋": "An agent may be waiting for you ✋",
|
|
19
21
|
"Animal Crossing (babble)": "Animal Crossing (babble)",
|
|
@@ -75,6 +77,7 @@
|
|
|
75
77
|
"Join the call": "Join the call",
|
|
76
78
|
"Keyboard shortcuts": "Keyboard shortcuts",
|
|
77
79
|
"Language": "Language",
|
|
80
|
+
"Laptop in front of your own characters": "Laptop in front of your own characters",
|
|
78
81
|
"Lead": "Lead",
|
|
79
82
|
"Lead context": "Lead context",
|
|
80
83
|
"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",
|
|
@@ -169,6 +172,7 @@
|
|
|
169
172
|
"This help": "This help",
|
|
170
173
|
"Time": "Time",
|
|
171
174
|
"to": "to",
|
|
175
|
+
"To sum up: ": "To sum up: ",
|
|
172
176
|
"tokens": "tokens",
|
|
173
177
|
"Tokens": "Tokens",
|
|
174
178
|
"Tokens recorded by Claude Code (counted once per message, not a bill)": "Tokens recorded by Claude Code (counted once per message, not a bill)",
|
package/public/locales/es.json
CHANGED
|
@@ -206,5 +206,9 @@
|
|
|
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",
|
|
211
|
+
"Also ": "También ",
|
|
212
|
+
" on ": " en ",
|
|
213
|
+
"To sum up: ": "En resumen: "
|
|
210
214
|
}
|
package/public/locales/ja.json
CHANGED
|
@@ -206,5 +206,9 @@
|
|
|
206
206
|
"Lively": "にぎやか",
|
|
207
207
|
"Normal": "ふつう",
|
|
208
208
|
"nothing to report": "報告事項なし",
|
|
209
|
-
"Quiet": "静か"
|
|
209
|
+
"Quiet": "静か",
|
|
210
|
+
"Laptop in front of your own characters": "自作キャラクターの前にノートパソコン",
|
|
211
|
+
"Also ": "それから ",
|
|
212
|
+
" on ": " は ",
|
|
213
|
+
"To sum up: ": "まとめると: "
|
|
210
214
|
}
|
|
@@ -206,5 +206,9 @@
|
|
|
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",
|
|
211
|
+
"Also ": "Também ",
|
|
212
|
+
" on ": " em ",
|
|
213
|
+
"To sum up: ": "Resumindo: "
|
|
210
214
|
}
|