@double_geste/agent-standup 1.24.2 → 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 +226 -30
- package/public/locales/da.json +209 -0
- package/public/locales/de.json +31 -1
- package/public/locales/en.json +30 -0
- package/public/locales/es.json +31 -1
- package/public/locales/ja.json +31 -1
- package/public/locales/pt-BR.json +31 -1
- package/public/locales/zh-CN.json +31 -1
- package/public/locales/zh-TW.json +31 -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>
|
|
@@ -261,6 +282,7 @@
|
|
|
261
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>
|
|
262
283
|
<h4 data-fr="Outils" data-en="Tools">Outils</h4>
|
|
263
284
|
<button class="mi" id="mCoffee" style="display:none"><span id="mCoffeeT"></span></button>
|
|
285
|
+
<button class="mi" id="mKeys"><span data-fr="⌨ Raccourcis clavier (?)" data-en="⌨ Keyboard shortcuts (?)">⌨ Raccourcis clavier (?)</span></button>
|
|
264
286
|
<button class="mi" id="mRecap"><span data-fr="📋 Récapitulatif" data-en="📋 Recap">📋 Récapitulatif</span></button>
|
|
265
287
|
<button class="mi" id="mExport"><span data-fr="🎞 Exporter la vidéo" data-en="🎞 Export video">🎞 Exporter la vidéo</span></button>
|
|
266
288
|
<button class="mi" id="mClip"><span data-fr="🎬 Clip à partager (25 s)" data-en="🎬 Clip to share (25 s)">🎬 Clip à partager (25 s)</span></button>
|
|
@@ -286,7 +308,8 @@
|
|
|
286
308
|
</main>
|
|
287
309
|
<footer>
|
|
288
310
|
<input id="seek" type="range" min="0" max="0" value="0"><span class="dim" id="pos">0 / 0</span>
|
|
289
|
-
<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>
|
|
290
313
|
</footer>
|
|
291
314
|
<div id="card"></div><div id="recap"><div class="box"></div></div>
|
|
292
315
|
|
|
@@ -310,11 +333,12 @@ let prefs = { lang: 'en', voice: true, roster: true, banter: true, reactions: fa
|
|
|
310
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); }
|
|
311
334
|
if (prefs.vstyle === 'babble') prefs.vstyle = 'animal', prefs.vstyleChosen = true;
|
|
312
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
|
|
313
337
|
let prefsTimer = null;
|
|
314
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); };
|
|
315
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.
|
|
316
340
|
// A missing string falls back to English, so a half-translated language still works. See public/locales/README.md to add one.
|
|
317
|
-
const LANGS = [['en', 'English', 'en'], ['fr', 'Français', 'fr'], ['zh-TW', '繁體中文', 'zh-Hant-TW'], ['zh-CN', '简体中文', 'zh-Hans-CN'], ['es', 'Español', 'es'], ['de', 'Deutsch', 'de'], ['pt-BR', 'Português (Brasil)', 'pt-BR'], ['ja', '日本語', 'ja']];
|
|
341
|
+
const LANGS = [['en', 'English', 'en'], ['fr', 'Français', 'fr'], ['zh-TW', '繁體中文', 'zh-Hant-TW'], ['zh-CN', '简体中文', 'zh-Hans-CN'], ['es', 'Español', 'es'], ['de', 'Deutsch', 'de'], ['da', 'Dansk', 'da'], ['pt-BR', 'Português (Brasil)', 'pt-BR'], ['ja', '日本語', 'ja']];
|
|
318
342
|
let LOC = {};
|
|
319
343
|
const FR = () => prefs.lang === 'fr';
|
|
320
344
|
const tr = (en) => (prefs.lang === 'en' || prefs.lang === 'fr' ? en : LOC[en] || en);
|
|
@@ -467,11 +491,15 @@ function avatarSVG(p, o, uid = '') { // uid keeps SVG ids unique per instance:
|
|
|
467
491
|
// =====================================================================
|
|
468
492
|
// state
|
|
469
493
|
// =====================================================================
|
|
470
|
-
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,
|
|
471
495
|
speakers: new Set(), say: {}, lastTool: {}, level: {}, detail: null, collapsed: new Set(), title: '', idle: false, locked: true, logTo: 0, trOk: true,
|
|
472
496
|
clock: 0, doneAt: {}, gone: new Set(), leaving: new Set(), demo: false, hold: false };
|
|
473
497
|
const VANISH_MS = 120000; // a finished agent leaves the call after 2 minutes (playback time)
|
|
474
|
-
|
|
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);
|
|
475
503
|
const nameOf = (id) => (S.agents[id] || {}).name || id;
|
|
476
504
|
|
|
477
505
|
// ---------- text prep ----------
|
|
@@ -487,7 +515,7 @@ const speakable = (t) => t.replace(/(?:[\w.-]+\/)+([\w.-]+)/g, (m, f) => f.repla
|
|
|
487
515
|
|
|
488
516
|
// ---------- translation (local claude CLI) ----------
|
|
489
517
|
const trMem = new Map(), trPending = new Map();
|
|
490
|
-
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)));
|
|
491
519
|
function ensureTr(texts) {
|
|
492
520
|
texts = [...new Set(texts)].filter((t) => t && !trMem.has(t));
|
|
493
521
|
const waits = texts.filter((t) => trPending.has(t)).map((t) => trPending.get(t)), fresh = texts.filter((t) => !trPending.has(t));
|
|
@@ -675,7 +703,7 @@ const TTS = {
|
|
|
675
703
|
for (const l of ['fr', 'en']) this.pool[l] = rankVoices(d.voices.filter((v) => v.locale.startsWith(l + '_')).map((v) => v.name)); } catch {}
|
|
676
704
|
},
|
|
677
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; },
|
|
678
|
-
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; },
|
|
679
707
|
buf(text, voice, rate) {
|
|
680
708
|
const key = this.engine + '|' + voice + '|' + rate + '|' + text;
|
|
681
709
|
if (!this.cache.has(key)) {
|
|
@@ -843,7 +871,7 @@ const layout = () => {
|
|
|
843
871
|
if (nar) { k = cols; fr = cols > 1 ? 2 : 1; } // side panel: small tiles, a banner-sized spotlight
|
|
844
872
|
else if (best < 0) { k = Math.min(2, cols); fr = Math.min(2, maxRows); }
|
|
845
873
|
st.style.setProperty('--fcols', k); st.style.setProperty('--fr', fr); st.classList.toggle('one', cols === 1);
|
|
846
|
-
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();
|
|
847
875
|
};
|
|
848
876
|
new ResizeObserver(layout).observe($('#center'));
|
|
849
877
|
|
|
@@ -921,7 +949,7 @@ function tally(ev) { // numbers that pile up over a session: token usage, erro
|
|
|
921
949
|
if (ev.kind === 'compact') { S.compacts[ev.agent] = (S.compacts[ev.agent] || 0) + 1; delete S.ctxWarned[ev.agent]; }
|
|
922
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); }
|
|
923
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); }
|
|
924
|
-
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); });
|
|
925
953
|
else if (ev.kind === 'tools' || ev.kind === 'say') S.lastAct[ev.agent] = ev.ts;
|
|
926
954
|
}
|
|
927
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)}`;
|
|
@@ -961,10 +989,10 @@ function agentState(id, now) {
|
|
|
961
989
|
}
|
|
962
990
|
const STATE_CLASSES = ['working', 'waiting', 'blocked', 'stalled', 'done'];
|
|
963
991
|
function paintStates() {
|
|
964
|
-
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 = [];
|
|
965
993
|
for (const p of Object.values(S.agents)) {
|
|
966
994
|
if (p.id === 'user') continue; const t = $('#t-' + p.id); if (!t) continue;
|
|
967
|
-
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); }
|
|
968
996
|
STATE_CLASSES.forEach((c) => t.classList.toggle(c, st.k === c));
|
|
969
997
|
const el = t.querySelector('.status'); if (el.textContent !== st.txt) el.textContent = st.txt; el.className = 'status ' + st.k;
|
|
970
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"
|
|
@@ -977,7 +1005,7 @@ function paintStates() {
|
|
|
977
1005
|
}
|
|
978
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')]]
|
|
979
1007
|
.filter(([k]) => n[k]).map(([k, i, l]) => `<span class="${k}" title="${n[k]} ${l}">${i} ${n[k]}<em> ${l}</em></span>`).join('');
|
|
980
|
-
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;
|
|
981
1009
|
}
|
|
982
1010
|
const RORDER = { blocked: 0, stalled: 1, working: 2, waiting: 3, idle: 4, done: 5 };
|
|
983
1011
|
function paintRoster(ros) { // a fixed list beside the call: who is doing what, urgent first, readable even with dozens of agents
|
|
@@ -1392,8 +1420,105 @@ function ackLine(ev) {
|
|
|
1392
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];
|
|
1393
1421
|
if (line === S.lastAck) line = pool[(seed + 1) % pool.length]; S.lastAck = line; return line;
|
|
1394
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
|
+
}
|
|
1395
1520
|
async function react(ev, token) {
|
|
1396
|
-
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;
|
|
1397
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]; }
|
|
1398
1523
|
else if (ev.kind === 'report') {
|
|
1399
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
|
|
@@ -1450,7 +1575,7 @@ const SPK = new Set(['say', 'report', 'order', 'user']);
|
|
|
1450
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; }
|
|
1451
1576
|
const isSub = (id) => id !== 'main' && id !== 'user';
|
|
1452
1577
|
async function run(i) {
|
|
1453
|
-
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();
|
|
1454
1579
|
if (i === 0 && prefs.epi && !S.live) { await card(L('ÉPISODE 1', 'EPISODE 1'), S.title); if (token !== S.token) return; }
|
|
1455
1580
|
while (token === S.token) {
|
|
1456
1581
|
const lim = S.stopAt || S.events.length;
|
|
@@ -1458,13 +1583,13 @@ async function run(i) {
|
|
|
1458
1583
|
if (S.idle) { S.idle = false; syncUI(); }
|
|
1459
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
|
|
1460
1585
|
const rem = S.events.length - i;
|
|
1461
|
-
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
|
|
1462
1587
|
let k = S.events.length, back = 0; while (k > i && back < 8) { k--; if (SPK.has(S.events[k].kind)) back++; }
|
|
1463
1588
|
const to = rem > 300 ? Math.max(k, S.events.length - 14) : k; if (to > i) { i = to; recomputeStatus(i); paintStatus(); }
|
|
1464
1589
|
}
|
|
1465
1590
|
}
|
|
1466
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
|
|
1467
|
-
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
|
|
1468
1593
|
S.boost = S.hurry || (S.live && S.events.length - i > 60) ? 3 : 1;
|
|
1469
1594
|
const ev = S.events[i]; S.idx = i; tally(ev);
|
|
1470
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; }
|
|
@@ -1487,7 +1612,7 @@ async function run(i) {
|
|
|
1487
1612
|
prefetchQuips(i); warmTTS(i);
|
|
1488
1613
|
const playOne = (g) => {
|
|
1489
1614
|
applyStatus(g);
|
|
1490
|
-
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);
|
|
1491
1616
|
startTalk(g.agent, line, label);
|
|
1492
1617
|
if (g.kind === 'order') { S.status[g.to] = 'working'; AUD.join(); setTimeout(() => { paintStatus(); fly(g.agent, g.to, '📨'); popTile(g.to); }, 30); }
|
|
1493
1618
|
if (g.kind === 'report') { AUD.ding(); setTimeout(() => { fly(g.agent, g.to, '✅'); popTile(g.agent); }, 30); }
|
|
@@ -1495,7 +1620,7 @@ async function run(i) {
|
|
|
1495
1620
|
};
|
|
1496
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
|
|
1497
1622
|
S.idx = j - 1; paintStatus(); await Promise.all(plays); if (token !== S.token) return;
|
|
1498
|
-
for (const g of group) { await
|
|
1623
|
+
for (const g of group) { await converse(g, token); if (token !== S.token) return; }
|
|
1499
1624
|
i = j;
|
|
1500
1625
|
}
|
|
1501
1626
|
if (token === S.token) {
|
|
@@ -1566,9 +1691,9 @@ setInterval(async () => {
|
|
|
1566
1691
|
if (d.error) return; if (d.total < S.events.length) { S.cur = null; return; } // transcript was rewritten: reload next tick
|
|
1567
1692
|
if (!d.events || !d.events.length) return;
|
|
1568
1693
|
for (const a of d.agents) if (!S.agents[a.id]) { S.agents[a.id] = persona(a); addTile(S.agents[a.id]); }
|
|
1569
|
-
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();
|
|
1570
1695
|
} catch { if (!S.offline) { S.offline = true; syncUI(); } } finally { polling = false; }
|
|
1571
|
-
},
|
|
1696
|
+
}, 400);
|
|
1572
1697
|
|
|
1573
1698
|
|
|
1574
1699
|
window.addEventListener('message', (e) => { // the VS Code status bar asks to follow a given session
|
|
@@ -1610,10 +1735,29 @@ async function renderBoardView() {
|
|
|
1610
1735
|
} catch {} finally { boardBusy = false; }
|
|
1611
1736
|
}
|
|
1612
1737
|
$('#boardv').onkeydown = (e) => { if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('bcard')) { e.preventDefault(); e.target.click(); } };
|
|
1613
|
-
|
|
1614
|
-
|
|
1738
|
+
async function openSession(proj, id) {
|
|
1739
|
+
prefs.proj = proj; S.pin = id; savePrefs(); setView('call');
|
|
1615
1740
|
if (S.live || S.demo) { if (S.demo) await setMode('live'); S.cur = null; fillProjects(); await followCurrent(); } else await setMode('live');
|
|
1616
|
-
}
|
|
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(); }
|
|
1617
1761
|
setInterval(() => { if (!document.hidden) renderBoardView(); }, 2500);
|
|
1618
1762
|
async function setMode(m) {
|
|
1619
1763
|
if (m === 'demo') { // a built-in fictional session: nothing to install, nothing private on screen
|
|
@@ -1631,10 +1775,10 @@ async function setMode(m) {
|
|
|
1631
1775
|
const rows = await (await fetch('/api/sessions')).json();
|
|
1632
1776
|
$('#sessions').innerHTML = rows.map((r) => { const d = new Date(r.mtime).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
1633
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('');
|
|
1634
|
-
$('#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; }
|
|
1635
1779
|
$('#sessions').value = `${pick.proj}|${pick.id}`; await openSelected();
|
|
1636
1780
|
}
|
|
1637
|
-
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')); } } }
|
|
1638
1782
|
$('#sessions').onchange = openSelected;
|
|
1639
1783
|
|
|
1640
1784
|
// =====================================================================
|
|
@@ -1728,7 +1872,7 @@ function applyLang() {
|
|
|
1728
1872
|
document.documentElement.lang = (LANGS.find((l) => l[0] === prefs.lang) || LANGS[0])[2];
|
|
1729
1873
|
document.querySelectorAll('[data-fr]').forEach((el) => (el.textContent = FR() ? el.dataset.fr : tr(el.dataset.en)));
|
|
1730
1874
|
const sel = $('#langSel'); if (!sel.options.length) sel.innerHTML = LANGS.map(([c, n]) => `<option value="${c}">${n}</option>`).join(''); sel.value = prefs.lang;
|
|
1731
|
-
[['#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));
|
|
1732
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');
|
|
1733
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.');
|
|
1734
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; } }
|
|
@@ -1756,11 +1900,26 @@ $('#langSel').onchange = () => setLang($('#langSel').value, true);
|
|
|
1756
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(); };
|
|
1757
1901
|
$('#charOpen').onclick = () => window.parent.postMessage({ agentStandup: 'openCharacters' }, '*');
|
|
1758
1902
|
$('#modeSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setMode(b.dataset.m); };
|
|
1759
|
-
|
|
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(); }
|
|
1760
1918
|
if (b.dataset.t === 'music') { $('#musicBox').style.display = prefs.music ? '' : 'none'; if (prefs.music) { AUD.init(); AUD.apply(); } } };
|
|
1761
1919
|
$('#station').innerHTML = STATIONS.map((s) => `<option value="${s.id}">📻 ${s.name}</option>`).join('');
|
|
1762
1920
|
$('#station').value = prefs.station; $('#musicVol').value = prefs.musicVol; $('#musicBox').style.display = prefs.music ? '' : 'none';
|
|
1763
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(); };
|
|
1764
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(); };
|
|
1765
1924
|
// ---- ⚙ → very natural voices (Kokoro): install box and status, polled while installing
|
|
1766
1925
|
let kokoroTimer = null;
|
|
@@ -1798,15 +1957,52 @@ $('#gB').onclick = () => {
|
|
|
1798
1957
|
if (synth && !EMBED) { try { synth.speak(new SpeechSynthesisUtterance('')); } catch {} } // unlock browser speech
|
|
1799
1958
|
if (S.live && S.events.length) run(S.idx);
|
|
1800
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);
|
|
1989
|
+
function openKeys() {
|
|
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>`;
|
|
1992
|
+
$('#recapx').onclick = () => $('#recap').classList.remove('on'); $('#recap').classList.add('on');
|
|
1993
|
+
}
|
|
1994
|
+
$('#mKeys').onclick = () => { $('#menu').classList.remove('on'); openKeys(); };
|
|
1801
1995
|
addEventListener('keydown', (e) => {
|
|
1802
|
-
if (
|
|
1803
|
-
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
|
|
1998
|
+
if (e.key === '?') { openKeys(); return; }
|
|
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'); }
|
|
1804
2000
|
});
|
|
1805
2001
|
if (EMBED) $('#mBrowser').style.display = '';
|
|
1806
2002
|
|
|
1807
2003
|
(async function init() {
|
|
1808
2004
|
if (!LANGS.some((l) => l[0] === prefs.lang)) prefs.lang = 'en'; await loadLocale(prefs.lang); await loadChars();
|
|
1809
|
-
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);
|
|
1810
2006
|
loadHealth().then(() => { // French by default only if translation can actually work; otherwise stay in English without a warning
|
|
1811
2007
|
const h = S.health || {};
|
|
1812
2008
|
if (!prefs.langChosen && FR() && h.claude === null && !h.apiKey) { prefs.lang = 'en'; applyLang(); }
|
|
@@ -1820,7 +2016,7 @@ if (EMBED) $('#mBrowser').style.display = '';
|
|
|
1820
2016
|
return;
|
|
1821
2017
|
}
|
|
1822
2018
|
S.live = Q.get('live') !== '0'; document.querySelectorAll('#modeSeg button').forEach((b) => b.classList.toggle('on', b.dataset.m === (S.live ? 'live' : 'replay')));
|
|
1823
|
-
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))); }
|
|
1824
2020
|
syncUI();
|
|
1825
2021
|
})();
|
|
1826
2022
|
</script>
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"language": "da",
|
|
4
|
+
"reviewed": false
|
|
5
|
+
},
|
|
6
|
+
" full — compaction soon": " fuld — komprimering nærmer sig",
|
|
7
|
+
"”": "”",
|
|
8
|
+
"↗ Open in browser": "↗ Åbn i browser",
|
|
9
|
+
"⌨ Keyboard shortcuts (?)": "⌨ Tastaturgenveje (?)",
|
|
10
|
+
"▶ Join": "▶ Deltag",
|
|
11
|
+
"▶ Watch the demo": "▶ Se demoen",
|
|
12
|
+
"☕ Buy the (replaced) dev a coffee": "☕ Køb en kop kaffe til den (erstattede) udvikler",
|
|
13
|
+
"☕ This dev was replaced by his agents — buy him a coffee": "☕ Denne udvikler blev erstattet af sine agenter — køb en kop kaffe til ham",
|
|
14
|
+
"🎞 Export video": "🎞 Eksportér video",
|
|
15
|
+
"🎬 Clip to share (25 s)": "🎬 Klip til deling (25 s)",
|
|
16
|
+
"📋 Recap": "📋 Resumé",
|
|
17
|
+
"Agent voices": "Agentstemmer",
|
|
18
|
+
"Agents": "Agenter",
|
|
19
|
+
"Agents list (left)": "Agentliste (til venstre)",
|
|
20
|
+
"All projects (most recent)": "Alle projekter (nyeste)",
|
|
21
|
+
"Ambience volume": "Lydstyrke for stemning",
|
|
22
|
+
"An agent may be waiting for you ✋": "En agent venter måske på dig ✋",
|
|
23
|
+
"Animal Crossing (babble)": "Animal Crossing (pludren)",
|
|
24
|
+
"Asking a question": "Stiller et spørgsmål",
|
|
25
|
+
"brief": "brief",
|
|
26
|
+
"Busiest agents": "Travleste agenter",
|
|
27
|
+
"Buy a coffee": "Køb en kop kaffe",
|
|
28
|
+
"cache read": "cache-læsning",
|
|
29
|
+
"cache write": "cache-skrivning",
|
|
30
|
+
"Call": "Opkald",
|
|
31
|
+
"Cartoon voices (real voice)": "Tegnefilmsstemmer (rigtig stemme)",
|
|
32
|
+
"Characters": "Figurer",
|
|
33
|
+
"Claude Code is not logged in: run “claude” once in a terminal to sign in.": "Claude Code er ikke logget ind: kør “claude” én gang i en terminal for at logge ind.",
|
|
34
|
+
"Claude Code not found: install it (npm i -g @anthropic-ai/claude-code) or set its path in the “agentStandup.claudePath” setting.": "Claude Code blev ikke fundet: installér det (npm i -g @anthropic-ai/claude-code) eller angiv stien i indstillingen “agentStandup.claudePath”.",
|
|
35
|
+
"Click to enable sound and follow your Claude Code session live.": "Klik for at aktivere lyd og følge din Claude Code-session live.",
|
|
36
|
+
"Close": "Luk",
|
|
37
|
+
"commands": "kommandoer",
|
|
38
|
+
"compaction": "komprimering",
|
|
39
|
+
"Context": "Kontekst",
|
|
40
|
+
"context ": "kontekst ",
|
|
41
|
+
"context compacted 🗜": "kontekst komprimeret 🗜",
|
|
42
|
+
"Copied": "Kopieret",
|
|
43
|
+
"Copy as Markdown": "Kopiér som Markdown",
|
|
44
|
+
"Copy failed": "Kopiering mislykkedes",
|
|
45
|
+
"custom character(s) loaded": "egne figurer indlæst",
|
|
46
|
+
"Demo — a team fixes a slow login": "Demo — et team retter et langsomt login",
|
|
47
|
+
"done": "færdig",
|
|
48
|
+
"Done": "Færdig",
|
|
49
|
+
"Done. ": "Færdig. ",
|
|
50
|
+
"Downloading, you can carry on: system voices are used meanwhile.": "Henter, du kan fortsætte: imens bruges systemstemmerne.",
|
|
51
|
+
"Drop your pixel art (PNG) in this folder to replace the characters:": "Læg dit pixelart (PNG) i denne mappe for at erstatte figurerne:",
|
|
52
|
+
"duration": "varighed",
|
|
53
|
+
"Editing ": "Redigerer ",
|
|
54
|
+
"Editor": "Editor",
|
|
55
|
+
"EPISODE 1": "AFSNIT 1",
|
|
56
|
+
"Episode recap": "Afsnitsresumé",
|
|
57
|
+
"errors": "fejl",
|
|
58
|
+
"failing in a loop?": "fejler i ring?",
|
|
59
|
+
"Fetching ": "Henter ",
|
|
60
|
+
"files": "filer",
|
|
61
|
+
"Files": "Filer",
|
|
62
|
+
"files changed": "filer ændret",
|
|
63
|
+
"Files touched": "Berørte filer",
|
|
64
|
+
"Final report": "Slutrapport",
|
|
65
|
+
"first error": "første fejl",
|
|
66
|
+
"Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "Lokale stemmer af god kvalitet, mest engelske (kun én fransk stemme). Gratis, intet forlader din computer.",
|
|
67
|
+
"Hide code & file names in videos": "Skjul kode og filnavne i videoer",
|
|
68
|
+
"Highlights": "Højdepunkter",
|
|
69
|
+
"Human": "Menneske",
|
|
70
|
+
"idle": "inaktiv",
|
|
71
|
+
"input": "input",
|
|
72
|
+
"Installing…": "Installerer…",
|
|
73
|
+
"is sharing their screen": "deler sin skærm",
|
|
74
|
+
"Join the call": "Deltag i opkaldet",
|
|
75
|
+
"Keyboard shortcuts": "Tastaturgenveje",
|
|
76
|
+
"Language": "Sprog",
|
|
77
|
+
"Lead": "Leder",
|
|
78
|
+
"Lead context": "Lederens kontekst",
|
|
79
|
+
"lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = lederen · you.png = dig · øvrige = underagenterne · navn-talk.png = åben mund",
|
|
80
|
+
"left the call": "forlod opkaldet",
|
|
81
|
+
"lines": "linjer",
|
|
82
|
+
"Listing ": "Viser ",
|
|
83
|
+
"Little lines & animations": "Små replikker og animationer",
|
|
84
|
+
"Loading the model (downloaded on first use, about 305 MB)…": "Indlæser modellen (hentes ved første brug, ca. 305 MB)…",
|
|
85
|
+
"longest wait ": "længste ventetid ",
|
|
86
|
+
"Looking for a tool": "Leder efter et værktøj",
|
|
87
|
+
"looping?": "går i ring?",
|
|
88
|
+
"Music volume": "Lydstyrke for musik",
|
|
89
|
+
"Mute / unmute": "Slå lyd fra / til",
|
|
90
|
+
"Natural voices": "Naturlige stemmer",
|
|
91
|
+
"need you": "har brug for dig",
|
|
92
|
+
"needs you?": "har brug for dig?",
|
|
93
|
+
"needs you? ": "har brug for dig? ",
|
|
94
|
+
"No agents were harmed.": "Ingen agenter kom til skade.",
|
|
95
|
+
"No edits.": "Ingen ændringer.",
|
|
96
|
+
"No French voice installed (Windows: Settings › Time & language › Speech).": "Ingen fransk stemme installeret (Windows: Indstillinger › Tid og sprog › Tale).",
|
|
97
|
+
"No recent session": "Ingen nylig session",
|
|
98
|
+
"No system voice found: agents will mumble.": "Ingen systemstemme fundet: agenterne vil mumle.",
|
|
99
|
+
"Notify me when agents finish": "Giv mig besked, når agenterne er færdige",
|
|
100
|
+
"npm not found: install Node.js to enable this option.": "npm blev ikke fundet: installér Node.js for at aktivere denne mulighed.",
|
|
101
|
+
"Office ambience (keys, coffee…)": "Kontorstemning (tastatur, kaffe…)",
|
|
102
|
+
"Office music (FIP radio)": "Kontormusik (FIP Radio)",
|
|
103
|
+
"OFFLINE": "OFFLINE",
|
|
104
|
+
"Open folder": "Åbn mappe",
|
|
105
|
+
"Opening file…": "Åbner fil…",
|
|
106
|
+
"output": "output",
|
|
107
|
+
"Pause / play": "Pause / afspil",
|
|
108
|
+
"Pause / Play (space)": "Pause / afspil (mellemrum)",
|
|
109
|
+
"PAUSED": "PAUSET",
|
|
110
|
+
"Project to follow": "Projekt der skal følges",
|
|
111
|
+
"Prompt from": "Prompt fra",
|
|
112
|
+
"Radio unreachable: check your connection or pick another station.": "Radioen kan ikke nås: tjek din forbindelse eller vælg en anden station.",
|
|
113
|
+
"Reactions": "Reaktioner",
|
|
114
|
+
"Reactions & jokes": "Reaktioner og jokes",
|
|
115
|
+
"Reactions bar (👍 👏 ☕)": "Reaktionslinje (👍 👏 ☕)",
|
|
116
|
+
"read-only": "skrivebeskyttet",
|
|
117
|
+
"Reading ": "Læser ",
|
|
118
|
+
"READY": "KLAR",
|
|
119
|
+
"Recap": "Resumé",
|
|
120
|
+
"Recent activity": "Seneste aktivitet",
|
|
121
|
+
"Reload": "Genindlæs",
|
|
122
|
+
"Remove finished agents (2 min)": "Fjern færdige agenter (2 min)",
|
|
123
|
+
"Repeated failures": "Gentagne fejl",
|
|
124
|
+
"report": "rapport",
|
|
125
|
+
"report to": "rapport til",
|
|
126
|
+
"Role": "Rolle",
|
|
127
|
+
"running": "kører",
|
|
128
|
+
"Running ": "Kører ",
|
|
129
|
+
"running…": "kører…",
|
|
130
|
+
"Said": "Sagde",
|
|
131
|
+
"Same call repeated": "Samme kald gentaget",
|
|
132
|
+
"Search the transcript": "Søg i transskriptionen",
|
|
133
|
+
"Search…": "Søg…",
|
|
134
|
+
"Searching “": "Søger efter “",
|
|
135
|
+
"session": "session",
|
|
136
|
+
"Sessions": "Sessioner",
|
|
137
|
+
"Settings": "Indstillinger",
|
|
138
|
+
"Simlish (babble)": "Simlish (pludren)",
|
|
139
|
+
"Sound": "Lyd",
|
|
140
|
+
"Sound (M key)": "Lyd (tasten M)",
|
|
141
|
+
"Sound is playing in another Agent Standup window. Click 🔈 to take it over here.": "Lyden afspilles i et andet Agent Standup-vindue. Klik på 🔈 for at overtage den her.",
|
|
142
|
+
"Sound is playing in another window — click to take it over here": "Lyden afspilles i et andet vindue — klik for at overtage den her",
|
|
143
|
+
"Source": "Kilde",
|
|
144
|
+
"Speed": "Hastighed",
|
|
145
|
+
"stalled": "gået i stå",
|
|
146
|
+
"stalled · ": "gået i stå · ",
|
|
147
|
+
"standing by": "på standby",
|
|
148
|
+
"Start Claude Code in this folder — the call will start by itself.": "Start Claude Code i denne mappe — opkaldet starter af sig selv.",
|
|
149
|
+
"sub-agents active": "underagenter aktive",
|
|
150
|
+
"task": "opgave",
|
|
151
|
+
"Tasks": "Opgaver",
|
|
152
|
+
"THE END": "SLUT",
|
|
153
|
+
"The engine stopped: system voices are used instead.": "Motoren stoppede: systemstemmerne bruges i stedet.",
|
|
154
|
+
"This dev was replaced by his own agents. Spare a coffee for his career change? ☕": "Denne udvikler blev erstattet af sine egne agenter. Har du en kop kaffe til hans karriereskift? ☕",
|
|
155
|
+
"This help": "Denne hjælp",
|
|
156
|
+
"Time": "Tid",
|
|
157
|
+
"to": "til",
|
|
158
|
+
"tokens": "tokens",
|
|
159
|
+
"Tokens": "Tokens",
|
|
160
|
+
"Tokens recorded by Claude Code (counted once per message, not a bill)": "Tokens registreret af Claude Code (talt én gang pr. besked, ikke en regning)",
|
|
161
|
+
"tool calls": "værktøjskald",
|
|
162
|
+
"tool errors": "værktøjsfejl",
|
|
163
|
+
"tools": "værktøjer",
|
|
164
|
+
"Tools": "Værktøjer",
|
|
165
|
+
"Transcript": "Transskription",
|
|
166
|
+
"Translation unavailable right now: check that “claude” works in a terminal.": "Oversættelse er ikke tilgængelig lige nu: tjek at “claude” virker i en terminal.",
|
|
167
|
+
"Tree": "Træ",
|
|
168
|
+
"Tree view ↔ call": "Trævisning ↔ opkald",
|
|
169
|
+
"Tree view: T to go back, ? for help": "Trævisning: T for at gå tilbage, ? for hjælp",
|
|
170
|
+
"Untitled session": "Unavngivet session",
|
|
171
|
+
"Updating todo list": "Opdaterer opgavelisten",
|
|
172
|
+
"Using skill ": "Bruger skill ",
|
|
173
|
+
"Very natural voices (installs on demand)": "Meget naturlige stemmer (installeres efter behov)",
|
|
174
|
+
"Voices ready.": "Stemmer klar.",
|
|
175
|
+
"VS Code was started from inside a Claude Code session: restart VS Code normally (Dock, Spotlight) to enable translation.": "VS Code blev startet inde fra en Claude Code-session: genstart VS Code på normal vis (Dock, Spotlight) for at aktivere oversættelse.",
|
|
176
|
+
"waiting": "venter",
|
|
177
|
+
"WAITING": "VENTER",
|
|
178
|
+
"Waiting for a session": "Venter på en session",
|
|
179
|
+
"waiting on ": "venter på ",
|
|
180
|
+
"waiting on sub-agents": "venter på underagenter",
|
|
181
|
+
"Web search": "Websøgning",
|
|
182
|
+
"working": "arbejder",
|
|
183
|
+
"Writing ": "Skriver ",
|
|
184
|
+
"You": "Dig",
|
|
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
|
+
"Your agents are done 🎉": "Dine agenter er færdige 🎉",
|
|
187
|
+
"Your Claude Code agents as a live team call": "Dine Claude Code-agenter som et levende teammøde",
|
|
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"
|
|
209
|
+
}
|
package/public/locales/de.json
CHANGED
|
@@ -176,5 +176,35 @@
|
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "npm nicht gefunden: Installiere Node.js, um diese Option zu nutzen.",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "Die Engine wurde beendet: stattdessen kommen die Systemstimmen zum Einsatz.",
|
|
178
178
|
"Very natural voices (installs on demand)": "Sehr natürliche Stimmen (werden bei Bedarf installiert)",
|
|
179
|
-
"Voices ready.": "Stimmen bereit."
|
|
179
|
+
"Voices ready.": "Stimmen bereit.",
|
|
180
|
+
"⌨ Keyboard shortcuts (?)": "⌨ Tastenkürzel (?)",
|
|
181
|
+
"Keyboard shortcuts": "Tastenkürzel",
|
|
182
|
+
"Mute / unmute": "Ton aus / an",
|
|
183
|
+
"Pause / play": "Pause / Wiedergabe",
|
|
184
|
+
"Recap": "Zusammenfassung",
|
|
185
|
+
"This help": "Diese Hilfe",
|
|
186
|
+
"Transcript": "Transkript",
|
|
187
|
+
"Tree view ↔ call": "Baumansicht ↔ Anruf",
|
|
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.",
|
|
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"
|
|
180
210
|
}
|
package/public/locales/en.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
" full — compaction soon": " full — compaction soon",
|
|
3
3
|
"”": "”",
|
|
4
4
|
"↗ Open in browser": "↗ Open in browser",
|
|
5
|
+
"⌨ Keyboard shortcuts (?)": "⌨ Keyboard shortcuts (?)",
|
|
5
6
|
"▶ Join": "▶ Join",
|
|
6
7
|
"▶ Watch the demo": "▶ Watch the demo",
|
|
7
8
|
"☕ Buy the (replaced) dev a coffee": "☕ Buy the (replaced) dev a coffee",
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"Call": "Call",
|
|
26
27
|
"Cartoon voices (real voice)": "Cartoon voices (real voice)",
|
|
27
28
|
"Characters": "Characters",
|
|
29
|
+
"Chatter between agents": "Chatter between agents",
|
|
28
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.",
|
|
29
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.",
|
|
30
32
|
"Click to enable sound and follow your Claude Code session live.": "Click to enable sound and follow your Claude Code session live.",
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
"Copy failed": "Copy failed",
|
|
40
42
|
"custom character(s) loaded": "custom character(s) loaded",
|
|
41
43
|
"Demo — a team fixes a slow login": "Demo — a team fixes a slow login",
|
|
44
|
+
"Display": "Display",
|
|
42
45
|
"done": "done",
|
|
43
46
|
"Done": "Done",
|
|
44
47
|
"Done. ": "Done. ",
|
|
@@ -57,7 +60,10 @@
|
|
|
57
60
|
"files changed": "files changed",
|
|
58
61
|
"Files touched": "Files touched",
|
|
59
62
|
"Final report": "Final report",
|
|
63
|
+
"Find a session": "Find a session",
|
|
64
|
+
"Find a session…": "Find a session…",
|
|
60
65
|
"first error": "first error",
|
|
66
|
+
"Go to a session": "Go to a session",
|
|
61
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.",
|
|
62
68
|
"Hide code & file names in videos": "Hide code & file names in videos",
|
|
63
69
|
"Highlights": "Highlights",
|
|
@@ -67,6 +73,7 @@
|
|
|
67
73
|
"Installing…": "Installing…",
|
|
68
74
|
"is sharing their screen": "is sharing their screen",
|
|
69
75
|
"Join the call": "Join the call",
|
|
76
|
+
"Keyboard shortcuts": "Keyboard shortcuts",
|
|
70
77
|
"Language": "Language",
|
|
71
78
|
"Lead": "Lead",
|
|
72
79
|
"Lead context": "Lead context",
|
|
@@ -75,20 +82,28 @@
|
|
|
75
82
|
"lines": "lines",
|
|
76
83
|
"Listing ": "Listing ",
|
|
77
84
|
"Little lines & animations": "Little lines & animations",
|
|
85
|
+
"Lively": "Lively",
|
|
78
86
|
"Loading the model (downloaded on first use, about 305 MB)…": "Loading the model (downloaded on first use, about 305 MB)…",
|
|
79
87
|
"longest wait ": "longest wait ",
|
|
80
88
|
"Looking for a tool": "Looking for a tool",
|
|
81
89
|
"looping?": "looping?",
|
|
90
|
+
"may be waiting for your approval": "may be waiting for your approval",
|
|
82
91
|
"Music volume": "Music volume",
|
|
92
|
+
"Mute / unmute": "Mute / unmute",
|
|
83
93
|
"Natural voices": "Natural voices",
|
|
84
94
|
"need you": "need you",
|
|
85
95
|
"needs you?": "needs you?",
|
|
86
96
|
"needs you? ": "needs you? ",
|
|
97
|
+
"Next / previous agent that needs you": "Next / previous agent that needs you",
|
|
87
98
|
"No agents were harmed.": "No agents were harmed.",
|
|
88
99
|
"No edits.": "No edits.",
|
|
89
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",
|
|
90
102
|
"No recent session": "No recent session",
|
|
91
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",
|
|
92
107
|
"Notify me when agents finish": "Notify me when agents finish",
|
|
93
108
|
"npm not found: install Node.js to enable this option.": "npm not found: install Node.js to enable this option.",
|
|
94
109
|
"Office ambience (keys, coffee…)": "Office ambience (keys, coffee…)",
|
|
@@ -97,10 +112,12 @@
|
|
|
97
112
|
"Open folder": "Open folder",
|
|
98
113
|
"Opening file…": "Opening file…",
|
|
99
114
|
"output": "output",
|
|
115
|
+
"Pause / play": "Pause / play",
|
|
100
116
|
"Pause / Play (space)": "Pause / Play (space)",
|
|
101
117
|
"PAUSED": "PAUSED",
|
|
102
118
|
"Project to follow": "Project to follow",
|
|
103
119
|
"Prompt from": "Prompt from",
|
|
120
|
+
"Quiet": "Quiet",
|
|
104
121
|
"Radio unreachable: check your connection or pick another station.": "Radio unreachable: check your connection or pick another station.",
|
|
105
122
|
"Reactions": "Reactions",
|
|
106
123
|
"Reactions & jokes": "Reactions & jokes",
|
|
@@ -108,18 +125,23 @@
|
|
|
108
125
|
"read-only": "read-only",
|
|
109
126
|
"Reading ": "Reading ",
|
|
110
127
|
"READY": "READY",
|
|
128
|
+
"Recap": "Recap",
|
|
111
129
|
"Recent activity": "Recent activity",
|
|
112
130
|
"Reload": "Reload",
|
|
113
131
|
"Remove finished agents (2 min)": "Remove finished agents (2 min)",
|
|
114
132
|
"Repeated failures": "Repeated failures",
|
|
133
|
+
"Replay: next / previous highlight": "Replay: next / previous highlight",
|
|
134
|
+
"Replay: previous / next event": "Replay: previous / next event",
|
|
115
135
|
"report": "report",
|
|
116
136
|
"report to": "report to",
|
|
137
|
+
"Resumed where you left off": "Resumed where you left off",
|
|
117
138
|
"Role": "Role",
|
|
118
139
|
"running": "running",
|
|
119
140
|
"Running ": "Running ",
|
|
120
141
|
"running…": "running…",
|
|
121
142
|
"Said": "Said",
|
|
122
143
|
"Same call repeated": "Same call repeated",
|
|
144
|
+
"Search settings…": "Search settings…",
|
|
123
145
|
"Search the transcript": "Search the transcript",
|
|
124
146
|
"Search…": "Search…",
|
|
125
147
|
"Searching “": "Searching “",
|
|
@@ -135,6 +157,7 @@
|
|
|
135
157
|
"Speed": "Speed",
|
|
136
158
|
"stalled": "stalled",
|
|
137
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",
|
|
138
161
|
"standing by": "standing by",
|
|
139
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.",
|
|
140
163
|
"sub-agents active": "sub-agents active",
|
|
@@ -143,6 +166,7 @@
|
|
|
143
166
|
"THE END": "THE END",
|
|
144
167
|
"The engine stopped: system voices are used instead.": "The engine stopped: system voices are used instead.",
|
|
145
168
|
"This dev was replaced by his own agents. Spare a coffee for his career change? ☕": "This dev was replaced by his own agents. Spare a coffee for his career change? ☕",
|
|
169
|
+
"This help": "This help",
|
|
146
170
|
"Time": "Time",
|
|
147
171
|
"to": "to",
|
|
148
172
|
"tokens": "tokens",
|
|
@@ -152,8 +176,11 @@
|
|
|
152
176
|
"tool errors": "tool errors",
|
|
153
177
|
"tools": "tools",
|
|
154
178
|
"Tools": "Tools",
|
|
179
|
+
"Transcript": "Transcript",
|
|
155
180
|
"Translation unavailable right now: check that “claude” works in a terminal.": "Translation unavailable right now: check that “claude” works in a terminal.",
|
|
156
181
|
"Tree": "Tree",
|
|
182
|
+
"Tree view ↔ call": "Tree view ↔ call",
|
|
183
|
+
"Tree view: T to go back, ? for help": "Tree view: T to go back, ? for help",
|
|
157
184
|
"Untitled session": "Untitled session",
|
|
158
185
|
"Updating todo list": "Updating todo list",
|
|
159
186
|
"Using skill ": "Using skill ",
|
|
@@ -164,11 +191,14 @@
|
|
|
164
191
|
"WAITING": "WAITING",
|
|
165
192
|
"Waiting for a session": "Waiting for a session",
|
|
166
193
|
"waiting on ": "waiting on ",
|
|
194
|
+
"waiting on another agent": "waiting on another agent",
|
|
167
195
|
"waiting on sub-agents": "waiting on sub-agents",
|
|
168
196
|
"Web search": "Web search",
|
|
197
|
+
"What the states mean": "What the states mean",
|
|
169
198
|
"working": "working",
|
|
170
199
|
"Writing ": "Writing ",
|
|
171
200
|
"You": "You",
|
|
201
|
+
"You can also switch between Call, Tree and Sessions at the top of the window.": "You can also switch between Call, Tree and Sessions at the top of the window.",
|
|
172
202
|
"Your agents are done 🎉": "Your agents are done 🎉",
|
|
173
203
|
"Your Claude Code agents as a live team call": "Your Claude Code agents as a live team call",
|
|
174
204
|
"your turn": "your turn"
|
package/public/locales/es.json
CHANGED
|
@@ -176,5 +176,35 @@
|
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "No se encontró npm: instala Node.js para activar esta opción.",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "El motor se detuvo: se usan las voces del sistema.",
|
|
178
178
|
"Very natural voices (installs on demand)": "Voces muy naturales (se instalan bajo demanda)",
|
|
179
|
-
"Voices ready.": "Voces listas."
|
|
179
|
+
"Voices ready.": "Voces listas.",
|
|
180
|
+
"⌨ Keyboard shortcuts (?)": "⌨ Atajos de teclado (?)",
|
|
181
|
+
"Keyboard shortcuts": "Atajos de teclado",
|
|
182
|
+
"Mute / unmute": "Silenciar / activar sonido",
|
|
183
|
+
"Pause / play": "Pausa / reproducir",
|
|
184
|
+
"Recap": "Resumen",
|
|
185
|
+
"This help": "Esta ayuda",
|
|
186
|
+
"Transcript": "Transcripción",
|
|
187
|
+
"Tree view ↔ call": "Vista de árbol ↔ llamada",
|
|
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.",
|
|
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"
|
|
180
210
|
}
|
package/public/locales/ja.json
CHANGED
|
@@ -176,5 +176,35 @@
|
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "npm が見つかりません。このオプションを使うには Node.js をインストールしてください。",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "エンジンが停止しました。代わりにシステム音声を使います。",
|
|
178
178
|
"Very natural voices (installs on demand)": "とても自然な音声(必要時にインストール)",
|
|
179
|
-
"Voices ready.": "音声の準備ができました。"
|
|
179
|
+
"Voices ready.": "音声の準備ができました。",
|
|
180
|
+
"⌨ Keyboard shortcuts (?)": "⌨ キーボードショートカット (?)",
|
|
181
|
+
"Keyboard shortcuts": "キーボードショートカット",
|
|
182
|
+
"Mute / unmute": "ミュート / 解除",
|
|
183
|
+
"Pause / play": "一時停止 / 再生",
|
|
184
|
+
"Recap": "要約",
|
|
185
|
+
"This help": "このヘルプ",
|
|
186
|
+
"Transcript": "文字起こし",
|
|
187
|
+
"Tree view ↔ call": "ツリー表示 ↔ 通話",
|
|
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.": "ウィンドウ上部で「通話」「ツリー」「セッション」を切り替えることもできます。",
|
|
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": "静か"
|
|
180
210
|
}
|
|
@@ -176,5 +176,35 @@
|
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "npm não encontrado: instale o Node.js para ativar esta opção.",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "O mecanismo parou: as vozes do sistema são usadas no lugar.",
|
|
178
178
|
"Very natural voices (installs on demand)": "Vozes muito naturais (instaladas sob demanda)",
|
|
179
|
-
"Voices ready.": "Vozes prontas."
|
|
179
|
+
"Voices ready.": "Vozes prontas.",
|
|
180
|
+
"⌨ Keyboard shortcuts (?)": "⌨ Atalhos de teclado (?)",
|
|
181
|
+
"Keyboard shortcuts": "Atalhos de teclado",
|
|
182
|
+
"Mute / unmute": "Silenciar / ativar som",
|
|
183
|
+
"Pause / play": "Pausar / reproduzir",
|
|
184
|
+
"Recap": "Resumo",
|
|
185
|
+
"This help": "Esta ajuda",
|
|
186
|
+
"Transcript": "Transcrição",
|
|
187
|
+
"Tree view ↔ call": "Visão em árvore ↔ chamada",
|
|
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.",
|
|
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"
|
|
180
210
|
}
|
|
@@ -176,5 +176,35 @@
|
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "未找到 npm:请安装 Node.js 以启用此选项。",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "引擎已停止:改用系统语音。",
|
|
178
178
|
"Very natural voices (installs on demand)": "超自然语音(按需安装)",
|
|
179
|
-
"Voices ready.": "语音已就绪。"
|
|
179
|
+
"Voices ready.": "语音已就绪。",
|
|
180
|
+
"⌨ Keyboard shortcuts (?)": "⌨ 键盘快捷键 (?)",
|
|
181
|
+
"Keyboard shortcuts": "键盘快捷键",
|
|
182
|
+
"Mute / unmute": "静音 / 取消静音",
|
|
183
|
+
"Pause / play": "暂停 / 播放",
|
|
184
|
+
"Recap": "回顾",
|
|
185
|
+
"This help": "此帮助",
|
|
186
|
+
"Transcript": "文字记录",
|
|
187
|
+
"Tree view ↔ call": "树状视图 ↔ 通话",
|
|
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.": "你也可以在窗口顶部切换通话、树状和会话视图。",
|
|
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": "安静"
|
|
180
210
|
}
|
|
@@ -176,5 +176,35 @@
|
|
|
176
176
|
"npm not found: install Node.js to enable this option.": "找不到 npm:請安裝 Node.js 才能啟用此選項。",
|
|
177
177
|
"The engine stopped: system voices are used instead.": "引擎已停止:改用系統語音。",
|
|
178
178
|
"Very natural voices (installs on demand)": "超自然語音(需要時才安裝)",
|
|
179
|
-
"Voices ready.": "語音已就緒。"
|
|
179
|
+
"Voices ready.": "語音已就緒。",
|
|
180
|
+
"⌨ Keyboard shortcuts (?)": "⌨ 鍵盤快速鍵 (?)",
|
|
181
|
+
"Keyboard shortcuts": "鍵盤快速鍵",
|
|
182
|
+
"Mute / unmute": "靜音 / 取消靜音",
|
|
183
|
+
"Pause / play": "暫停 / 播放",
|
|
184
|
+
"Recap": "回顧",
|
|
185
|
+
"This help": "此說明",
|
|
186
|
+
"Transcript": "逐字稿",
|
|
187
|
+
"Tree view ↔ call": "樹狀檢視 ↔ 通話",
|
|
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.": "你也可以在視窗上方切換通話、樹狀和工作階段檢視。",
|
|
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": "安靜"
|
|
180
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 {
|