@double_geste/agent-standup 1.24.3 → 1.25.0
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 +219 -31
- package/public/locales/da.json +21 -1
- package/public/locales/de.json +21 -1
- package/public/locales/en.json +20 -0
- package/public/locales/es.json +21 -1
- package/public/locales/ja.json +21 -1
- package/public/locales/pt-BR.json +21 -1
- package/public/locales/zh-CN.json +21 -1
- package/public/locales/zh-TW.json +21 -1
- package/server.js +5 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@double_geste/agent-standup",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
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
|
@@ -190,6 +190,24 @@
|
|
|
190
190
|
.coffee-btn { color:#ffd24a !important; background:rgba(255,210,74,.12); box-shadow:inset 0 0 0 1px rgba(255,210,74,.35) } .coffee-btn:hover { background:rgba(255,210,74,.22) !important }
|
|
191
191
|
.glink { color:var(--dim); font-size:12.5px; text-decoration:underline; text-underline-offset:3px } .glink:hover { color:#ffd24a }
|
|
192
192
|
#mCoffee { background:rgba(255,210,74,.10); box-shadow:inset 0 0 0 1px rgba(255,210,74,.3); margin-top:4px } #mCoffee:hover { background:rgba(255,210,74,.2) }
|
|
193
|
+
#menuQ { background:var(--bg); color:var(--text); border:1px solid var(--line); border-radius:8px; padding:6px 9px; font:inherit; font-size:12.5px; outline:none } #menuQ:focus { border-color:var(--accent) }
|
|
194
|
+
#switcher { position:fixed; inset:0; z-index:20; background:rgba(5,6,12,.7); display:flex; align-items:flex-start; justify-content:center; padding-top:12vh } #switcher .sbox { width:min(560px,92vw); background:var(--panel); border:1px solid var(--line); border-radius:14px; box-shadow:0 20px 60px rgba(0,0,0,.6); overflow:hidden }
|
|
195
|
+
#swq { width:100%; border:0; border-bottom:1px solid var(--line); background:transparent; color:var(--text); font:inherit; font-size:15px; padding:14px 16px; outline:none } #swl { max-height:50vh; overflow:auto } .swr { display:grid; grid-template-columns:auto 1fr; gap:2px 10px; padding:9px 14px; cursor:pointer; align-items:center } .swr.on, .swr:hover { background:var(--panel2) } .swr b { font-size:13px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap } .swr i { grid-column:2; font-style:normal; font-size:11px; color:var(--dim) }
|
|
196
|
+
#sps { display:none; gap:2px } body.replay #sps { display:inline-flex } #sps button { background:var(--panel2); color:var(--dim); border:1px solid var(--line); border-radius:6px; padding:1px 6px; font:inherit; font-size:11px; cursor:pointer } #sps button.on { color:var(--text); border-color:var(--accent) } body.compact #sps { display:none !important }
|
|
197
|
+
#menu .mfold, #menu .mhide { display:none !important } #menu h4.fold { cursor:pointer; user-select:none } #menu h4.fold::before { content:'▾ ' } #menu h4.fold.closed::before { content:'▸ ' }
|
|
198
|
+
/* compact: a very narrow panel (the VS Code side bar) shows one row per agent instead of tiles */
|
|
199
|
+
body.compact #stage { display:flex; flex-direction:column; gap:6px; padding:8px; overflow:auto }
|
|
200
|
+
body.compact .tile, body.compact .tile.focus { height:54px !important; flex:none; border-radius:10px; display:flex; align-items:center; overflow:visible; animation:none; order:0 } body.compact .tile.lead { order:-2 }
|
|
201
|
+
body.compact .tile > svg:first-child { width:54px; height:54px; flex:none; border-radius:9px 0 0 9px }
|
|
202
|
+
body.compact .tile .bubble, body.compact .tile .thought, body.compact .tile .ticker, body.compact .tile .tbadge { display:none !important }
|
|
203
|
+
body.compact .tile .tag { position:static; flex:1; min-width:0; padding:0 8px; flex-direction:column; gap:2px; align-items:flex-start } body.compact .tile .tag b, body.compact .tile .tag span { background:none; backdrop-filter:none; padding:0; max-width:100%; overflow:hidden; text-overflow:ellipsis }
|
|
204
|
+
body.compact .tile .tag span:not(.tk):not(.cx) { display:none }
|
|
205
|
+
body.compact .tile .status { position:static; max-width:none; margin-right:6px; font-size:10.5px; padding:2px 7px; flex:none; max-width:46% }
|
|
206
|
+
body.compact .tile .prop { top:2px; left:34px; font-size:16px }
|
|
207
|
+
body.compact .tile .status:empty { display:none }
|
|
208
|
+
body.compact .tile .tag { order:1 } body.compact .tile .status { order:2 } body.compact .tile .prop { order:3 }
|
|
209
|
+
body.compact footer { gap:6px; padding-left:8px; padding-right:8px } body.compact footer label.dim { display:none } body.compact #speed { width:70px; min-width:0 }
|
|
210
|
+
body.compact #present { display:none !important } body.compact #hl { display:none } body.compact #gantt { display:none } body.compact #roster { display:none }
|
|
193
211
|
#menu.on { display:block } #menu h4 { margin:8px 10px 4px; font-size:10.5px; letter-spacing:.08em; text-transform:uppercase; color:var(--dim); font-weight:600 }
|
|
194
212
|
.mi { display:flex; align-items:center; justify-content:space-between; gap:10px; width:100%; text-align:left; padding:8px 10px; border-radius:8px } .mi:hover { background:var(--panel2) }
|
|
195
213
|
.sw { width:30px; height:17px; border-radius:99px; background:#3a3e50; position:relative; flex:none; transition:background .15s } .sw::after { content:''; position:absolute; top:2px; left:2px; width:13px; height:13px; border-radius:50%; background:#fff; transition:transform .15s }
|
|
@@ -236,6 +254,7 @@
|
|
|
236
254
|
</header>
|
|
237
255
|
|
|
238
256
|
<div id="menu">
|
|
257
|
+
<input id="menuQ" type="search" placeholder="Search settings…" aria-label="Search settings" style="margin:4px 8px 2px;width:calc(100% - 16px)">
|
|
239
258
|
<h4 data-fr="Langue" data-en="Language">Langue</h4>
|
|
240
259
|
<div class="box"><select id="langSel" aria-label="Language"></select></div>
|
|
241
260
|
<div class="hint" id="trHint" style="display:none"></div>
|
|
@@ -248,9 +267,11 @@
|
|
|
248
267
|
<div class="box" id="musicBox" style="display:none"><select id="station"></select><input id="musicVol" type="range" min="0" max="1" step="0.05" style="width:100%" aria-label="Music volume"></div>
|
|
249
268
|
<div class="box" id="vstyleBox"><select id="vstyle"></select></div>
|
|
250
269
|
<div class="box" id="kokoroBox" style="display:none"><button class="btn" id="kokoroInstall" style="width:100%"></button><div class="hint" id="kokoroHint" style="padding:2px 0 6px"></div></div>
|
|
270
|
+
<h4 data-fr="Affichage" data-en="Display">Affichage</h4>
|
|
251
271
|
<button class="mi" data-t="roster"><span data-fr="Liste des agents (à gauche)" data-en="Agents list (left)">Liste des agents (à gauche)</span><i class="sw"></i></button>
|
|
252
272
|
<button class="mi" data-t="reactions"><span data-fr="Barre de réactions (👍 👏 ☕)" data-en="Reactions bar (👍 👏 ☕)">Barre de réactions (👍 👏 ☕)</span><i class="sw"></i></button>
|
|
253
273
|
<button class="mi" data-t="banter"><span data-fr="Petites répliques et animations" data-en="Little lines & animations">Petites répliques et animations</span><i class="sw"></i></button>
|
|
274
|
+
<div class="box"><div class="lbl"><span data-fr="Bavardage entre agents" data-en="Chatter between agents">Bavardage entre agents</span></div><select id="chatSel"><option value="quiet" data-fr="Discret" data-en="Quiet">Discret</option><option value="normal" data-fr="Normal" data-en="Normal">Normal</option><option value="lively" data-fr="Animé" data-en="Lively">Animé</option></select></div>
|
|
254
275
|
<button class="mi" data-t="vanish"><span data-fr="Retirer les agents terminés (2 min)" data-en="Remove finished agents (2 min)">Retirer les agents terminés (2 min)</span><i class="sw"></i></button>
|
|
255
276
|
<button class="mi" data-t="epi"><span data-fr="Réactions et blagues" data-en="Reactions & jokes">Réactions et blagues</span><i class="sw"></i></button>
|
|
256
277
|
<h4 data-fr="Source" data-en="Source">Source</h4>
|
|
@@ -287,7 +308,8 @@
|
|
|
287
308
|
</main>
|
|
288
309
|
<footer>
|
|
289
310
|
<input id="seek" type="range" min="0" max="0" value="0"><span class="dim" id="pos">0 / 0</span>
|
|
290
|
-
<label class="dim"><span data-fr="Vitesse" data-en="Speed">Vitesse</span> <input id="speed" type="range" min="0.5" max="
|
|
311
|
+
<label class="dim"><span data-fr="Vitesse" data-en="Speed">Vitesse</span> <input id="speed" type="range" min="0.5" max="8" step="0.25" value="1.25" style="width:80px;vertical-align:middle"></label>
|
|
312
|
+
<span id="sps" class="dim" role="group" aria-label="Speed presets"><button data-s="1">1×</button><button data-s="2">2×</button><button data-s="4">4×</button><button data-s="8">8×</button></span>
|
|
291
313
|
</footer>
|
|
292
314
|
<div id="card"></div><div id="recap"><div class="box"></div></div>
|
|
293
315
|
|
|
@@ -311,6 +333,7 @@ let prefs = { lang: 'en', voice: true, roster: true, banter: true, reactions: fa
|
|
|
311
333
|
const sv = window.SAVED_PREFS && typeof window.SAVED_PREFS === 'object' ? window.SAVED_PREFS : {}; Object.assign(prefs, (sv._t || 0) > (loc._t || 0) ? sv : loc); }
|
|
312
334
|
if (prefs.vstyle === 'babble') prefs.vstyle = 'animal', prefs.vstyleChosen = true;
|
|
313
335
|
if (!prefs.vstyleChosen && (prefs.vstyle === 'animal' || prefs.vstyle === 'sims')) prefs.vstyle = 'cartoon'; // the gibberish styles are opt-in: understandable voices by default
|
|
336
|
+
if (!prefs.notifyChosen) prefs.notify = true; // “tell me when the agents are done” is on unless the person chose otherwise
|
|
314
337
|
let prefsTimer = null;
|
|
315
338
|
const savePrefs = () => { prefs._t = Date.now(); try { localStorage.setItem('cc.prefs', JSON.stringify(prefs)); } catch {} clearTimeout(prefsTimer); prefsTimer = setTimeout(() => { fetch('/api/prefs', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(prefs) }).catch(() => {}); }, 400); };
|
|
316
339
|
// Languages: French and English live in the code as pairs L(french, english); every other language is a catalogue public/locales/<code>.json mapping the English text to its translation.
|
|
@@ -468,11 +491,15 @@ function avatarSVG(p, o, uid = '') { // uid keeps SVG ids unique per instance:
|
|
|
468
491
|
// =====================================================================
|
|
469
492
|
// state
|
|
470
493
|
// =====================================================================
|
|
471
|
-
const S = { owner: null, agents: {}, events: [], idx: 0, playing: false, token: 0, status: {}, tc: {}, cur: null, live: true, userPaused: false, boost: 1, stopAt: 0,
|
|
494
|
+
const S = { owner: null, errRun: {}, exQ: [], agents: {}, events: [], idx: 0, playing: false, token: 0, status: {}, tc: {}, cur: null, live: true, userPaused: false, boost: 1, stopAt: 0,
|
|
472
495
|
speakers: new Set(), say: {}, lastTool: {}, level: {}, detail: null, collapsed: new Set(), title: '', idle: false, locked: true, logTo: 0, trOk: true,
|
|
473
496
|
clock: 0, doneAt: {}, gone: new Set(), leaving: new Set(), demo: false, hold: false };
|
|
474
497
|
const VANISH_MS = 120000; // a finished agent leaves the call after 2 minutes (playback time)
|
|
475
|
-
|
|
498
|
+
// 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; };
|
|
500
|
+
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
|
+
const caughtUp = () => !S.live || lagMs() < 6000;
|
|
502
|
+
const spd = () => (S.live ? 1.12 * lagFactor() : +$('#speed').value) * (S.boost || 1);
|
|
476
503
|
const nameOf = (id) => (S.agents[id] || {}).name || id;
|
|
477
504
|
|
|
478
505
|
// ---------- text prep ----------
|
|
@@ -488,7 +515,7 @@ const speakable = (t) => t.replace(/(?:[\w.-]+\/)+([\w.-]+)/g, (m, f) => f.repla
|
|
|
488
515
|
|
|
489
516
|
// ---------- translation (local claude CLI) ----------
|
|
490
517
|
const trMem = new Map(), trPending = new Map();
|
|
491
|
-
const baseText = (ev, max) => short(ev.text, max || (S.live ? (ev.kind === 'order' ?
|
|
518
|
+
const baseText = (ev, max) => short(ev.text, max || (S.live ? (ev.kind === 'order' ? 220 : 180) : (ev.kind === 'order' ? 200 : 230)));
|
|
492
519
|
function ensureTr(texts) {
|
|
493
520
|
texts = [...new Set(texts)].filter((t) => t && !trMem.has(t));
|
|
494
521
|
const waits = texts.filter((t) => trPending.has(t)).map((t) => trPending.get(t)), fresh = texts.filter((t) => !trPending.has(t));
|
|
@@ -676,7 +703,7 @@ const TTS = {
|
|
|
676
703
|
for (const l of ['fr', 'en']) this.pool[l] = rankVoices(d.voices.filter((v) => v.locale.startsWith(l + '_')).map((v) => v.name)); } catch {}
|
|
677
704
|
},
|
|
678
705
|
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; },
|
|
679
|
-
rate() { return Math.round(Math.max(120, Math.min(330, 178 * (S.live ? 1.12 : +$('#speed').value))) / 10) * 10; },
|
|
706
|
+
rate() { return Math.round(Math.max(120, Math.min(330, 178 * (S.live ? 1.12 * lagFactor() : +$('#speed').value))) / 10) * 10; },
|
|
680
707
|
buf(text, voice, rate) {
|
|
681
708
|
const key = this.engine + '|' + voice + '|' + rate + '|' + text;
|
|
682
709
|
if (!this.cache.has(key)) {
|
|
@@ -844,7 +871,7 @@ const layout = () => {
|
|
|
844
871
|
if (nar) { k = cols; fr = cols > 1 ? 2 : 1; } // side panel: small tiles, a banner-sized spotlight
|
|
845
872
|
else if (best < 0) { k = Math.min(2, cols); fr = Math.min(2, maxRows); }
|
|
846
873
|
st.style.setProperty('--fcols', k); st.style.setProperty('--fr', fr); st.classList.toggle('one', cols === 1);
|
|
847
|
-
document.body.classList.toggle('narrow', $('#center').clientWidth < 480); drawGantt();
|
|
874
|
+
document.body.classList.toggle('narrow', $('#center').clientWidth < 480); document.body.classList.toggle('compact', $('#center').clientWidth < 300); drawGantt();
|
|
848
875
|
};
|
|
849
876
|
new ResizeObserver(layout).observe($('#center'));
|
|
850
877
|
|
|
@@ -922,7 +949,7 @@ function tally(ev) { // numbers that pile up over a session: token usage, erro
|
|
|
922
949
|
if (ev.kind === 'compact') { S.compacts[ev.agent] = (S.compacts[ev.agent] || 0) + 1; delete S.ctxWarned[ev.agent]; }
|
|
923
950
|
const det = loopStep(S.rep, ev); if (det) { S.loops[ev.agent] = det; later('badges', renderBadges, 300); } else if (S.loops[ev.agent] && (ev.kind === 'results' ? (S.loops[ev.agent].kind === 'errors' && ev.items.every((it) => !it.err)) : (ev.kind === 'tools' && S.rep[ev.agent].n < 2))) { delete S.loops[ev.agent]; later('badges', renderBadges, 300); }
|
|
924
951
|
if (ev.kind === 'usage') { const c = ev.u.i + ev.u.r + ev.u.w; if (c > 0 && ev.model !== '<synthetic>') { S.ctx[ev.agent] = c; S.model[ev.agent] = ev.model; later('tok', renderCtx, 300); } const u = (S.usage[ev.agent] = S.usage[ev.agent] || { i: 0, o: 0, r: 0, w: 0 }); u.i += ev.u.i; u.o += ev.u.o; u.r += ev.u.r; u.w += ev.u.w; later('tok', renderTokens, 300); }
|
|
925
|
-
else if (ev.kind === 'results') ev.items.forEach((it) => { if (it.err) { S.errs[ev.agent] = (S.errs[ev.agent] || 0) + 1; later('badges', renderBadges, 300); oops(ev.agent); } });
|
|
952
|
+
else if (ev.kind === 'results') ev.items.forEach((it) => { if (it.err) { S.errs[ev.agent] = (S.errs[ev.agent] || 0) + 1; later('badges', renderBadges, 300); oops(ev.agent); noteTrouble(ev.agent, false); } else noteTrouble(ev.agent, true); });
|
|
926
953
|
else if (ev.kind === 'tools' || ev.kind === 'say') S.lastAct[ev.agent] = ev.ts;
|
|
927
954
|
}
|
|
928
955
|
const tokTip = (u) => `${L('Jetons enregistrés par Claude Code (comptés une fois par message, ce n’est pas une facture)', 'Tokens recorded by Claude Code (counted once per message, not a bill)')}\n${L('entrée', 'input')} ${fmtTok(u.i)} · ${L('sortie', 'output')} ${fmtTok(u.o)} · ${L('écriture cache', 'cache write')} ${fmtTok(u.w)} · ${L('lecture cache', 'cache read')} ${fmtTok(u.r)}`;
|
|
@@ -962,10 +989,10 @@ function agentState(id, now) {
|
|
|
962
989
|
}
|
|
963
990
|
const STATE_CLASSES = ['working', 'waiting', 'blocked', 'stalled', 'done'];
|
|
964
991
|
function paintStates() {
|
|
965
|
-
const now = Date.now(), n = { working: 0, waiting: 0, blocked: 0, stalled: 0, done: 0 }, ros = [];
|
|
992
|
+
const now = Date.now(), n = { working: 0, waiting: 0, blocked: 0, stalled: 0, done: 0 }, ros = [], attn = [];
|
|
966
993
|
for (const p of Object.values(S.agents)) {
|
|
967
994
|
if (p.id === 'user') continue; const t = $('#t-' + p.id); if (!t) continue;
|
|
968
|
-
const st = agentState(p.id, now), shown = t.style.display !== 'none'; if (shown && n[st.k] !== undefined && !S.gone.has(p.id)) n[st.k]++;
|
|
995
|
+
const st = agentState(p.id, now), shown = t.style.display !== 'none'; if (shown && n[st.k] !== undefined && !S.gone.has(p.id)) { n[st.k]++; if (st.k === 'blocked' || st.k === 'stalled') attn.push(p.id); }
|
|
969
996
|
STATE_CLASSES.forEach((c) => t.classList.toggle(c, st.k === c));
|
|
970
997
|
const el = t.querySelector('.status'); if (el.textContent !== st.txt) el.textContent = st.txt; el.className = 'status ' + st.k;
|
|
971
998
|
const pr = t.querySelector('.prop'), want = { blocked: '✋', stalled: '😰', waiting: '⌛' }[st.k] || '', was = S.prevState[p.id]; // a raised hand, a sweat drop, an hourglass: the call metaphor for "needs you / stuck / waiting"
|
|
@@ -978,7 +1005,7 @@ function paintStates() {
|
|
|
978
1005
|
}
|
|
979
1006
|
const b = $('#board'), parts = [['working', '⚙', L('au travail', 'working')], ['waiting', '🔗', L('en attente', 'waiting')], ['blocked', '✋', L('vous attendent', 'need you')], ['stalled', '⚠', L('bloqués', 'stalled')], ['done', '✓', L('terminés', 'done')]]
|
|
980
1007
|
.filter(([k]) => n[k]).map(([k, i, l]) => `<span class="${k}" title="${n[k]} ${l}">${i} ${n[k]}<em> ${l}</em></span>`).join('');
|
|
981
|
-
if (b.innerHTML !== parts) b.innerHTML = parts; S.nBlocked = n.blocked + n.stalled; paintRoster(ros); return n;
|
|
1008
|
+
if (b.innerHTML !== parts) b.innerHTML = parts; S.nBlocked = n.blocked + n.stalled; S.attn = attn; paintRoster(ros); return n;
|
|
982
1009
|
}
|
|
983
1010
|
const RORDER = { blocked: 0, stalled: 1, working: 2, waiting: 3, idle: 4, done: 5 };
|
|
984
1011
|
function paintRoster(ros) { // a fixed list beside the call: who is doing what, urgent first, readable even with dozens of agents
|
|
@@ -1393,8 +1420,105 @@ function ackLine(ev) {
|
|
|
1393
1420
|
const pool = cat && hash(String(ev.ts)) % 4 ? R[cat] : R.any, seed = hash(String(ev.ts) + ev.to); let line = pool[seed % pool.length];
|
|
1394
1421
|
if (line === S.lastAck) line = pool[(seed + 1) % pool.length]; S.lastAck = line; return line;
|
|
1395
1422
|
}
|
|
1423
|
+
// =====================================================================
|
|
1424
|
+
// conversation: short exchanges between agents at the right moments, built from what really happened in the session
|
|
1425
|
+
// =====================================================================
|
|
1426
|
+
const CHAT = { quiet: 0.45, normal: 1, lively: 1.8 }, chatMul = () => CHAT[prefs.chat] || CHAT.quiet; // quiet unless chosen otherwise, so the call does not tire anyone
|
|
1427
|
+
const EXW = { en: { edit: ['edit', 'edits'], call: ['tool call', 'tool calls'], error: ['error', 'errors'] }, fr: { edit: ['modification', 'modifications'], call: ['appel d’outil', 'appels d’outils'], error: ['erreur', 'erreurs'] } };
|
|
1428
|
+
const P = (k, n) => { const w = EXW[FR() ? 'fr' : 'en'][k]; return `${n} ${n === 1 ? w[0] : w[1]}`; };
|
|
1429
|
+
const EXCH = {
|
|
1430
|
+
en: { done: ['All done.', 'Finished.', 'Okay, that’s done.', 'Done on my side.', 'That’s wrapped up.'],
|
|
1431
|
+
ask: ['Quick question: is that the whole scope?', 'Anything else I should touch?', 'Any constraints I should know about?', 'Should I check with you before big changes?'],
|
|
1432
|
+
askBack: ['Just that, thanks.', 'No, keep it focused.', 'Use your judgment.', 'Nothing special, go ahead.', 'That’s all, thanks.'],
|
|
1433
|
+
ok: [(f) => `Nice, ${f.what} and no errors.`, (f) => (f.file ? `Good, ${f.file} looks fine.` : 'Good, that looks fine.'), (f, b) => `Perfect${f.dur ? ', that took ' + f.dur : ''}. Thanks ${b}.`, (f) => `Great, ${f.what}. Clean.`],
|
|
1434
|
+
err: [(f) => `I saw ${P('error', f.errs)} along the way. All good now?`, (f) => `${P('error', f.errs)} on the way, is it solid?`],
|
|
1435
|
+
errBack: ['Yes, they were just retries.', 'All fixed in the end.', 'Yes, a few hiccups, nothing serious.', 'It’s solid now.'],
|
|
1436
|
+
slow: [(f) => `That took ${f.dur}. Anything blocking you?`, (f) => `${f.dur}, that was long. All fine?`], slowBack: ['No, it was just a big one.', 'Nothing blocking, just a lot to read.', 'All fine, it was heavy.'],
|
|
1437
|
+
chime: [(b) => `Good job, ${b}.`, () => 'Nice one.', (b) => `Great, ${b}.`],
|
|
1438
|
+
trouble: [(b) => `${b}, you okay? That’s a lot of errors.`, (b) => `${b}, need a hand with those errors?`], troubleBack: ['Working on it.', 'Yes, almost got it.', 'Give me a minute.', 'I’m on it, thanks.'],
|
|
1439
|
+
sync: ['Quick sync: where are we?', 'Status check, everyone?', 'Let’s do a quick round.'], syncAns: [(a) => `Still on it: ${a}.`, (a) => `${a}, going well.`, (a) => `Currently ${a}.`] },
|
|
1440
|
+
fr: { done: ['Terminé.', 'C’est fini.', 'Voilà, c’est fait.', 'Fini de mon côté.', 'C’est bouclé.'],
|
|
1441
|
+
ask: ['Petite question : c’est tout ce qu’il y a à faire ?', 'Il y a autre chose à toucher ?', 'Des contraintes que je dois connaître ?', 'Je te préviens avant les gros changements ?'],
|
|
1442
|
+
askBack: ['Juste ça, merci.', 'Non, reste concentré.', 'Fais au mieux.', 'Rien de spécial, vas-y.', 'C’est tout, merci.'],
|
|
1443
|
+
ok: [(f) => `Bien, ${f.what} et aucune erreur.`, (f) => (f.file ? `Bien, ${f.file} a l’air bon.` : 'Bien, ça a l’air bon.'), (f, b) => `Parfait${f.dur ? ', ça a pris ' + f.dur : ''}. Merci ${b}.`, (f) => `Super, ${f.what}. Propre.`],
|
|
1444
|
+
err: [(f) => `J’ai vu ${P('error', f.errs)} en route. Tout est bon maintenant ?`, (f) => `${P('error', f.errs)} en chemin, c’est solide ?`],
|
|
1445
|
+
errBack: ['Oui, c’étaient juste des essais.', 'Tout est réglé à la fin.', 'Oui, quelques accrocs, rien de grave.', 'C’est solide maintenant.'],
|
|
1446
|
+
slow: [(f) => `Ça a pris ${f.dur}. Quelque chose te bloque ?`, (f) => `${f.dur}, c’était long. Tout va bien ?`], slowBack: ['Non, c’était juste un gros morceau.', 'Rien ne bloque, il y avait beaucoup à lire.', 'Tout va bien, c’était lourd.'],
|
|
1447
|
+
chime: [(b) => `Bon boulot, ${b}.`, () => 'Joli.', (b) => `Super, ${b}.`],
|
|
1448
|
+
trouble: [(b) => `${b}, ça va ? Ça fait beaucoup d’erreurs.`, (b) => `${b}, besoin d’un coup de main avec ces erreurs ?`], troubleBack: ['Je m’en occupe.', 'Oui, j’y suis presque.', 'Donne-moi une minute.', 'Je gère, merci.'],
|
|
1449
|
+
sync: ['Petit point rapide : où en est-on ?', 'Point d’étape, tout le monde ?', 'On fait un tour de table rapide.'], syncAns: [(a) => `Toujours dessus : ${a}.`, (a) => `${a}, ça avance bien.`, (a) => `En ce moment : ${a}.`] },
|
|
1450
|
+
};
|
|
1451
|
+
const exPick = (arr, seed, ...args) => { let k = seed % arr.length; if (arr[k] === S.lastX && arr.length > 1) k = (k + 1) % arr.length; S.lastX = arr[k]; return typeof arr[k] === 'function' ? arr[k](...args) : arr[k]; };
|
|
1452
|
+
function exchFacts(i, agent) { // what this agent did since it received its order: edits, tool calls, errors, the last file, how long it took
|
|
1453
|
+
const ev = S.events; let o = -1;
|
|
1454
|
+
for (let k = i; k >= Math.max(0, i - 600); k--) if (ev[k].kind === 'order' && ev[k].to === agent) { o = k; break; }
|
|
1455
|
+
const f = { tools: 0, edits: 0, errs: 0, file: '', dur: '', ms: 0 };
|
|
1456
|
+
for (let k = o >= 0 ? o : Math.max(0, i - 250); k <= i; k++) {
|
|
1457
|
+
const e = ev[k]; if (e.agent !== agent) continue;
|
|
1458
|
+
if (e.kind === 'tools') for (const t of e.tools) { f.tools++; if (/^(Edit|Write|MultiEdit|NotebookEdit)/.test(t.name)) { f.edits++; f.file = base((t.d || {}).file || t.arg) || f.file; } }
|
|
1459
|
+
else if (e.kind === 'results') f.errs += e.items.filter((it) => it.err).length;
|
|
1460
|
+
}
|
|
1461
|
+
if (o >= 0 && ev[i].ts && ev[o].ts) { f.ms = Math.max(0, ev[i].ts - ev[o].ts); f.dur = fmtShort(f.ms); }
|
|
1462
|
+
f.what = f.edits ? P('edit', f.edits) : f.tools ? P('call', f.tools) : L('rien à signaler', 'nothing to report'); return f;
|
|
1463
|
+
}
|
|
1464
|
+
const exVisible = (id) => S.agents[id] && !S.gone.has(id) && $('#t-' + id) && $('#t-' + id).style.display !== 'none';
|
|
1465
|
+
async function exSay(id, line, token) { // one line of an exchange, then a short human pause
|
|
1466
|
+
if (!exVisible(id) || token !== S.token) return false; startTalk(id, line, '💬'); await speakLine(line, S.agents[id], true, token); stopTalk(id);
|
|
1467
|
+
if (token !== S.token) return false; await sleep((150 + Math.random() * 250) / spd()); return token === S.token;
|
|
1468
|
+
}
|
|
1469
|
+
const exReady = () => prefs.banter && !S.hurry && S.boost <= 1 && !S.recomputing && !S.speakers.size && caughtUp() && performance.now() - (S.exAt || 0) > (S.live ? 22000 : 9000 / spd()) / chatMul();
|
|
1470
|
+
const exLog = (kind) => { (S.exLog = S.exLog || []).push(kind); };
|
|
1471
|
+
function noteTrouble(agent, ok) { // three errors in a row: someone asks if everything is alright
|
|
1472
|
+
if (ok) { S.errRun[agent] = 0; return; } if (!prefs.banter || S.recomputing) return;
|
|
1473
|
+
S.errRun[agent] = (S.errRun[agent] || 0) + 1; if (S.errRun[agent] === 3) (S.exQ = S.exQ || []).push(agent);
|
|
1474
|
+
}
|
|
1475
|
+
function pickOther(not, prefer) { const ids = Object.keys(S.agents).filter((id) => id !== 'user' && !not.includes(id) && exVisible(id)); const pr = ids.filter(prefer || (() => false)); const a = pr.length ? pr : ids; return a.length ? a[Math.floor(Math.random() * a.length)] : null; }
|
|
1476
|
+
async function handoffX(g, seed, token) { // an order: the new agent asks a question, the lead answers
|
|
1477
|
+
const E = EXCH[FR() ? 'fr' : 'en'], b = g.to, a = g.agent; if (!exVisible(a) || !exVisible(b)) return false; exLog('handoff');
|
|
1478
|
+
return (await exSay(b, exPick(E.ask, seed), token)) && (await exSay(a, exPick(E.askBack, seed >>> 3), token));
|
|
1479
|
+
}
|
|
1480
|
+
async function reportX(g, i, seed, token) { // a report: the receiver reacts to what really happened
|
|
1481
|
+
const E = EXCH[FR() ? 'fr' : 'en'], b = g.agent, a = g.to; if (a === 'user' || !exVisible(a) || !exVisible(b)) return false; const f = exchFacts(i, b);
|
|
1482
|
+
if (f.errs > 0) { exLog('report-errors'); return (await exSay(a, exPick(E.err, seed, f), token)) && (await exSay(b, exPick(E.errBack, seed >>> 3), token)); }
|
|
1483
|
+
if (f.ms >= 120000) { exLog('report-slow'); return (await exSay(a, exPick(E.slow, seed, f), token)) && (await exSay(b, exPick(E.slowBack, seed >>> 3), token)); }
|
|
1484
|
+
exLog('report-ok'); if (!(await exSay(a, exPick(E.ok, seed, f, nameOf(b)), token))) return false;
|
|
1485
|
+
const c = (seed >>> 5) % 100 < 35 ? pickOther([a, b]) : null; if (c) await exSay(c, exPick(E.chime, seed >>> 7, nameOf(b)), token); return true;
|
|
1486
|
+
}
|
|
1487
|
+
async function troubleX(token) { // someone keeps failing: a colleague checks in
|
|
1488
|
+
const E = EXCH[FR() ? 'fr' : 'en']; while (S.exQ && S.exQ.length) {
|
|
1489
|
+
const x = S.exQ.shift(); if (!exVisible(x)) continue; const h = pickOther([x], (id) => id === 'main'); if (!h) continue; const seed = hash('t' + x + Date.now());
|
|
1490
|
+
exLog('trouble'); return (await exSay(h, exPick(E.trouble, seed, nameOf(x)), token)) && (await exSay(x, exPick(E.troubleBack, seed >>> 3), token));
|
|
1491
|
+
}
|
|
1492
|
+
return false;
|
|
1493
|
+
}
|
|
1494
|
+
async function syncX(g, token) { // every few minutes of the session: the lead asks where everyone is, the busy agents answer
|
|
1495
|
+
if (chatMul() < 0.9 || !g.ts || g.ts - (S.syncTs || 0) < 180000 || !exVisible('main')) return false;
|
|
1496
|
+
const now = Date.now(), busy = Object.keys(S.agents).filter((id) => id !== 'user' && id !== 'main' && exVisible(id) && agentState(id, now).k === 'working').slice(0, 3); if (busy.length < 2) return false;
|
|
1497
|
+
S.syncTs = g.ts; const E = EXCH[FR() ? 'fr' : 'en'], seed = hash('s' + g.ts); exLog('sync');
|
|
1498
|
+
if (!(await exSay('main', exPick(E.sync, seed), token))) return false;
|
|
1499
|
+
for (const [n, id] of busy.entries()) { const act = (S.act[id] || '').replace(/^\S+\s/, '') || L('au travail', 'working'); if (!(await exSay(id, exPick(E.syncAns, seed >>> (n + 1), act.charAt(0).toLowerCase() + act.slice(1)), token))) return false; }
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1502
|
+
async function converse(g, token) { // after a line has been said: an exchange when it suits, else the usual one-liners
|
|
1503
|
+
if (S.hurry || S.boost > 1) return; const i = S.events.indexOf(g), seed = hash('x' + g.ts + g.agent), roll = (seed % 1000) / 1000, m = chatMul();
|
|
1504
|
+
if (exReady()) {
|
|
1505
|
+
let ran = false;
|
|
1506
|
+
if (g.kind === 'order' && g.agent !== 'user' && roll < 0.25 * m) ran = await handoffX(g, seed, token);
|
|
1507
|
+
else if (g.kind === 'report' && roll < 0.55 * m) ran = await reportX(g, i, seed, token);
|
|
1508
|
+
if (!ran && token === S.token && S.exQ && S.exQ.length && exReady()) ran = await troubleX(token);
|
|
1509
|
+
if (!ran && token === S.token && exReady()) ran = await syncX(g, token);
|
|
1510
|
+
if (ran) { S.exAt = performance.now(); return; } if (token !== S.token) return;
|
|
1511
|
+
}
|
|
1512
|
+
await react(g, token);
|
|
1513
|
+
}
|
|
1514
|
+
// What is said out loud for an order or a report: a natural lead-in, then the text
|
|
1515
|
+
function talkLine(g) {
|
|
1516
|
+
const E = EXCH[FR() ? 'fr' : 'en'], seed = hash('l' + g.ts + g.agent), t = spoken(g);
|
|
1517
|
+
if (g.kind === 'report') return t.replace(/^(Terminé\.|Done\.)\s*/, E.done[seed % E.done.length] + ' ');
|
|
1518
|
+
return t;
|
|
1519
|
+
}
|
|
1396
1520
|
async function react(ev, token) {
|
|
1397
|
-
if ((!prefs.epi && !prefs.banter) || S.boost > 1 || S.hurry) return; const seed = hash(String(ev.ts) + ev.agent), R = REACT[FR() ? 'fr' : 'en']; let who, line, thanks = false;
|
|
1521
|
+
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;
|
|
1398
1522
|
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]; }
|
|
1399
1523
|
else if (ev.kind === 'report') {
|
|
1400
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
|
|
@@ -1451,7 +1575,7 @@ const SPK = new Set(['say', 'report', 'order', 'user']);
|
|
|
1451
1575
|
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; }
|
|
1452
1576
|
const isSub = (id) => id !== 'main' && id !== 'user';
|
|
1453
1577
|
async function run(i) {
|
|
1454
|
-
const token = ++S.token; S.playing = true; S.idle = false; S.hurry = false; S.boost = 1; S.spokeAt = performance.now(); S.fill = 0; syncUI(); AUD.init();
|
|
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();
|
|
1455
1579
|
if (i === 0 && prefs.epi && !S.live) { await card(L('ÉPISODE 1', 'EPISODE 1'), S.title); if (token !== S.token) return; }
|
|
1456
1580
|
while (token === S.token) {
|
|
1457
1581
|
const lim = S.stopAt || S.events.length;
|
|
@@ -1459,13 +1583,13 @@ async function run(i) {
|
|
|
1459
1583
|
if (S.idle) { S.idle = false; syncUI(); }
|
|
1460
1584
|
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
|
|
1461
1585
|
const rem = S.events.length - i;
|
|
1462
|
-
if (rem > 300 || aheadSpeakable(i, 16) > 16) { // only a really big backlog is skipped; before that, lines are shortened instead
|
|
1586
|
+
if (rem > 300 || aheadSpeakable(i, 16) > 16 || lagMs() > 60000) { // only a really big backlog is skipped; before that, lines are shortened instead
|
|
1463
1587
|
let k = S.events.length, back = 0; while (k > i && back < 8) { k--; if (SPK.has(S.events[k].kind)) back++; }
|
|
1464
1588
|
const to = rem > 300 ? Math.max(k, S.events.length - 14) : k; if (to > i) { i = to; recomputeStatus(i); paintStatus(); }
|
|
1465
1589
|
}
|
|
1466
1590
|
}
|
|
1467
1591
|
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
|
|
1468
|
-
S.pace = !S.live ? 0 : q > 7 ? 70 : q > 3 ? 130 : 260; S.hurry = q > 12; // voices are only skipped when more than a dozen lines are queued
|
|
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
|
|
1469
1593
|
S.boost = S.hurry || (S.live && S.events.length - i > 60) ? 3 : 1;
|
|
1470
1594
|
const ev = S.events[i]; S.idx = i; tally(ev);
|
|
1471
1595
|
if (!S.live && (ev.kind === 'say' || ev.kind === 'report' || ev.kind === 'order' || ev.kind === 'tools')) { await ponder(i, token); if (token !== S.token) return; }
|
|
@@ -1488,7 +1612,7 @@ async function run(i) {
|
|
|
1488
1612
|
prefetchQuips(i); warmTTS(i);
|
|
1489
1613
|
const playOne = (g) => {
|
|
1490
1614
|
applyStatus(g);
|
|
1491
|
-
const label = g.kind === 'order' ? `${L('pour', 'to')} ${nameOf(g.to)}` : g.kind === 'report' ? `${L('rapport à', 'report to')} ${nameOf(g.to)}` : '', line =
|
|
1615
|
+
const label = g.kind === 'order' ? `${L('pour', 'to')} ${nameOf(g.to)}` : g.kind === 'report' ? `${L('rapport à', 'report to')} ${nameOf(g.to)}` : '', line = talkLine(g);
|
|
1492
1616
|
startTalk(g.agent, line, label);
|
|
1493
1617
|
if (g.kind === 'order') { S.status[g.to] = 'working'; AUD.join(); setTimeout(() => { paintStatus(); fly(g.agent, g.to, '📨'); popTile(g.to); }, 30); }
|
|
1494
1618
|
if (g.kind === 'report') { AUD.ding(); setTimeout(() => { fly(g.agent, g.to, '✅'); popTile(g.agent); }, 30); }
|
|
@@ -1496,7 +1620,7 @@ async function run(i) {
|
|
|
1496
1620
|
};
|
|
1497
1621
|
let plays; 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
|
|
1498
1622
|
S.idx = j - 1; paintStatus(); await Promise.all(plays); if (token !== S.token) return;
|
|
1499
|
-
for (const g of group) { await
|
|
1623
|
+
for (const g of group) { await converse(g, token); if (token !== S.token) return; }
|
|
1500
1624
|
i = j;
|
|
1501
1625
|
}
|
|
1502
1626
|
if (token === S.token) {
|
|
@@ -1567,9 +1691,9 @@ setInterval(async () => {
|
|
|
1567
1691
|
if (d.error) return; if (d.total < S.events.length) { S.cur = null; return; } // transcript was rewritten: reload next tick
|
|
1568
1692
|
if (!d.events || !d.events.length) return;
|
|
1569
1693
|
for (const a of d.agents) if (!S.agents[a.id]) { S.agents[a.id] = persona(a); addTile(S.agents[a.id]); }
|
|
1570
|
-
const from = S.events.length; S.events = S.events.concat(d.events); $('#seek').max = S.events.length - 1; trNew(from); renderLog(false); paintStatus();
|
|
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); paintStatus();
|
|
1571
1695
|
} catch { if (!S.offline) { S.offline = true; syncUI(); } } finally { polling = false; }
|
|
1572
|
-
},
|
|
1696
|
+
}, 400);
|
|
1573
1697
|
|
|
1574
1698
|
|
|
1575
1699
|
window.addEventListener('message', (e) => { // the VS Code status bar asks to follow a given session
|
|
@@ -1611,10 +1735,29 @@ async function renderBoardView() {
|
|
|
1611
1735
|
} catch {} finally { boardBusy = false; }
|
|
1612
1736
|
}
|
|
1613
1737
|
$('#boardv').onkeydown = (e) => { if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('bcard')) { e.preventDefault(); e.target.click(); } };
|
|
1614
|
-
|
|
1615
|
-
|
|
1738
|
+
async function openSession(proj, id) {
|
|
1739
|
+
prefs.proj = proj; S.pin = id; savePrefs(); setView('call');
|
|
1616
1740
|
if (S.live || S.demo) { if (S.demo) await setMode('live'); S.cur = null; fillProjects(); await followCurrent(); } else await setMode('live');
|
|
1617
|
-
}
|
|
1741
|
+
}
|
|
1742
|
+
$('#boardv').onclick = (e) => { const c = e.target.closest('.bcard'); if (c) openSession(c.dataset.p, c.dataset.i); };
|
|
1743
|
+
// Cmd/Ctrl+K: jump to any session from any view
|
|
1744
|
+
async function openSwitcher() {
|
|
1745
|
+
let sw = $('#switcher'); if (sw) { closeSwitcher(); return; }
|
|
1746
|
+
sw = document.createElement('div'); sw.id = 'switcher'; sw.innerHTML = `<div class="sbox"><input id="swq" type="search" aria-label="${esc(L('Chercher une session', 'Find a session'))}" placeholder="${esc(L('Chercher une session…', 'Find a session…'))}"><div id="swl" role="listbox"></div></div>`; document.body.appendChild(sw);
|
|
1747
|
+
let rows = []; try { rows = (await (await fetch('/api/board')).json()).sort((a, b) => BORDER.indexOf(a.state) - BORDER.indexOf(b.state) || b.mtime - a.mtime); } catch {}
|
|
1748
|
+
let sel = 0, shown = [];
|
|
1749
|
+
const draw = () => {
|
|
1750
|
+
const q = $('#swq').value.trim().toLowerCase(), now = Date.now(); shown = rows.filter((r) => !q || (r.title + ' ' + niceProj(r.proj)).toLowerCase().includes(q)).slice(0, 12); sel = Math.min(sel, Math.max(0, shown.length - 1));
|
|
1751
|
+
$('#swl').innerHTML = shown.map((r, i) => { const [ic, en, fr] = BSTATE[r.state]; return `<div class="swr${i === sel ? ' on' : ''}" role="option" data-i="${i}"><span class="stc ${r.state}">${ic} ${L(fr, en)}</span><b>${esc(/^[0-9a-f]{8}-[0-9a-f-]{27}$/.test(r.title) ? L('Session sans titre', 'Untitled session') : r.title)}</b><i>${esc(niceProj(r.proj))} · ${agoStr(now - r.mtime)}</i></div>`; }).join('') || `<div class="swr dim">${L('Aucune session récente', 'No recent session')}</div>`;
|
|
1752
|
+
};
|
|
1753
|
+
const pick = (i) => { const r = shown[i]; closeSwitcher(); if (r) openSession(r.proj, r.id); };
|
|
1754
|
+
$('#swq').oninput = () => { sel = 0; draw(); };
|
|
1755
|
+
$('#swq').onkeydown = (e) => { if (e.key === 'ArrowDown') { sel = Math.min(shown.length - 1, sel + 1); draw(); e.preventDefault(); } else if (e.key === 'ArrowUp') { sel = Math.max(0, sel - 1); draw(); e.preventDefault(); } else if (e.key === 'Enter') pick(sel); else if (e.key === 'Escape') closeSwitcher(); };
|
|
1756
|
+
$('#swl').onclick = (e) => { const r = e.target.closest('.swr[data-i]'); if (r) pick(+r.dataset.i); };
|
|
1757
|
+
sw.onclick = (e) => { if (e.target === sw) closeSwitcher(); };
|
|
1758
|
+
draw(); $('#swq').focus();
|
|
1759
|
+
}
|
|
1760
|
+
function closeSwitcher() { const sw = $('#switcher'); if (sw) sw.remove(); }
|
|
1618
1761
|
setInterval(() => { if (!document.hidden) renderBoardView(); }, 2500);
|
|
1619
1762
|
async function setMode(m) {
|
|
1620
1763
|
if (m === 'demo') { // a built-in fictional session: nothing to install, nothing private on screen
|
|
@@ -1632,10 +1775,10 @@ async function setMode(m) {
|
|
|
1632
1775
|
const rows = await (await fetch('/api/sessions')).json();
|
|
1633
1776
|
$('#sessions').innerHTML = rows.map((r) => { const d = new Date(r.mtime).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
1634
1777
|
return `<option value="${esc(r.proj)}|${r.id}" data-title="${esc(r.title)}">${r.agents ? '👥' + r.agents + ' · ' : ''}${esc(r.title.slice(0, 40))} (${d})</option>`; }).join('');
|
|
1635
|
-
$('#empty').classList.remove('on'); const pick = rows.find((r) => r.agents) || rows[0]; if (!pick) { $('#empty').classList.add('on'); return; }
|
|
1778
|
+
$('#empty').classList.remove('on'); const pick = rows.find((r) => r.id === Q.get('pin')) || rows.find((r) => r.agents) || rows[0]; if (!pick) { $('#empty').classList.add('on'); return; }
|
|
1636
1779
|
$('#sessions').value = `${pick.proj}|${pick.id}`; await openSelected();
|
|
1637
1780
|
}
|
|
1638
|
-
async function openSelected() { const [p, i] = $('#sessions').value.split('|'); if (await loadSession(p, i)) { S.locked = false; $('#gate').classList.remove('on'); } }
|
|
1781
|
+
async function openSelected() { const [p, i] = $('#sessions').value.split('|'); if (await loadSession(p, i)) { S.locked = false; $('#gate').classList.remove('on'); const at = (prefs.pos || {})[p + '/' + i]; if (!S.live && at > 3 && at < S.events.length - 3) { seekTo(at); toast(L('Reprise là où vous vous étiez arrêté', 'Resumed where you left off')); } } }
|
|
1639
1782
|
$('#sessions').onchange = openSelected;
|
|
1640
1783
|
|
|
1641
1784
|
// =====================================================================
|
|
@@ -1729,7 +1872,7 @@ function applyLang() {
|
|
|
1729
1872
|
document.documentElement.lang = (LANGS.find((l) => l[0] === prefs.lang) || LANGS[0])[2];
|
|
1730
1873
|
document.querySelectorAll('[data-fr]').forEach((el) => (el.textContent = FR() ? el.dataset.fr : tr(el.dataset.en)));
|
|
1731
1874
|
const sel = $('#langSel'); if (!sel.options.length) sel.innerHTML = LANGS.map(([c, n]) => `<option value="${c}">${n}</option>`).join(''); sel.value = prefs.lang;
|
|
1732
|
-
[['#bPlay', L('Pause / Lecture (espace)', 'Pause / Play (space)')], ['#bLog', 'Transcript'], ['#bMute', L('Son (touche M)', 'Sound (M key)')], ['#bMenu', L('Réglages', 'Settings')], ['#dclose', L('Fermer', 'Close')], ['#officeVol', L('Volume de l’ambiance', 'Ambience volume')], ['#musicVol', L('Volume de la musique', 'Music volume')], ['#liveProj', L('Projet à suivre', 'Project to follow')], ['#reactBar', L('Réactions', 'Reactions')], ['#logQ', L('Rechercher dans la transcription', 'Search the transcript')], ['#roster', L('Agents', 'Agents')]].forEach(([q, t]) => { const el = $(q); if (!el) return; el.title = t; el.setAttribute('aria-label', t); }); $('#logQ').placeholder = L('Rechercher…', 'Search…'); document.querySelectorAll('#reactBar button').forEach((b) => b.setAttribute('aria-label', b.dataset.e));
|
|
1875
|
+
[['#bPlay', L('Pause / Lecture (espace)', 'Pause / Play (space)')], ['#bLog', 'Transcript'], ['#bMute', L('Son (touche M)', 'Sound (M key)')], ['#bMenu', L('Réglages', 'Settings')], ['#dclose', L('Fermer', 'Close')], ['#officeVol', L('Volume de l’ambiance', 'Ambience volume')], ['#musicVol', L('Volume de la musique', 'Music volume')], ['#liveProj', L('Projet à suivre', 'Project to follow')], ['#reactBar', L('Réactions', 'Reactions')], ['#logQ', L('Rechercher dans la transcription', 'Search the transcript')], ['#roster', L('Agents', 'Agents')]].forEach(([q, t]) => { const el = $(q); if (!el) return; el.title = t; el.setAttribute('aria-label', t); }); $('#logQ').placeholder = L('Rechercher…', 'Search…'); $('#menuQ').placeholder = L('Chercher un réglage…', 'Search settings…'); document.querySelectorAll('#reactBar button').forEach((b) => b.setAttribute('aria-label', b.dataset.e));
|
|
1733
1876
|
$('#gT').textContent = L('Rejoindre l’appel', 'Join the call'); $('#gP').textContent = L('Cliquez pour activer le son et suivre votre session Claude Code en direct.', 'Click to enable sound and follow your Claude Code session live.'); $('#gB').textContent = L('▶ Rejoindre', '▶ Join');
|
|
1734
1877
|
$('#eDemo').textContent = L('▶ Voir la démo', '▶ Watch the demo'); $('#eT').textContent = L('En attente d’une session', 'Waiting for a session'); $('#eP').textContent = L('Lancez Claude Code dans ce dossier : l’appel démarrera tout seul.', 'Start Claude Code in this folder — the call will start by itself.');
|
|
1735
1878
|
for (const id in S.agents) { const p = persona(S.agents[id]); Object.assign(S.agents[id], { name: p.name, role: p.role }); const t = $('#t-' + id); if (t) { t.querySelector('.tag span').textContent = p.role; t.querySelector('.tag b').textContent = p.name; if (id === 'main' || id === 'user') t.querySelector('.tag span').textContent = p.role; } }
|
|
@@ -1757,11 +1900,26 @@ $('#langSel').onchange = () => setLang($('#langSel').value, true);
|
|
|
1757
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(); };
|
|
1758
1901
|
$('#charOpen').onclick = () => window.parent.postMessage({ agentStandup: 'openCharacters' }, '*');
|
|
1759
1902
|
$('#modeSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setMode(b.dataset.m); };
|
|
1760
|
-
|
|
1903
|
+
// ⚙ menu: sections that fold (remembered) and a search box
|
|
1904
|
+
function menuInit() {
|
|
1905
|
+
const m = $('#menu'), secs = []; let cur = null;
|
|
1906
|
+
for (const el of [...m.children]) { if (el.tagName === 'H4') { cur = { h: el, items: [] }; secs.push(cur); } else if (cur && !el.classList.contains('keys')) cur.items.push(el); }
|
|
1907
|
+
secs.forEach((sc, i) => { sc.h.classList.add('fold'); sc.h.dataset.i = i; sc.h.onclick = () => { const c = !sc.h.classList.contains('closed'); sc.h.classList.toggle('closed', c); sc.items.forEach((e) => e.classList.toggle('mfold', c)); const set = new Set(prefs.menuClosed || []); c ? set.add(i) : set.delete(i); prefs.menuClosed = [...set]; savePrefs(); };
|
|
1908
|
+
if ((prefs.menuClosed || []).includes(i)) { sc.h.classList.add('closed'); sc.items.forEach((e) => e.classList.add('mfold')); } });
|
|
1909
|
+
$('#menuQ').oninput = () => {
|
|
1910
|
+
const q = $('#menuQ').value.trim().toLowerCase();
|
|
1911
|
+
for (const sc of secs) { let any = false;
|
|
1912
|
+
for (const e of sc.items) { const hit = !q || e.textContent.toLowerCase().includes(q) || [...e.querySelectorAll('option')].some((o) => o.textContent.toLowerCase().includes(q)); e.classList.toggle('mhide', !hit); if (hit && q) any = true; if (q) e.classList.remove('mfold'); else e.classList.toggle('mfold', sc.h.classList.contains('closed')); }
|
|
1913
|
+
sc.h.classList.toggle('mhide', !!q && !any && !sc.h.textContent.toLowerCase().includes(q)); if (q) sc.h.classList.remove('closed'); }
|
|
1914
|
+
if (!q) secs.forEach((sc, i) => sc.h.classList.toggle('closed', (prefs.menuClosed || []).includes(i)));
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
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(); }
|
|
1761
1918
|
if (b.dataset.t === 'music') { $('#musicBox').style.display = prefs.music ? '' : 'none'; if (prefs.music) { AUD.init(); AUD.apply(); } } };
|
|
1762
1919
|
$('#station').innerHTML = STATIONS.map((s) => `<option value="${s.id}">📻 ${s.name}</option>`).join('');
|
|
1763
1920
|
$('#station').value = prefs.station; $('#musicVol').value = prefs.musicVol; $('#musicBox').style.display = prefs.music ? '' : 'none';
|
|
1764
1921
|
$('#station').onchange = (e) => { prefs.station = e.target.value; savePrefs(); AUD.init(); RADIO.sync(); };
|
|
1922
|
+
$('#chatSel').value = ['quiet', 'normal', 'lively'].includes(prefs.chat) ? prefs.chat : 'quiet'; $('#chatSel').onchange = () => { prefs.chat = $('#chatSel').value; savePrefs(); };
|
|
1765
1923
|
$('#vstyle').innerHTML = [['animal', 'Animal Crossing (babillage)', 'Animal Crossing (babble)'], ['sims', 'Simlish (babillage)', 'Simlish (babble)'], ['cartoon', 'Voix cartoon (voix réelle)', 'Cartoon voices (real voice)'], ['natural', 'Voix naturelles', 'Natural voices'], ['kokoro', 'Voix très naturelles (à installer)', 'Very natural voices (installs on demand)']].map(([v, fr, en]) => `<option value="${v}" data-fr="${fr}" data-en="${en}">${L(fr, en)}</option>`).join(''); $('#vstyle').value = prefs.vstyle; $('#vstyle').onchange = () => { prefs.vstyle = $('#vstyle').value; prefs.vstyleChosen = true; savePrefs(); TTS.init().then(kokoroUI); kokoroUI(); };
|
|
1766
1924
|
// ---- ⚙ → very natural voices (Kokoro): install box and status, polled while installing
|
|
1767
1925
|
let kokoroTimer = null;
|
|
@@ -1799,22 +1957,52 @@ $('#gB').onclick = () => {
|
|
|
1799
1957
|
if (synth && !EMBED) { try { synth.speak(new SpeechSynthesisUtterance('')); } catch {} } // unlock browser speech
|
|
1800
1958
|
if (S.live && S.events.length) run(S.idx);
|
|
1801
1959
|
};
|
|
1960
|
+
// J / K (or a click on the “need you” chip) jump to the agents that need you or are stalled, one after the other
|
|
1961
|
+
function jumpAttn(dir = 1) {
|
|
1962
|
+
const a = (S.attn || []).filter((id) => $('#t-' + id));
|
|
1963
|
+
if (!a.length) { toast(L('Personne n’a besoin de vous pour le moment', 'Nobody needs you right now')); return; }
|
|
1964
|
+
S.attnAt = ((S.attnAt == null ? (dir > 0 ? -1 : 0) : S.attnAt) + dir + a.length) % a.length; const id = a[S.attnAt];
|
|
1965
|
+
if (document.body.classList.contains('tree') || document.body.classList.contains('board')) setView('call');
|
|
1966
|
+
S.focusLock = Date.now() + 25000; setFocus(id); const t = $('#t-' + id); if (t) { t.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); popTile(id); }
|
|
1967
|
+
toast(`${nameOf(id)} — ${agentState(id, Date.now()).txt.replace(/^\S+\s/, '')}`);
|
|
1968
|
+
}
|
|
1969
|
+
$('#board').onclick = (e) => { if (e.target.closest('.blocked, .stalled')) jumpAttn(1); };
|
|
1970
|
+
// Replay: arrow keys step through the call, N jumps between highlights, speed presets, and the position of each session is remembered
|
|
1971
|
+
const stepKind = (e) => !['usage', 'results', 'finish', 'compact'].includes(e.kind);
|
|
1972
|
+
function stepEvent(dir) {
|
|
1973
|
+
if (S.live || !S.events.length) return; let i = S.idx + dir; while (i > 0 && i < S.events.length - 1 && !stepKind(S.events[i])) i += dir;
|
|
1974
|
+
seekTo(Math.max(0, Math.min(S.events.length - 1, i)));
|
|
1975
|
+
}
|
|
1976
|
+
function stepHighlight(dir) {
|
|
1977
|
+
if (S.live) return; const hs = computeHighlights().map((h) => h.i).sort((a, b) => a - b); if (!hs.length) { toast(L('Aucun moment clé dans cette session', 'No highlight in this session')); return; }
|
|
1978
|
+
const next = dir > 0 ? hs.find((i) => i > S.idx + 1) : [...hs].reverse().find((i) => i < S.idx - 1); seekTo(next != null ? next : dir > 0 ? hs[0] : hs[hs.length - 1]);
|
|
1979
|
+
}
|
|
1980
|
+
function setSpeed(v) { $('#speed').value = v; document.querySelectorAll('#sps button').forEach((b) => b.classList.toggle('on', +b.dataset.s === +v)); }
|
|
1981
|
+
$('#sps').onclick = (e) => { const b = e.target.closest('button'); if (b) setSpeed(+b.dataset.s); };
|
|
1982
|
+
$('#speed').addEventListener('input', () => document.querySelectorAll('#sps button').forEach((b) => b.classList.toggle('on', +b.dataset.s === +$('#speed').value)));
|
|
1983
|
+
const posKey = () => (S.cur ? S.cur.proj + '/' + S.cur.id : '');
|
|
1984
|
+
setInterval(() => {
|
|
1985
|
+
if (S.live || S.demo || !S.cur || !S.events.length || S.idx < 3) return; const k = posKey(), pos = prefs.pos || (prefs.pos = {});
|
|
1986
|
+
const done = S.idx >= S.events.length - 3; if (done) { if (k in pos) { delete pos[k]; savePrefs(); } return; }
|
|
1987
|
+
if (pos[k] !== S.idx) { delete pos[k]; pos[k] = S.idx; const ks = Object.keys(pos); while (ks.length > 40) delete pos[ks.shift()]; savePrefs(); }
|
|
1988
|
+
}, 3000);
|
|
1802
1989
|
function openKeys() {
|
|
1803
|
-
const rows = [['Space', L('Pause / lecture', 'Pause / play')], ['M', L('Son coupé / activé', 'Mute / unmute')], ['T', L('Vue arbre ↔ appel', 'Tree view ↔ call')], ['R', L('Récapitulatif', 'Recap')], ['L', L('Transcription', 'Transcript')], ['?', L('Cette aide', 'This help')], ['Esc', L('Fermer', 'Close')]];
|
|
1804
|
-
$('#recap .box').innerHTML = `<h1>${L('Raccourcis clavier', 'Keyboard shortcuts')}</h1><table>${rows.map(([k, d]) => `<tr><td class="f"><b>${esc(k)}</b></td><td>${esc(d)}</td></tr>`).join('')}</table><p class="dim" style="margin:14px 0 0;font-size:12px">${L('Les vues Appel, Arbre et Sessions se changent aussi en haut de la fenêtre.', 'You can also switch between Call, Tree and Sessions at the top of the window.')}</p><div style="margin-top:14px"><button class="btn" id="recapx">${L('Fermer', 'Close')}</button></div>`;
|
|
1990
|
+
const rows = [['Space', L('Pause / lecture', 'Pause / play')], ['← / →', L('Replay : événement précédent / suivant', 'Replay: previous / next event')], ['N / Shift+N', L('Replay : moment clé suivant / précédent', 'Replay: next / previous highlight')], ['M', L('Son coupé / activé', 'Mute / unmute')], ['T', L('Vue arbre ↔ appel', 'Tree view ↔ call')], ['R', L('Récapitulatif', 'Recap')], ['L', L('Transcription', 'Transcript')], ['J / K', L('Agent suivant / précédent qui a besoin de vous', 'Next / previous agent that needs you')], ['Cmd / Ctrl + K', L('Aller à une session', 'Go to a session')], ['?', L('Cette aide', 'This help')], ['Esc', L('Fermer', 'Close')]];
|
|
1991
|
+
$('#recap .box').innerHTML = `<h1>${L('Raccourcis clavier', 'Keyboard shortcuts')}</h1><table>${rows.map(([k, d]) => `<tr><td class="f"><b>${esc(k)}</b></td><td>${esc(d)}</td></tr>`).join('')}</table><h3>${L('Les états', 'What the states mean')}</h3><table>${[['⚙', L('au travail', 'working')], ['🔗', L('attend un autre agent', 'waiting on another agent')], ['✋', L('attend peut-être votre feu vert', 'may be waiting for your approval')], ['⚠', L('bloqué : un sous-agent est silencieux depuis longtemps', 'stalled: a sub-agent has been silent for a long time')], ['✓', L('terminé', 'done')]].map(([k, d]) => `<tr><td class="f">${k}</td><td>${esc(d)}</td></tr>`).join('')}</table><p class="dim" style="margin:14px 0 0;font-size:12px">${L('Les vues Appel, Arbre et Sessions se changent aussi en haut de la fenêtre.', 'You can also switch between Call, Tree and Sessions at the top of the window.')}</p><div style="margin-top:14px"><button class="btn" id="recapx">${L('Fermer', 'Close')}</button></div>`;
|
|
1805
1992
|
$('#recapx').onclick = () => $('#recap').classList.remove('on'); $('#recap').classList.add('on');
|
|
1806
1993
|
}
|
|
1807
1994
|
$('#mKeys').onclick = () => { $('#menu').classList.remove('on'); openKeys(); };
|
|
1808
1995
|
addEventListener('keydown', (e) => {
|
|
1809
|
-
if (
|
|
1996
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); openSwitcher(); return; }
|
|
1997
|
+
if (/INPUT|SELECT|TEXTAREA/.test(e.target.tagName) || e.metaKey || e.ctrlKey || e.altKey) return; // browser shortcuts (Cmd+R, Ctrl+N…) stay the browser's
|
|
1810
1998
|
if (e.key === '?') { openKeys(); return; }
|
|
1811
|
-
if (e.key === ' ') { e.preventDefault(); togglePlay(); } else if (e.key.toLowerCase() === 'm') toggleMute(); else if (e.key.toLowerCase() === 't') { const toTree = !document.body.classList.contains('tree'); setView(toTree ? 'tree' : 'call'); if (toTree) toast(L('Vue arbre : T pour revenir, ? pour l’aide', 'Tree view: T to go back, ? for help')); } else if (e.key.toLowerCase() === 'r') openRecap(); else if (e.key.toLowerCase() === 'l') $('#bLog').click(); else if (e.key === 'Escape') { $('#recap').classList.remove('on'); $('#menu').classList.remove('on'); }
|
|
1999
|
+
if (e.key === ' ') { if (e.target.closest && e.target.closest('button, .tile, .rr, .bcard, a, [role=button]')) return; e.preventDefault(); togglePlay(); } else if (e.key.toLowerCase() === 'm') toggleMute(); else if (e.key.toLowerCase() === 't') { const toTree = !document.body.classList.contains('tree'); setView(toTree ? 'tree' : 'call'); if (toTree) toast(L('Vue arbre : T pour revenir, ? pour l’aide', 'Tree view: T to go back, ? for help')); } else if (e.key.toLowerCase() === 'r') openRecap(); else if (e.key.toLowerCase() === 'l') $('#bLog').click(); else if (e.key === 'ArrowRight' && !S.live) { e.preventDefault(); stepEvent(1); } else if (e.key === 'ArrowLeft' && !S.live) { e.preventDefault(); stepEvent(-1); } else if (e.key.toLowerCase() === 'n') stepHighlight(e.shiftKey ? -1 : 1); else if (e.key.toLowerCase() === 'j') jumpAttn(1); else if (e.key.toLowerCase() === 'k') jumpAttn(-1); else if (e.key === 'Escape') { $('#recap').classList.remove('on'); $('#menu').classList.remove('on'); }
|
|
1812
2000
|
});
|
|
1813
2001
|
if (EMBED) $('#mBrowser').style.display = '';
|
|
1814
2002
|
|
|
1815
2003
|
(async function init() {
|
|
1816
2004
|
if (!LANGS.some((l) => l[0] === prefs.lang)) prefs.lang = 'en'; await loadLocale(prefs.lang); await loadChars();
|
|
1817
|
-
document.body.classList.toggle('rost', prefs.roster !== false); document.body.classList.toggle('react', !!prefs.reactions); setView(prefs.view); applyLang(); applyMute(); TTS.init().then(updateHint);
|
|
2005
|
+
document.body.classList.toggle('rost', prefs.roster !== false); document.body.classList.toggle('react', !!prefs.reactions); setView(prefs.view); menuInit(); addEventListener('click', () => { try { if (prefs.notify && !EMBED && 'Notification' in window && Notification.permission === 'default') Notification.requestPermission(); } catch {} }, { once: true }); applyLang(); applyMute(); TTS.init().then(updateHint);
|
|
1818
2006
|
loadHealth().then(() => { // French by default only if translation can actually work; otherwise stay in English without a warning
|
|
1819
2007
|
const h = S.health || {};
|
|
1820
2008
|
if (!prefs.langChosen && FR() && h.claude === null && !h.apiKey) { prefs.lang = 'en'; applyLang(); }
|
|
@@ -1828,7 +2016,7 @@ if (EMBED) $('#mBrowser').style.display = '';
|
|
|
1828
2016
|
return;
|
|
1829
2017
|
}
|
|
1830
2018
|
S.live = Q.get('live') !== '0'; document.querySelectorAll('#modeSeg button').forEach((b) => b.classList.toggle('on', b.dataset.m === (S.live ? 'live' : 'replay')));
|
|
1831
|
-
if (S.live) { S.locked = true; $('#gate').classList.add('on'); S.cur = null; syncUI(); fillProjects(); await followCurrent(); } else { S.live = true; await setMode('replay'); }
|
|
2019
|
+
if (S.live) { S.locked = true; $('#gate').classList.add('on'); S.cur = null; syncUI(); fillProjects(); await followCurrent(); } else { S.live = true; await setMode('replay'); if (Q.get('at') != null && S.events.length) seekTo(Math.max(0, Math.min(S.events.length - 1, +Q.get('at') || 0))); }
|
|
1832
2020
|
syncUI();
|
|
1833
2021
|
})();
|
|
1834
2022
|
</script>
|
package/public/locales/da.json
CHANGED
|
@@ -185,5 +185,25 @@
|
|
|
185
185
|
"You can also switch between Call, Tree and Sessions at the top of the window.": "Du kan også skifte mellem Opkald, Træ og Sessioner øverst i vinduet.",
|
|
186
186
|
"Your agents are done 🎉": "Dine agenter er færdige 🎉",
|
|
187
187
|
"Your Claude Code agents as a live team call": "Dine Claude Code-agenter som et levende teammøde",
|
|
188
|
-
"your turn": "din tur"
|
|
188
|
+
"your turn": "din tur",
|
|
189
|
+
"Display": "Visning",
|
|
190
|
+
"Search settings…": "Søg i indstillinger…",
|
|
191
|
+
"may be waiting for your approval": "venter måske på din godkendelse",
|
|
192
|
+
"Next / previous agent that needs you": "Næste / forrige agent, der har brug for dig",
|
|
193
|
+
"Nobody needs you right now": "Ingen har brug for dig lige nu",
|
|
194
|
+
"stalled: a sub-agent has been silent for a long time": "gået i stå: en underagent har været tavs længe",
|
|
195
|
+
"waiting on another agent": "venter på en anden agent",
|
|
196
|
+
"What the states mean": "Hvad tilstandene betyder",
|
|
197
|
+
"Find a session": "Find en session",
|
|
198
|
+
"Find a session…": "Find en session…",
|
|
199
|
+
"Go to a session": "Gå til en session",
|
|
200
|
+
"No highlight in this session": "Ingen højdepunkter i denne session",
|
|
201
|
+
"Replay: next / previous highlight": "Afspilning: næste / forrige højdepunkt",
|
|
202
|
+
"Replay: previous / next event": "Afspilning: forrige / næste hændelse",
|
|
203
|
+
"Resumed where you left off": "Fortsat hvor du slap",
|
|
204
|
+
"Chatter between agents": "Snak mellem agenter",
|
|
205
|
+
"Lively": "Livlig",
|
|
206
|
+
"Normal": "Normal",
|
|
207
|
+
"nothing to report": "intet at rapportere",
|
|
208
|
+
"Quiet": "Stille"
|
|
189
209
|
}
|
package/public/locales/de.json
CHANGED
|
@@ -186,5 +186,25 @@
|
|
|
186
186
|
"Transcript": "Transkript",
|
|
187
187
|
"Tree view ↔ call": "Baumansicht ↔ Anruf",
|
|
188
188
|
"Tree view: T to go back, ? for help": "Baumansicht: T zum Zurückkehren, ? für Hilfe",
|
|
189
|
-
"You can also switch between Call, Tree and Sessions at the top of the window.": "Du kannst oben im Fenster auch zwischen Anruf, Baum und Sitzungen wechseln."
|
|
189
|
+
"You can also switch between Call, Tree and Sessions at the top of the window.": "Du kannst oben im Fenster auch zwischen Anruf, Baum und Sitzungen wechseln.",
|
|
190
|
+
"Display": "Anzeige",
|
|
191
|
+
"Search settings…": "Einstellung suchen…",
|
|
192
|
+
"may be waiting for your approval": "wartet möglicherweise auf deine Freigabe",
|
|
193
|
+
"Next / previous agent that needs you": "Nächster / vorheriger Agent, der dich braucht",
|
|
194
|
+
"Nobody needs you right now": "Im Moment braucht dich niemand",
|
|
195
|
+
"stalled: a sub-agent has been silent for a long time": "hängt: ein Unteragent ist seit langem still",
|
|
196
|
+
"waiting on another agent": "wartet auf einen anderen Agenten",
|
|
197
|
+
"What the states mean": "Was die Zustände bedeuten",
|
|
198
|
+
"Find a session": "Sitzung suchen",
|
|
199
|
+
"Find a session…": "Sitzung suchen…",
|
|
200
|
+
"Go to a session": "Zu einer Sitzung springen",
|
|
201
|
+
"No highlight in this session": "Keine Höhepunkte in dieser Sitzung",
|
|
202
|
+
"Replay: next / previous highlight": "Wiedergabe: nächster / vorheriger Höhepunkt",
|
|
203
|
+
"Replay: previous / next event": "Wiedergabe: vorheriges / nächstes Ereignis",
|
|
204
|
+
"Resumed where you left off": "Dort fortgesetzt, wo du aufgehört hast",
|
|
205
|
+
"Chatter between agents": "Gespräche zwischen Agenten",
|
|
206
|
+
"Lively": "Lebhaft",
|
|
207
|
+
"Normal": "Normal",
|
|
208
|
+
"nothing to report": "nichts zu melden",
|
|
209
|
+
"Quiet": "Ruhig"
|
|
190
210
|
}
|
package/public/locales/en.json
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"Call": "Call",
|
|
27
27
|
"Cartoon voices (real voice)": "Cartoon voices (real voice)",
|
|
28
28
|
"Characters": "Characters",
|
|
29
|
+
"Chatter between agents": "Chatter between agents",
|
|
29
30
|
"Claude Code is not logged in: run “claude” once in a terminal to sign in.": "Claude Code is not logged in: run “claude” once in a terminal to sign in.",
|
|
30
31
|
"Claude Code not found: install it (npm i -g @anthropic-ai/claude-code) or set its path in the “agentStandup.claudePath” setting.": "Claude Code not found: install it (npm i -g @anthropic-ai/claude-code) or set its path in the “agentStandup.claudePath” setting.",
|
|
31
32
|
"Click to enable sound and follow your Claude Code session live.": "Click to enable sound and follow your Claude Code session live.",
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
"Copy failed": "Copy failed",
|
|
41
42
|
"custom character(s) loaded": "custom character(s) loaded",
|
|
42
43
|
"Demo — a team fixes a slow login": "Demo — a team fixes a slow login",
|
|
44
|
+
"Display": "Display",
|
|
43
45
|
"done": "done",
|
|
44
46
|
"Done": "Done",
|
|
45
47
|
"Done. ": "Done. ",
|
|
@@ -58,7 +60,10 @@
|
|
|
58
60
|
"files changed": "files changed",
|
|
59
61
|
"Files touched": "Files touched",
|
|
60
62
|
"Final report": "Final report",
|
|
63
|
+
"Find a session": "Find a session",
|
|
64
|
+
"Find a session…": "Find a session…",
|
|
61
65
|
"first error": "first error",
|
|
66
|
+
"Go to a session": "Go to a session",
|
|
62
67
|
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.",
|
|
63
68
|
"Hide code & file names in videos": "Hide code & file names in videos",
|
|
64
69
|
"Highlights": "Highlights",
|
|
@@ -77,21 +82,28 @@
|
|
|
77
82
|
"lines": "lines",
|
|
78
83
|
"Listing ": "Listing ",
|
|
79
84
|
"Little lines & animations": "Little lines & animations",
|
|
85
|
+
"Lively": "Lively",
|
|
80
86
|
"Loading the model (downloaded on first use, about 305 MB)…": "Loading the model (downloaded on first use, about 305 MB)…",
|
|
81
87
|
"longest wait ": "longest wait ",
|
|
82
88
|
"Looking for a tool": "Looking for a tool",
|
|
83
89
|
"looping?": "looping?",
|
|
90
|
+
"may be waiting for your approval": "may be waiting for your approval",
|
|
84
91
|
"Music volume": "Music volume",
|
|
85
92
|
"Mute / unmute": "Mute / unmute",
|
|
86
93
|
"Natural voices": "Natural voices",
|
|
87
94
|
"need you": "need you",
|
|
88
95
|
"needs you?": "needs you?",
|
|
89
96
|
"needs you? ": "needs you? ",
|
|
97
|
+
"Next / previous agent that needs you": "Next / previous agent that needs you",
|
|
90
98
|
"No agents were harmed.": "No agents were harmed.",
|
|
91
99
|
"No edits.": "No edits.",
|
|
92
100
|
"No French voice installed (Windows: Settings › Time & language › Speech).": "No French voice installed (Windows: Settings › Time & language › Speech).",
|
|
101
|
+
"No highlight in this session": "No highlight in this session",
|
|
93
102
|
"No recent session": "No recent session",
|
|
94
103
|
"No system voice found: agents will mumble.": "No system voice found: agents will mumble.",
|
|
104
|
+
"Nobody needs you right now": "Nobody needs you right now",
|
|
105
|
+
"Normal": "Normal",
|
|
106
|
+
"nothing to report": "nothing to report",
|
|
95
107
|
"Notify me when agents finish": "Notify me when agents finish",
|
|
96
108
|
"npm not found: install Node.js to enable this option.": "npm not found: install Node.js to enable this option.",
|
|
97
109
|
"Office ambience (keys, coffee…)": "Office ambience (keys, coffee…)",
|
|
@@ -105,6 +117,7 @@
|
|
|
105
117
|
"PAUSED": "PAUSED",
|
|
106
118
|
"Project to follow": "Project to follow",
|
|
107
119
|
"Prompt from": "Prompt from",
|
|
120
|
+
"Quiet": "Quiet",
|
|
108
121
|
"Radio unreachable: check your connection or pick another station.": "Radio unreachable: check your connection or pick another station.",
|
|
109
122
|
"Reactions": "Reactions",
|
|
110
123
|
"Reactions & jokes": "Reactions & jokes",
|
|
@@ -117,14 +130,18 @@
|
|
|
117
130
|
"Reload": "Reload",
|
|
118
131
|
"Remove finished agents (2 min)": "Remove finished agents (2 min)",
|
|
119
132
|
"Repeated failures": "Repeated failures",
|
|
133
|
+
"Replay: next / previous highlight": "Replay: next / previous highlight",
|
|
134
|
+
"Replay: previous / next event": "Replay: previous / next event",
|
|
120
135
|
"report": "report",
|
|
121
136
|
"report to": "report to",
|
|
137
|
+
"Resumed where you left off": "Resumed where you left off",
|
|
122
138
|
"Role": "Role",
|
|
123
139
|
"running": "running",
|
|
124
140
|
"Running ": "Running ",
|
|
125
141
|
"running…": "running…",
|
|
126
142
|
"Said": "Said",
|
|
127
143
|
"Same call repeated": "Same call repeated",
|
|
144
|
+
"Search settings…": "Search settings…",
|
|
128
145
|
"Search the transcript": "Search the transcript",
|
|
129
146
|
"Search…": "Search…",
|
|
130
147
|
"Searching “": "Searching “",
|
|
@@ -140,6 +157,7 @@
|
|
|
140
157
|
"Speed": "Speed",
|
|
141
158
|
"stalled": "stalled",
|
|
142
159
|
"stalled · ": "stalled · ",
|
|
160
|
+
"stalled: a sub-agent has been silent for a long time": "stalled: a sub-agent has been silent for a long time",
|
|
143
161
|
"standing by": "standing by",
|
|
144
162
|
"Start Claude Code in this folder — the call will start by itself.": "Start Claude Code in this folder — the call will start by itself.",
|
|
145
163
|
"sub-agents active": "sub-agents active",
|
|
@@ -173,8 +191,10 @@
|
|
|
173
191
|
"WAITING": "WAITING",
|
|
174
192
|
"Waiting for a session": "Waiting for a session",
|
|
175
193
|
"waiting on ": "waiting on ",
|
|
194
|
+
"waiting on another agent": "waiting on another agent",
|
|
176
195
|
"waiting on sub-agents": "waiting on sub-agents",
|
|
177
196
|
"Web search": "Web search",
|
|
197
|
+
"What the states mean": "What the states mean",
|
|
178
198
|
"working": "working",
|
|
179
199
|
"Writing ": "Writing ",
|
|
180
200
|
"You": "You",
|
package/public/locales/es.json
CHANGED
|
@@ -186,5 +186,25 @@
|
|
|
186
186
|
"Transcript": "Transcripción",
|
|
187
187
|
"Tree view ↔ call": "Vista de árbol ↔ llamada",
|
|
188
188
|
"Tree view: T to go back, ? for help": "Vista de árbol: T para volver, ? para la ayuda",
|
|
189
|
-
"You can also switch between Call, Tree and Sessions at the top of the window.": "También puedes cambiar entre Llamada, Árbol y Sesiones en la parte superior de la ventana."
|
|
189
|
+
"You can also switch between Call, Tree and Sessions at the top of the window.": "También puedes cambiar entre Llamada, Árbol y Sesiones en la parte superior de la ventana.",
|
|
190
|
+
"Display": "Pantalla",
|
|
191
|
+
"Search settings…": "Buscar ajuste…",
|
|
192
|
+
"may be waiting for your approval": "puede estar esperando tu aprobación",
|
|
193
|
+
"Next / previous agent that needs you": "Siguiente / anterior agente que te necesita",
|
|
194
|
+
"Nobody needs you right now": "Nadie te necesita ahora mismo",
|
|
195
|
+
"stalled: a sub-agent has been silent for a long time": "bloqueado: un subagente lleva mucho tiempo en silencio",
|
|
196
|
+
"waiting on another agent": "esperando a otro agente",
|
|
197
|
+
"What the states mean": "Qué significan los estados",
|
|
198
|
+
"Find a session": "Buscar una sesión",
|
|
199
|
+
"Find a session…": "Buscar una sesión…",
|
|
200
|
+
"Go to a session": "Ir a una sesión",
|
|
201
|
+
"No highlight in this session": "No hay momentos clave en esta sesión",
|
|
202
|
+
"Replay: next / previous highlight": "Repetición: siguiente / anterior momento clave",
|
|
203
|
+
"Replay: previous / next event": "Repetición: evento anterior / siguiente",
|
|
204
|
+
"Resumed where you left off": "Reanudado donde lo dejaste",
|
|
205
|
+
"Chatter between agents": "Charla entre agentes",
|
|
206
|
+
"Lively": "Animado",
|
|
207
|
+
"Normal": "Normal",
|
|
208
|
+
"nothing to report": "nada que informar",
|
|
209
|
+
"Quiet": "Tranquilo"
|
|
190
210
|
}
|
package/public/locales/ja.json
CHANGED
|
@@ -186,5 +186,25 @@
|
|
|
186
186
|
"Transcript": "文字起こし",
|
|
187
187
|
"Tree view ↔ call": "ツリー表示 ↔ 通話",
|
|
188
188
|
"Tree view: T to go back, ? for help": "ツリー表示:Tで戻る、?でヘルプ",
|
|
189
|
-
"You can also switch between Call, Tree and Sessions at the top of the window.": "ウィンドウ上部で「通話」「ツリー」「セッション」を切り替えることもできます。"
|
|
189
|
+
"You can also switch between Call, Tree and Sessions at the top of the window.": "ウィンドウ上部で「通話」「ツリー」「セッション」を切り替えることもできます。",
|
|
190
|
+
"Display": "表示",
|
|
191
|
+
"Search settings…": "設定を検索…",
|
|
192
|
+
"may be waiting for your approval": "あなたの承認を待っている可能性があります",
|
|
193
|
+
"Next / previous agent that needs you": "対応が必要な次 / 前のエージェント",
|
|
194
|
+
"Nobody needs you right now": "今はだれも対応を求めていません",
|
|
195
|
+
"stalled: a sub-agent has been silent for a long time": "停滞:サブエージェントが長時間沈黙しています",
|
|
196
|
+
"waiting on another agent": "別のエージェントを待っています",
|
|
197
|
+
"What the states mean": "状態の意味",
|
|
198
|
+
"Find a session": "セッションを検索",
|
|
199
|
+
"Find a session…": "セッションを検索…",
|
|
200
|
+
"Go to a session": "セッションへ移動",
|
|
201
|
+
"No highlight in this session": "このセッションにはハイライトがありません",
|
|
202
|
+
"Replay: next / previous highlight": "リプレイ:次 / 前のハイライト",
|
|
203
|
+
"Replay: previous / next event": "リプレイ:前 / 次のイベント",
|
|
204
|
+
"Resumed where you left off": "前回の続きから再開しました",
|
|
205
|
+
"Chatter between agents": "エージェント同士の会話",
|
|
206
|
+
"Lively": "にぎやか",
|
|
207
|
+
"Normal": "ふつう",
|
|
208
|
+
"nothing to report": "報告事項なし",
|
|
209
|
+
"Quiet": "静か"
|
|
190
210
|
}
|
|
@@ -186,5 +186,25 @@
|
|
|
186
186
|
"Transcript": "Transcrição",
|
|
187
187
|
"Tree view ↔ call": "Visão em árvore ↔ chamada",
|
|
188
188
|
"Tree view: T to go back, ? for help": "Visão em árvore: T para voltar, ? para ajuda",
|
|
189
|
-
"You can also switch between Call, Tree and Sessions at the top of the window.": "Você também pode alternar entre Chamada, Árvore e Sessões no topo da janela."
|
|
189
|
+
"You can also switch between Call, Tree and Sessions at the top of the window.": "Você também pode alternar entre Chamada, Árvore e Sessões no topo da janela.",
|
|
190
|
+
"Display": "Exibição",
|
|
191
|
+
"Search settings…": "Buscar configuração…",
|
|
192
|
+
"may be waiting for your approval": "pode estar esperando sua aprovação",
|
|
193
|
+
"Next / previous agent that needs you": "Próximo / anterior agente que precisa de você",
|
|
194
|
+
"Nobody needs you right now": "Ninguém precisa de você agora",
|
|
195
|
+
"stalled: a sub-agent has been silent for a long time": "travado: um subagente está em silêncio há muito tempo",
|
|
196
|
+
"waiting on another agent": "esperando outro agente",
|
|
197
|
+
"What the states mean": "O que significam os estados",
|
|
198
|
+
"Find a session": "Buscar uma sessão",
|
|
199
|
+
"Find a session…": "Buscar uma sessão…",
|
|
200
|
+
"Go to a session": "Ir para uma sessão",
|
|
201
|
+
"No highlight in this session": "Nenhum destaque nesta sessão",
|
|
202
|
+
"Replay: next / previous highlight": "Replay: próximo / anterior destaque",
|
|
203
|
+
"Replay: previous / next event": "Replay: evento anterior / próximo",
|
|
204
|
+
"Resumed where you left off": "Retomado de onde você parou",
|
|
205
|
+
"Chatter between agents": "Conversa entre agentes",
|
|
206
|
+
"Lively": "Animado",
|
|
207
|
+
"Normal": "Normal",
|
|
208
|
+
"nothing to report": "nada a reportar",
|
|
209
|
+
"Quiet": "Quieto"
|
|
190
210
|
}
|
|
@@ -186,5 +186,25 @@
|
|
|
186
186
|
"Transcript": "文字记录",
|
|
187
187
|
"Tree view ↔ call": "树状视图 ↔ 通话",
|
|
188
188
|
"Tree view: T to go back, ? for help": "树状视图:按 T 返回,按 ? 查看帮助",
|
|
189
|
-
"You can also switch between Call, Tree and Sessions at the top of the window.": "你也可以在窗口顶部切换通话、树状和会话视图。"
|
|
189
|
+
"You can also switch between Call, Tree and Sessions at the top of the window.": "你也可以在窗口顶部切换通话、树状和会话视图。",
|
|
190
|
+
"Display": "显示",
|
|
191
|
+
"Search settings…": "搜索设置…",
|
|
192
|
+
"may be waiting for your approval": "可能在等你批准",
|
|
193
|
+
"Next / previous agent that needs you": "下一个 / 上一个需要你的代理",
|
|
194
|
+
"Nobody needs you right now": "目前没有人需要你",
|
|
195
|
+
"stalled: a sub-agent has been silent for a long time": "卡住:某个子代理已长时间没有动静",
|
|
196
|
+
"waiting on another agent": "正在等待另一个代理",
|
|
197
|
+
"What the states mean": "各状态的含义",
|
|
198
|
+
"Find a session": "查找会话",
|
|
199
|
+
"Find a session…": "查找会话…",
|
|
200
|
+
"Go to a session": "跳转到会话",
|
|
201
|
+
"No highlight in this session": "此会话没有精彩片段",
|
|
202
|
+
"Replay: next / previous highlight": "回放:下一个 / 上一个精彩片段",
|
|
203
|
+
"Replay: previous / next event": "回放:上一个 / 下一个事件",
|
|
204
|
+
"Resumed where you left off": "已从上次停下的位置继续",
|
|
205
|
+
"Chatter between agents": "代理之间的交谈",
|
|
206
|
+
"Lively": "热闹",
|
|
207
|
+
"Normal": "正常",
|
|
208
|
+
"nothing to report": "没什么可报告的",
|
|
209
|
+
"Quiet": "安静"
|
|
190
210
|
}
|
|
@@ -186,5 +186,25 @@
|
|
|
186
186
|
"Transcript": "逐字稿",
|
|
187
187
|
"Tree view ↔ call": "樹狀檢視 ↔ 通話",
|
|
188
188
|
"Tree view: T to go back, ? for help": "樹狀檢視:按 T 返回,按 ? 查看說明",
|
|
189
|
-
"You can also switch between Call, Tree and Sessions at the top of the window.": "你也可以在視窗上方切換通話、樹狀和工作階段檢視。"
|
|
189
|
+
"You can also switch between Call, Tree and Sessions at the top of the window.": "你也可以在視窗上方切換通話、樹狀和工作階段檢視。",
|
|
190
|
+
"Display": "顯示",
|
|
191
|
+
"Search settings…": "搜尋設定…",
|
|
192
|
+
"may be waiting for your approval": "可能在等你核准",
|
|
193
|
+
"Next / previous agent that needs you": "下一個 / 上一個需要你的代理",
|
|
194
|
+
"Nobody needs you right now": "目前沒有人需要你",
|
|
195
|
+
"stalled: a sub-agent has been silent for a long time": "卡住:某個子代理已長時間沒有動靜",
|
|
196
|
+
"waiting on another agent": "正在等待另一個代理",
|
|
197
|
+
"What the states mean": "各狀態的意義",
|
|
198
|
+
"Find a session": "尋找工作階段",
|
|
199
|
+
"Find a session…": "尋找工作階段…",
|
|
200
|
+
"Go to a session": "跳到工作階段",
|
|
201
|
+
"No highlight in this session": "此工作階段沒有重點時刻",
|
|
202
|
+
"Replay: next / previous highlight": "重播:下一個 / 上一個重點時刻",
|
|
203
|
+
"Replay: previous / next event": "重播:上一個 / 下一個事件",
|
|
204
|
+
"Resumed where you left off": "已從上次停下的位置繼續",
|
|
205
|
+
"Chatter between agents": "代理之間的交談",
|
|
206
|
+
"Lively": "熱鬧",
|
|
207
|
+
"Normal": "正常",
|
|
208
|
+
"nothing to report": "沒什麼好回報的",
|
|
209
|
+
"Quiet": "安靜"
|
|
190
210
|
}
|
package/server.js
CHANGED
|
@@ -522,11 +522,12 @@ function start(port = PORT) {
|
|
|
522
522
|
const json = (o, code = 200) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(o)); };
|
|
523
523
|
// Endpoints with side effects (they spawn programs or claim the sound), and the one that serves pictures from a session, refuse requests that a browser marks as coming from another site, e.g. an <img> on a web page.
|
|
524
524
|
const sfs = req.headers['sec-fetch-site'];
|
|
525
|
-
if (sfs && sfs !== 'same-origin' && sfs !== 'none' && ['/api/translate', '/api/quips', '/api/warm', '/api/audio', '/api/tts', '/api/prefs', '/api/voices', '/api/kokoro', '/api/kokoro/install', '/api/media'].includes(url.pathname)) { res.writeHead(403); return res.end('forbidden'); }
|
|
525
|
+
if (sfs && sfs !== 'same-origin' && sfs !== 'none' && (req.method !== 'GET' || ['/api/translate', '/api/quips', '/api/warm', '/api/audio', '/api/tts', '/api/prefs', '/api/voices', '/api/kokoro', '/api/kokoro/install', '/api/media'].includes(url.pathname))) { res.writeHead(403); return res.end('forbidden'); }
|
|
526
526
|
const readBody = (cb) => { // request bodies are small JSON: stop reading beyond 256 KB and give up after 10 s
|
|
527
|
-
let
|
|
528
|
-
|
|
529
|
-
req.on('
|
|
527
|
+
const chunks = []; let size = 0, over = false; req.setTimeout(10000, () => req.destroy());
|
|
528
|
+
if (+req.headers['content-length'] > 262144) { res.writeHead(413); res.end('too large'); return req.destroy(); }
|
|
529
|
+
req.on('data', (d) => { if (over) return; size += d.length; if (size > 262144) { over = true; res.writeHead(413); res.end('too large'); req.destroy(); } else chunks.push(d); }); // bytes, not characters
|
|
530
|
+
req.on('end', () => { if (!over) cb(Buffer.concat(chunks).toString('utf8')); });
|
|
530
531
|
};
|
|
531
532
|
const textList = (x) => (Array.isArray(x) ? x : []).slice(0, 200).map((t) => String(t).slice(0, 2000));
|
|
532
533
|
try {
|