@double_geste/agent-standup 1.28.0 → 1.30.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent Standup (command line)
2
2
 
3
- Your Claude Code and Codex sessions as a live team call, without any editor. Free, MIT, and it never calls Claude: it only reads the session files Claude Code and Codex already write.
3
+ Your Claude Code, Codex and Mistral Vibe sessions as a live team call, without any editor. Free, MIT, and it never calls Claude: it only reads the session files Claude Code, Codex and Mistral Vibe already write.
4
4
 
5
5
  ```bash
6
6
  npx @double_geste/agent-standup # then open http://localhost:4747 (add --open to open it for you)
@@ -13,6 +13,6 @@ Install once for status bars (faster than npx): `npm i -g @double_geste/agent-st
13
13
  - **tmux**: `set -g status-right '#(agent-standup status --here)'`
14
14
  - **Anything else**: `agent-standup status --json`
15
15
 
16
- Full documentation, the VS Code extension, the desktop app and the demo: https://atoll92.github.io/agent-standup/ · https://github.com/Atoll92/agent-standup.git
16
+ Full documentation, the VS Code extension, the desktop app and the demo: https://agent-standup.com · https://github.com/Atoll92/agent-standup.git
17
17
 
18
- Unofficial: not affiliated with Anthropic or OpenAI.
18
+ Unofficial: not affiliated with Anthropic, OpenAI or Mistral AI.
@@ -9,12 +9,12 @@ const pkg = require('../package.json');
9
9
  const argv = process.argv.slice(2), flags = {}, rest = [];
10
10
  for (let i = 0; i < argv.length; i++) {
11
11
  const a = argv[i], val = () => argv[++i];
12
- if (a === '--port') flags.port = val(); else if (a === '--projects') flags.projects = val(); else if (a === '--codex') flags.codex = val(); else if (a === '--characters') flags.characters = val(); else if (a === '--config-dir') flags.config = val();
12
+ if (a === '--port') flags.port = val(); else if (a === '--projects') flags.projects = val(); else if (a === '--codex') flags.codex = val(); else if (a === '--vibe') flags.vibe = val(); else if (a === '--characters') flags.characters = val(); else if (a === '--config-dir') flags.config = val();
13
13
  else if (a === '--open') flags.open = true; else if (a === '--here') flags.here = true; else if (a === '--json') flags.json = true; else if (a === '--titles') flags.titles = true;
14
14
  else if (a === '-h' || a === '--help') flags.help = true; else if (a === '-v' || a === '--version') flags.version = true; else rest.push(a);
15
15
  }
16
16
  const cmd = rest[0] || 'serve';
17
- const HELP = `Agent Standup ${pkg.version} — your Claude Code and Codex agents as a live video call
17
+ const HELP = `Agent Standup ${pkg.version} — your Claude Code, Codex and Mistral Vibe agents as a live video call
18
18
 
19
19
  Usage
20
20
  agent-standup [serve] [--port 4747] [--open] start the local server (http://localhost:4747)
@@ -26,6 +26,7 @@ Options
26
26
  --open open the page in your browser
27
27
  --projects DIR where the session files are (default ~/.claude/projects, or $CLAUDE_CONFIG_DIR/projects)
28
28
  --codex DIR where Codex keeps its files (default ~/.codex, or $CODEX_HOME); "off" to leave Codex sessions out
29
+ --vibe DIR where Mistral Vibe keeps its files (default ~/.vibe, or $VIBE_HOME); "off" to leave Vibe sessions out
29
30
  --characters DIR folder with your own pixel-art characters (default ~/.agent-standup/characters)
30
31
  --here status: only sessions of the current folder (matched on its real path, as Claude Code names it)
31
32
  --json status: machine-readable output (add --titles to include session titles)
@@ -34,6 +35,7 @@ if (flags.help || cmd === 'help') { process.stdout.write(HELP); process.exit(0);
34
35
  if (flags.version || cmd === 'version') { console.log(pkg.version); process.exit(0); }
35
36
  if (flags.projects) process.env.AGENT_STANDUP_PROJECTS = path.resolve(flags.projects);
36
37
  if (flags.codex !== undefined) process.env.AGENT_STANDUP_CODEX = /^(0|off|no|false)?$/i.test(String(flags.codex)) ? 'off' : path.resolve(String(flags.codex));
38
+ if (flags.vibe !== undefined) process.env.AGENT_STANDUP_VIBE = /^(0|off|no|false)?$/i.test(String(flags.vibe)) ? 'off' : path.resolve(String(flags.vibe));
37
39
  if (flags.config) process.env.CLAUDE_CONFIG_DIR = path.resolve(flags.config);
38
40
  if (flags.characters) process.env.AGENT_STANDUP_CHARACTERS = path.resolve(flags.characters);
39
41
  if (flags.port !== undefined) { // a real port number or nothing: no silent fallback to another port
package/codex.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // events a Claude Code record gives (say, user, tool, results, usage, compact), so everything after that is shared: the timeline, the live feed, the page.
5
5
  // All Codex sessions are listed under one made-up project, PROJ. No Claude Code project folder can have that name: theirs are paths with every other character turned into "-".
6
6
  const fs = require('fs'), os = require('os'), path = require('path');
7
- const PROJ = '_codex';
7
+ const PROJ = '_codex', SOURCE = 'codex';
8
8
  // Where Codex keeps its files: AGENT_STANDUP_CODEX (a folder; "0" or "off" turns Codex sessions off), else $CODEX_HOME, else ~/.codex.
9
9
  // When AGENT_STANDUP_PROJECTS points the app at one folder of Claude Code sessions (--projects, the tests), Codex is only read if it is asked for too.
10
10
  function home() {
@@ -206,4 +206,4 @@ function parser({ toolLabel, clip, resultMax = {} }) {
206
206
  if (p.type === 'web_search_call' && !cx.items && id) call(id, 'WebSearch', { query: (p.action && p.action.query) || '' });
207
207
  };
208
208
  }
209
- module.exports = { PROJ, home, dirKey, sessions, row, fileOf, inProject, projectLabel, title, boardRow, parser };
209
+ module.exports = { PROJ, SOURCE, home, dirKey, sessions, row, fileOf, inProject, projectLabel, title, boardRow, parser };
package/kokoro.js CHANGED
@@ -7,8 +7,8 @@ const DIR = process.env.AGENT_STANDUP_KOKORO_DIR || (WIN ? path.join(process.env
7
7
  const PKG = 'kokoro-js@1.2.1', DTYPE = 'q4', SIZE_MB = 730, MAX_RESTARTS = 3;
8
8
  const WORKER = process.env.AGENT_STANDUP_KOKORO_WORKER || path.join(__dirname, 'kokoro-worker.js'), LOG = path.join(DIR, 'install.log');
9
9
 
10
- // In the order agents get them (the lead takes the first), so successive agents sound distinct. French has a single voice in Kokoro.
11
- const VOICES = [['af_heart', 'Heart', 'en_US'], ['am_fenrir', 'Fenrir', 'en_US'], ['bf_emma', 'Emma', 'en_GB'], ['am_michael', 'Michael', 'en_US'], ['af_bella', 'Bella', 'en_US'], ['bm_george', 'George', 'en_GB'], ['af_sarah', 'Sarah', 'en_US'], ['am_puck', 'Puck', 'en_US'], ['ff_siwis', 'Siwis', 'fr_FR']].map(([name, label, locale]) => ({ name, label, locale }));
10
+ // In the order agents get them (the lead takes the first), so successive agents sound distinct. English only: kokoro-js has no French voice (it refuses ff_siwis), so French lines are read by the system voices.
11
+ const VOICES = [['af_heart', 'Heart', 'en_US'], ['am_fenrir', 'Fenrir', 'en_US'], ['bf_emma', 'Emma', 'en_GB'], ['am_michael', 'Michael', 'en_US'], ['af_bella', 'Bella', 'en_US'], ['bm_george', 'George', 'en_GB'], ['af_sarah', 'Sarah', 'en_US'], ['am_puck', 'Puck', 'en_US']].map(([name, label, locale]) => ({ name, label, locale }));
12
12
  const voices = () => VOICES, hasVoice = (n) => VOICES.some((v) => v.name === n);
13
13
  const speedFor = (wpm) => Math.max(0.5, Math.min(2, Math.round((wpm / 178) * 20) / 20)); // 178 words a minute is about Kokoro's natural pace
14
14
  const exists = (f) => { try { fs.statSync(f); return true; } catch { return false; } };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@double_geste/agent-standup",
3
- "version": "1.28.0",
4
- "description": "Your Claude Code and Codex 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.",
3
+ "version": "1.30.0",
4
+ "description": "Your Claude Code, Codex and Mistral Vibe 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"
7
7
  },
@@ -10,6 +10,7 @@
10
10
  "bin",
11
11
  "server.js",
12
12
  "codex.js",
13
+ "vibe.js",
13
14
  "kokoro.js",
14
15
  "kokoro-worker.js",
15
16
  "demo.js",
@@ -27,7 +28,7 @@
27
28
  "type": "git",
28
29
  "url": "git+https://github.com/Atoll92/agent-standup.git"
29
30
  },
30
- "homepage": "https://atoll92.github.io/agent-standup/",
31
+ "homepage": "https://agent-standup.com",
31
32
  "bugs": {
32
33
  "url": "https://github.com/Atoll92/agent-standup/issues"
33
34
  },
package/public/index.html CHANGED
@@ -18,7 +18,7 @@
18
18
  /* ---------- header ---------- */
19
19
  header { height:48px; flex:none; display:flex; align-items:center; gap:10px; padding:0 12px; border-bottom:1px solid var(--line); background:var(--bg) }
20
20
  .brand { display:flex; align-items:center; gap:9px; min-width:0; overflow:hidden } .brand .logo { flex:none } .brand > div { min-width:0 } .brand b { overflow:hidden; text-overflow:ellipsis }
21
- header > * { flex-shrink:0 } header .brand { flex-shrink:100 } header #board { flex-shrink:50 } .tok, .ctxp { flex-shrink:2; min-width:0; overflow:hidden; text-overflow:ellipsis } #pill { flex-shrink:.05; min-width:0; overflow:hidden; text-overflow:ellipsis } /* in a narrow window the buttons (the gear above all) stay whole: the name gives way first, then the counters */
21
+ header > * { flex-shrink:0 } header .brand { flex-shrink:100; min-width:26px } header #board { flex-shrink:.01 } .tok, .ctxp { flex-shrink:2; min-width:0; overflow:hidden; text-overflow:ellipsis } #pill { flex-shrink:.05; min-width:min-content; overflow:hidden; text-overflow:ellipsis } /* in a narrow window the buttons (the gear above all) stay whole: the name gives way first (down to the logo), then the counters; the chips only when nothing else can (.01, not 0: the header must never overflow) */
22
22
  @media (max-width:720px) { .ctxp { display:none !important } } @media (max-width:620px) { .tok { display:none !important } } @media (max-width:560px) { #bCoffee { display:none !important } } @media (max-width:480px) { header { gap:6px; padding-left:8px; padding-right:8px } } /* narrow: the coffee link stays in ⚙ › Tools */
23
23
  .logo { width:26px; height:26px; display:block; flex:none }
24
24
  .brand b { font:700 14px "Comic Sans MS","Chalkboard SE",system-ui,sans-serif; white-space:nowrap }
@@ -153,7 +153,7 @@
153
153
  /* the header's status chips are styled by id: "board" is also the class the Sessions view puts on <body>, and a rule on .board used to hide the whole page in a narrow window */
154
154
  #board { display:flex; gap:6px; font-size:11.5px; flex-wrap:nowrap; min-width:0; overflow:hidden } #board em { font-style:normal } #board span { flex:none; padding:2px 8px; border-radius:99px; background:var(--panel2); border:1px solid var(--line); white-space:nowrap }
155
155
  #board .working { color:var(--gold) } #board .waiting { color:#8fd3ff } #board .blocked { color:#fff; background:#c0392b; border-color:#c0392b } #board .stalled { color:#1b1d27; background:#ffb347; border-color:#ffb347 } #board .done { color:var(--ok) }
156
- @media (max-width:1180px) { #board em { display:none } .meet { display:none } }
156
+ @media (max-width:1180px) { #board em { display:none } .meet { display:none } } #board.tight em { display:none } /* tight: set by fitBoard when the chips do not all fit with their words */
157
157
  @media (max-width:640px) { #board { display:none } }
158
158
  /* what is said sits above everything else on the tile (badge, prop, ticker) and is never cut: a line longer than its tile grows over the tiles below */
159
159
  .bubble { display:none; position:absolute; z-index:6; top:6px; left:6px; width:max-content; max-width:calc(100% - 12px); background:#fff; color:#1b1d27; border-radius:11px; padding:6px 10px; font-size:12.5px; line-height:1.35; box-shadow:0 4px 14px rgba(0,0,0,.35); overflow-wrap:anywhere }
@@ -290,13 +290,14 @@
290
290
  <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>
291
291
  <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>
292
292
  <h4 data-fr="Source" data-en="Source">Source</h4>
293
- <div class="box"><div class="seg" style="width:100%" id="modeSeg"><button data-m="live" style="flex:1">Live</button><button data-m="replay" style="flex:1">Replay</button><button data-m="demo" style="flex:1">Demo</button></div>
293
+ <div class="box"><div class="seg" style="width:100%" id="modeSeg"><button data-m="live" style="flex:1" data-fr="Live" data-en="Live">Live</button><button data-m="replay" style="flex:1" data-fr="Replay" data-en="Replay">Replay</button><button data-m="demo" style="flex:1" data-fr="Démo" data-en="Demo">Demo</button></div>
294
294
  <select id="liveProj" style="display:none;margin-top:6px" title="Project to follow"></select>
295
295
  <select id="sessions" style="display:none"></select></div>
296
296
  <h4 data-fr="Personnages" data-en="Characters">Personnages</h4>
297
297
  <div class="box"><select id="charPack" aria-label="Characters"><option value="none" data-fr="Personnages dessinés" data-en="Drawn characters">Personnages dessinés</option><option value="folder" data-fr="Mon dossier" data-en="My folder">Mon dossier</option><option value="cats" data-fr="Pack de chats" data-en="Cat pack">Pack de chats</option></select></div>
298
298
  <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>
299
299
  <button class="mi" data-t="customLaptop"><span data-fr="Ordinateur devant vos personnages" data-en="Laptop in front of your own characters">Ordinateur devant vos personnages</span><i class="sw"></i></button>
300
+ <button class="mi" data-t="ownNames"><span data-fr="Garder les noms de mes agents" data-en="Keep my agents’ own names">Garder les noms de mes agents</span><i class="sw"></i></button>
300
301
  <h4 data-fr="Outils" data-en="Tools">Outils</h4>
301
302
  <button class="mi" id="mCoffee" style="display:none"><span id="mCoffeeT"></span></button>
302
303
  <button class="mi" id="mKeys"><span data-fr="⌨ Raccourcis clavier (?)" data-en="⌨ Keyboard shortcuts (?)">⌨ Raccourcis clavier (?)</span></button>
@@ -344,7 +345,7 @@ const CID = Math.random().toString(36).slice(2, 12); // identifies this window
344
345
  const Q = new URLSearchParams(location.search);
345
346
  const EMBED = Q.get('embed') === '1' || window.parent !== window;
346
347
 
347
- let prefs = { lang: 'en', voice: true, roster: true, banter: true, reactions: false, office: false, officeVol: 0.8, vstyle: 'cartoon', notify: false, epi: false, vanish: true, music: false, station: 'fip', musicVol: 0.35, view: 'call', hideCode: false };
348
+ let prefs = { lang: 'en', voice: true, roster: true, banter: true, reactions: false, office: false, officeVol: 0.8, vstyle: 'cartoon', notify: false, epi: false, vanish: true, music: false, station: 'fip', musicVol: 0.35, view: 'call', hideCode: false, ownNames: true };
348
349
  { let loc = {}; try { loc = JSON.parse(localStorage.getItem('cc.prefs') || '{}') || {}; } catch {} // the settings file (shared by every window) and this origin's copy: the more recent one wins
349
350
  const sv = window.SAVED_PREFS && typeof window.SAVED_PREFS === 'object' ? window.SAVED_PREFS : {}; Object.assign(prefs, (sv._t || 0) > (loc._t || 0) ? sv : loc); }
350
351
  if (prefs.vstyle === 'babble') prefs.vstyle = 'animal', prefs.vstyleChosen = true;
@@ -411,11 +412,16 @@ function renderCharInfo() {
411
412
  $('#charOpen').style.display = EMBED ? '' : 'none';
412
413
  }
413
414
  function persona(a) { const p = persona0(a); p.custom = customFor(a.id); return p; }
415
+ const subName = (a, h) => (a.own && prefs.ownNames ? (nameBook[a.id] = a.own) : a.given || uniqueName(a.id, h)); // own: the name the person gave this agent (see ownName in server.js); otherwise a first name from the list. a.own is tested first: the landing page reuses this section and has no settings
416
+ function renameAll() { // ⚙ → own names on or off: every sub-agent is named again, in the order it arrived
417
+ for (const k in nameBook) delete nameBook[k]; for (const p of Object.values(S.agents)) if (p.id !== 'main' && p.id !== 'user') p.name = subName(p, hash(p.id));
418
+ buildStage(); paintStatus(); renderLog(true); syncUI();
419
+ }
414
420
  function persona0(a) {
415
- const h = hash(a.id), base = { ...a };
416
- if (a.id === 'main') return { ...base, name: a.name || 'Claude', role: L('Chef', 'Lead'), shirt: '#e8845f', shape: 0, acc: 1, eye: 0, glasses: false, scene: 6, prop: 1, acc2: '#f5c04a', lead: true, vi: 0, gid: 'main' };
421
+ const h = hash(a.id), base = { ...a, given: 'given' in a ? a.given : a.name };
422
+ if (a.id === 'main') return { ...base, name: a.name || 'Margot', role: L('Chef', 'Lead'), shirt: '#e8845f', shape: 0, acc: 1, eye: 0, glasses: false, scene: 6, prop: 1, acc2: '#f5c04a', lead: true, vi: 0, gid: 'main' };
417
423
  if (a.id === 'user') return { ...base, name: L('Vous', 'You'), role: L('Humain', 'Human'), shirt: '#8a94c9', shape: 1, acc: 2, eye: 1, glasses: false, scene: 7, prop: 1, acc2: '#ff7a6b', vi: 3, gid: 'user' };
418
- return { ...base, name: a.name || uniqueName(a.id, h), roleEn: a.roleEn || a.role, role: (FR() && a.roleFr) || a.roleEn || a.role, shirt: PAL[(h >>> 6) % 8], shape: (h >>> 3) % 5, acc: (h >>> 9) % 6, eye: (h >>> 12) % 3, glasses: ((h >>> 15) % 4) === 0,
424
+ return { ...base, name: subName(base, h), roleEn: a.roleEn || a.role, role: (FR() && a.roleFr) || a.roleEn || a.role, shirt: PAL[(h >>> 6) % 8], shape: (h >>> 3) % 5, acc: (h >>> 9) % 6, eye: (h >>> 12) % 3, glasses: ((h >>> 15) % 4) === 0,
419
425
  scene: (h >>> 18) % 6, prop: (h >>> 21) % 3, acc2: PAL[(h >>> 24) % 8], vi: h >>> 4, gid: h.toString(36) };
420
426
  }
421
427
 
@@ -756,33 +762,39 @@ const synth = window.speechSynthesis;
756
762
  const estMs = (text) => Math.min(9000, 900 + text.length * 42) / spd();
757
763
 
758
764
  const NOVELTY = /grandma|grandpa|rocko|bad news|bahh|bells|boing|bubbles|cellos|good news|jester|organ|superstar|trinoids|whisper|wobble|zarvox|albert|fred|junior|kathy|ralph|hysterical|deranged|pipe/i;
759
- const GOOD = /premium|enhanced|natural|siri|neural|améli|amelie|thomas|jacques|audrey|marie|eddy|flo\b|reed|sandy|shelley|samantha|daniel|karen|moira|serena|oliver|google|hortense|julie|paul|denise|henri|eloise|zira|david|hazel|mark|aria|guy/i;
760
- const voiceScore = (n) => (/premium|enhanced|natural|neural|siri/i.test(n) ? 100 : 0) + (GOOD.test(n) ? 30 : 0) - (NOVELTY.test(n) ? 200 : 0);
761
- function rankVoices(list) { // list of names → best distinct voices first
762
- const seen = new Set(), out = [];
763
- [...list].sort((a, b) => voiceScore(b) - voiceScore(a)).forEach((n) => { const base = n.replace(/\s*\(.*$/, ''); if (voiceScore(n) > -100 && !seen.has(base)) { seen.add(base); out.push(n); } });
765
+ const ROBOT = /^(eddy|flo|reed|sandy|shelley)\b/i; /* Apple's synthetic voices: understandable, and plainly a machine. Kept for when nothing else is installed */
766
+ const GOOD = /premium|enhanced|natural|siri|neural|améli|amelie|thomas|jacques|audrey|aurélie|aurelie|marie|samantha|daniel|karen|moira|serena|oliver|google|hortense|julie|paul|denise|henri|eloise|zira|david|hazel|mark|aria|guy/i;
767
+ const voiceScore = (n) => (/premium|enhanced|natural|neural|siri/i.test(n) ? 100 : 0) + (GOOD.test(n) ? 30 : 0) - (ROBOT.test(n) ? 50 : 0) - (NOVELTY.test(n) ? 200 : 0);
768
+ const frHome = () => [...(navigator.languages || []), navigator.language || ''].map((l) => String(l).replace('-', '_')).find((l) => /^fr_[A-Z]{2}$/.test(l)) || 'fr_FR'; /* French from France unless the computer itself is set to another French */
769
+ function rankVoices(list, lang) { // [{ name, locale }] → names, best distinct voices first. The synthetic ones only when fewer than two others exist
770
+ const home = lang === 'fr' ? frHome() : '', sc = (v) => voiceScore(v.name) + (home && v.locale === home ? 10 : 0), seen = new Set(), out = [];
771
+ [...list].sort((a, b) => sc(b) - sc(a)).forEach((v) => { const base = v.name.replace(/\s*\(.*$/, ''), x = sc(v); if (x > -100 && (x >= 0 || out.length < 2) && !seen.has(base)) { seen.add(base); out.push(v.name); } });
764
772
  return out.slice(0, 5);
765
773
  }
766
774
  const TTS = {
767
- server: false, engine: 'system', platform: '', pool: { fr: [], en: [] }, cache: new Map(), made: new Map(), q: [], busy: false, now: 0,
775
+ server: false, engine: 'system', platform: '', pool: { fr: [], en: [] }, sys: { fr: [], en: [] }, kv: new Set(), cache: new Map(), made: new Map(), q: [], busy: false, now: 0,
768
776
  async init() {
769
- this.engine = 'system'; this.server = false; this.pool = { fr: [], en: [] }; this.cache.clear(); this.made.clear(); this.q = [];
777
+ this.engine = 'system'; this.server = false; this.pool = { fr: [], en: [] }; this.sys = { fr: [], en: [] }; this.kv = new Set(); this.cache.clear(); this.made.clear(); this.q = [];
770
778
  try {
771
- if (prefs.vstyle === 'kokoro') { // natural voices installed on demand; until they are, the system voices stay in use
772
- const k = await (await fetch('/api/voices?engine=kokoro')).json();
773
- if (k.available && k.ready) { this.server = true; this.engine = 'kokoro'; this.platform = k.platform || ''; for (const l of ['fr', 'en']) this.pool[l] = k.voices.filter((v) => v.locale.startsWith(l + '_')).map((v) => v.name); return; }
779
+ let k = null; if (prefs.vstyle === 'kokoro') { try { k = await (await fetch('/api/voices?engine=kokoro')).json(); } catch {} if (!k || !k.available || !k.ready) k = null; } // natural voices installed on demand; until they are, the system voices stay in use
780
+ let d = { available: false, voices: [] }; try { d = await (await fetch('/api/voices')).json(); } catch {}
781
+ this.platform = d.platform || (k && k.platform) || ''; const sys = {}; for (const l of ['fr', 'en']) sys[l] = d.available ? rankVoices(d.voices.filter((v) => v.locale.startsWith(l + '_')), l) : [];
782
+ if (k) { // Kokoro speaks the languages it has voices for (English); the others are read by the system voices, each character keeping one of its own
783
+ this.server = true; this.engine = 'kokoro'; this.sys = sys;
784
+ for (const l of ['fr', 'en']) { const kv = k.voices.filter((v) => v.locale.startsWith(l + '_')).map((v) => v.name); kv.forEach((v) => this.kv.add(v)); this.pool[l] = kv.length ? kv : sys[l]; }
785
+ return;
774
786
  }
775
- const d = await (await fetch('/api/voices')).json(); this.platform = d.platform || ''; if (!d.available) return; this.server = true;
776
- for (const l of ['fr', 'en']) this.pool[l] = rankVoices(d.voices.filter((v) => v.locale.startsWith(l + '_')).map((v) => v.name)); } catch {}
787
+ if (!d.available) return; this.server = true; this.pool = sys; } catch {}
777
788
  },
778
789
  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; },
779
790
  rate() { return Math.round(Math.max(120, Math.min(330, 178 * (S.live ? 1.12 * lagFactor() * (S.boost > 1 ? 1.25 : 1) : +$('#speed').value))) / 10) * 10; },
780
791
  rateAt(k, boost) { const i = S.idx, b = S.boost; S.idx = k; S.boost = boost; const r = this.rate(); S.idx = i; S.boost = b; return r; }, // the rate rate() will give once playback is at event k
781
792
  buf(text, voice, rate, ahead) { // ahead: made in advance (see warmTTS); otherwise the line is wanted now
782
- const vt = this.engine + '|' + voice + '|' + text, K = (r) => this.engine + '|' + voice + '|' + r + '|' + text, key = K(rate), got = this.made.get(vt) || []; // got: the rates at which this text is ready in this voice
793
+ const eng = this.kv.has(voice) ? 'kokoro' : 'system', fb = eng === 'kokoro' && this.sys.en.length ? this.sys.en[Math.max(0, this.pool.en.indexOf(voice)) % this.sys.en.length] : ''; /* fb: the system voice that reads the line if Kokoro cannot */
794
+ const vt = eng + '|' + voice + '|' + text, K = (r) => eng + '|' + voice + '|' + r + '|' + text, key = K(rate), got = this.made.get(vt) || []; // got: the rates at which this text is ready in this voice
783
795
  if (!ahead && S.live && !got.includes(rate)) { const r = got.find((x) => Math.abs(x - rate) <= rate * 0.2 && this.cache.has(K(x))); if (r) return this.cache.get(K(r)); } // made ahead at the rate of a moment ago (the call fell behind or caught up since): played as it is, a second of silence is worse than a slightly different pace
784
796
  if (!this.cache.has(key)) {
785
- this.cache.set(key, fetch(`/api/tts?engine=${this.engine}&voice=${encodeURIComponent(voice)}&rate=${rate}&text=${encodeURIComponent(text)}`).then((r) => { if (!r.ok) throw new Error('tts'); return r.arrayBuffer(); }).then((b) => AUD.ctx.decodeAudioData(b)));
797
+ this.cache.set(key, fetch(`/api/tts?engine=${eng}&voice=${encodeURIComponent(voice)}&rate=${rate}${fb ? '&fb=' + encodeURIComponent(fb) : ''}&text=${encodeURIComponent(text)}`).then((r) => { if (!r.ok) throw new Error('tts'); return r.arrayBuffer(); }).then((b) => AUD.ctx.decodeAudioData(b)));
786
798
  if (this.cache.size > 60) this.cache.delete(this.cache.keys().next().value);
787
799
  this.cache.get(key).then(() => { this.made.set(vt, [...(this.made.get(vt) || []), rate]); if (this.made.size > 120) this.made.delete(this.made.keys().next().value); }, () => this.cache.delete(key));
788
800
  if (!ahead) { this.now++; const end = () => { this.now--; this.pump(); }; this.cache.get(key).then(end, end); } // a line wanted now never waits behind the ones made ahead: they stop being started until it is ready
@@ -902,7 +914,7 @@ async function speakServer(text, p, gain, token, fr = FR()) {
902
914
  function speakBrowser(text, p, token, fr = FR()) { // fallback where no server voice exists (Windows/Linux)
903
915
  return new Promise((resolve) => {
904
916
  const vs = synth.getVoices().filter((v) => v.lang.toLowerCase().startsWith(fr ? 'fr' : 'en')); if (!vs.length) return resolve(false);
905
- const ranked = rankVoices(vs.map((v) => v.name)), voice = vs.find((v) => v.name === (p.id === 'main' ? ranked[0] : ranked[(p.vi >>> 2) % ranked.length])) || vs[0];
917
+ const ranked = rankVoices(vs.map((v) => ({ name: v.name, locale: v.lang.replace('-', '_') })), fr ? 'fr' : 'en'), voice = vs.find((v) => v.name === (p.id === 'main' ? ranked[0] : ranked[(p.vi >>> 2) % ranked.length])) || vs[0];
906
918
  const parts = speakable(text).match(/[^.!?;:]+[.!?;:]*/g) || [text]; synth.cancel(); let i = 0, done = false, started = false, t0 = 0, said = 0; const all = parts.reduce((n, x) => n + x.length, 0);
907
919
  const fin = (ok) => { if (!done) { done = true; resolve(ok); } };
908
920
  setTimeout(() => { if (!started && !done) { synth.cancel(); fin(false); } }, 2500); // engine never started (e.g. inside a webview)
@@ -1032,7 +1044,7 @@ function paintStatus() {
1032
1044
  const prev = $('.row.now'); if (prev) prev.classList.remove('now');
1033
1045
  const now = $(`.row[data-i="${S.idx}"]`); if (now) { now.classList.add('now'); if (document.body.classList.contains('log')) now.scrollIntoView({ block: 'nearest' }); }
1034
1046
  later('detail', renderDetail); later('tree', renderTree); later('meet', renderMeet, 400); drawGantt();
1035
- $('#seek').value = S.idx; $('#pos').textContent = `${Math.min(S.idx + 1, S.events.length)} / ${S.events.length}`;
1047
+ const sk = $('#seek'); sk.max = Math.max(0, S.events.length - 1); sk.value = S.idx; $('#pos').textContent = `${Math.min(S.idx + 1, S.events.length)} / ${S.events.length}`;
1036
1048
  }
1037
1049
  const applyStatus = (ev) => {
1038
1050
  if (ev.kind === 'order') { (S.waitOn[ev.agent] = S.waitOn[ev.agent] || new Set()).add(ev.to); S.orderTs[ev.to] = ev.ts || 0; S.status[ev.to] = 'working'; delete S.doneAt[ev.to]; S.gone.delete(ev.to); S.leaving.delete(ev.to); if (!ev.bg && ev.toolId) S.run[ev.agent] = { id: ev.toolId, ts: ev.ts, start: performance.now(), name: 'Agent' }; const t = $('#t-' + ev.to); if (t) t.classList.remove('vanishing'); }
@@ -1141,8 +1153,14 @@ function paintStates() {
1141
1153
  paintPill();
1142
1154
  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')]]
1143
1155
  .filter(([k]) => n[k] && !S.pillSess).map(([k, i, l]) => `<span class="${k}" title="${n[k]} ${l}">${i} ${n[k]}<em> ${l}</em></span>`).join('');
1144
- if (b.innerHTML !== parts) b.innerHTML = parts; S.nBlocked = n.blocked + n.stalled; S.attn = attn; paintRoster(ros); return n;
1156
+ if (b.innerHTML !== parts) b.innerHTML = parts; fitBoard(); S.nBlocked = n.blocked + n.stalled; S.attn = attn; paintRoster(ros); return n;
1157
+ }
1158
+ function fitBoard(force) { // the chips lose their words when they do not all fit (a long language, a narrower window): a chip is never cut mid-word. Tried again with the words only when the window or the texts around have changed
1159
+ const b = $('#board'), key = innerWidth + '|' + b.textContent.length + '|' + $('#pill').textContent.length + '|' + $('#subtitle').textContent.length;
1160
+ if (b.classList.contains('tight')) { if (!force && key === fitBoard.key) return; b.classList.remove('tight'); }
1161
+ fitBoard.key = key; if (b.scrollWidth > b.clientWidth + 1) b.classList.add('tight');
1145
1162
  }
1163
+ addEventListener('resize', () => fitBoard(true)); new ResizeObserver(() => requestAnimationFrame(() => fitBoard(true))).observe($('#board')); /* the room the chips have also changes when the texts beside them do (a title, a longer status) */
1146
1164
  const RORDER = { blocked: 0, stalled: 1, working: 2, waiting: 3, idle: 4, done: 5 };
1147
1165
  function paintRoster(ros) { // a fixed list beside the call: who is doing what, urgent first, readable even with dozens of agents
1148
1166
  const box = $('#roster'); if (!box || !document.body.classList.contains('rost')) return;
@@ -1912,7 +1930,7 @@ async function loadSession(proj, id) {
1912
1930
  if (proj === '_demo') S.events.forEach((e) => { if (e.fr) trMem.set(baseText(e), e.fr); }); // the demo ships its own French
1913
1931
  $('#subtitle').textContent = S.title; recomputeStatus(0); buildStage(); renderHighlights(); setFocus('main'); renderLog(true); computeGantt(); layout(); paintStatus(); prefetchTr(0); return true;
1914
1932
  }
1915
- const niceProj = (p) => (p === '_demo' ? '' : p === '_codex' ? 'Codex' : String(p).replace(/^-+/, '').replace(/-/g, '/').split('/').slice(-2).join('/'));
1933
+ const niceProj = (p) => (p === '_demo' ? '' : p === '_codex' ? 'Codex' : p === '_vibe' ? 'Mistral Vibe' : String(p).replace(/^-+/, '').replace(/-/g, '/').split('/').slice(-2).join('/'));
1916
1934
  async function fillProjects() { // live view: follow the latest activity anywhere, or pin one project
1917
1935
  const sel = $('#liveProj'); let rows = []; try { rows = await (await fetch('/api/projects')).json(); } catch {}
1918
1936
  sel.innerHTML = `<option value="">${L('Tous les projets (le plus récent)', 'All projects (most recent)')}</option>` + rows.map((r) => `<option value="${esc(r.proj)}">${esc(niceProj(r.proj))}</option>`).join('');
@@ -2189,7 +2207,7 @@ function menuInit() {
2189
2207
  $('#menuNone').style.display = q && !secs.some((sc) => sc.items.some((e) => !e.classList.contains('mhide'))) ? '' : 'none';
2190
2208
  };
2191
2209
  }
2192
- $('#menu').onclick = (e) => { const b = e.target.closest('.mi[data-t]'); if (!b) return; prefs[b.dataset.t] = !prefs[b.dataset.t]; savePrefs(); if (prefs[b.dataset.t] && ['voice', 'office', 'music', 'notify'].includes(b.dataset.t)) claimAudio(); b.querySelector('.sw').classList.toggle('on', prefs[b.dataset.t]); AUD.apply(); if (b.dataset.t === 'office') $('#officeBox').style.display = prefs.office ? '' : 'none'; if (b.dataset.t === 'customLaptop') { for (const k in imgCache) delete imgCache[k]; buildStage(); paintStatus(); } if (b.dataset.t === 'reactions') document.body.classList.toggle('react', prefs.reactions); if (b.dataset.t === 'roster') { document.body.classList.toggle('rost', prefs.roster); paintStates(); } if (b.dataset.t === 'notify') { prefs.notifyChosen = true; savePrefs(); } if (b.dataset.t === 'notify' && prefs.notify) askNotify(); if (b.dataset.t === 'vanish' && !prefs.vanish) { S.gone.clear(); paintStatus(); }
2210
+ $('#menu').onclick = (e) => { const b = e.target.closest('.mi[data-t]'); if (!b) return; prefs[b.dataset.t] = !prefs[b.dataset.t]; savePrefs(); if (prefs[b.dataset.t] && ['voice', 'office', 'music', 'notify'].includes(b.dataset.t)) claimAudio(); b.querySelector('.sw').classList.toggle('on', prefs[b.dataset.t]); AUD.apply(); if (b.dataset.t === 'office') $('#officeBox').style.display = prefs.office ? '' : 'none'; if (b.dataset.t === 'customLaptop') { for (const k in imgCache) delete imgCache[k]; buildStage(); paintStatus(); } if (b.dataset.t === 'ownNames') renameAll(); 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(); }
2193
2211
  if (b.dataset.t === 'music') { $('#musicBox').style.display = prefs.music ? '' : 'none'; if (prefs.music) { AUD.init(); AUD.apply(); } } };
2194
2212
  $('#station').innerHTML = STATIONS.map((s) => `<option value="${s.id}">📻 ${s.name}</option>`).join('');
2195
2213
  $('#station').value = prefs.station; $('#musicVol').value = prefs.musicVol; $('#musicBox').style.display = prefs.music ? '' : 'none';
@@ -2205,8 +2223,8 @@ async function kokoroUI() {
2205
2223
  b.disabled = st.installing || !st.npm; b.textContent = st.installing ? L('Installation…', 'Installing…') : L(`Installer (${st.sizeMb} Mo)`, `Install (${st.sizeMb} MB)`);
2206
2224
  h.textContent = st.installing ? L('Téléchargement en cours, vous pouvez continuer : les voix du système servent en attendant.', 'Downloading, you can carry on: system voices are used meanwhile.')
2207
2225
  : st.error ? st.error : !st.npm && !st.installed ? L('npm est introuvable : installez Node.js pour activer cette option.', 'npm not found: install Node.js to enable this option.')
2208
- : st.installed ? (st.gaveUp ? L('Le moteur s’est arrêté : les voix du système servent à la place.', 'The engine stopped: system voices are used instead.') : st.ready ? L('Voix prêtes.', 'Voices ready.') : L('Chargement du modèle (téléchargé au premier usage, environ 305 Mo)…', 'Loading the model (downloaded on first use, about 305 MB)…'))
2209
- : L('Voix locales de bonne qualité, en anglais surtout (une seule voix française). Gratuit, rien n’est envoyé nulle part.', 'Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.');
2226
+ : st.installed ? (st.gaveUp ? L('Le moteur s’est arrêté : les voix du système servent à la place.', 'The engine stopped: system voices are used instead.') : st.ready ? L('Voix prêtes.', 'Voices ready.') + ' ' + L('Le français est lu par les voix de votre système : en installer une « améliorée » ou « premium » change tout.', 'French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.') : L('Chargement du modèle (téléchargé au premier usage, environ 305 Mo)…', 'Loading the model (downloaded on first use, about 305 MB)…'))
2227
+ : L('Voix locales de bonne qualité, en anglais (le français passe par les voix de votre système). Gratuit, rien n’est envoyé nulle part.', 'Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.');
2210
2228
  if ((st.installing || (st.installed && !st.ready && !st.gaveUp)) && !kokoroTimer) kokoroTimer = setInterval(async () => { await kokoroUI(); }, 2500);
2211
2229
  if (!st.installing && (st.ready || st.gaveUp || !st.installed) && kokoroTimer) { clearInterval(kokoroTimer); kokoroTimer = null; if (st.ready && TTS.engine !== 'kokoro') TTS.init(); }
2212
2230
  }
@@ -2255,7 +2273,7 @@ function lastWord() { // the last sentence the lead said: a closing question (
2255
2273
  function paintPill() {
2256
2274
  const pill = $('#pill'), now = Date.now(); let cls = '', txt = '', to = '', tip = '', sess = false;
2257
2275
  if (S.offline) { cls = 'off'; txt = '⚠ ' + L('HORS LIGNE', 'OFFLINE'); }
2258
- else if (!S.live) txt = 'REPLAY';
2276
+ else if (!S.live) txt = L('REPLAY', 'REPLAY');
2259
2277
  else if (!S.cur) txt = L('En attente d’une session', 'Waiting for a session');
2260
2278
  else if (S.locked) txt = L('PRÊT', 'READY');
2261
2279
  else if (!S.playing) txt = L('PAUSE', 'PAUSED');
@@ -2265,7 +2283,7 @@ function paintPill() {
2265
2283
  const what = (id) => `${nameOf(id)} — ${st[id].txt.replace(/^\S+\s/, '')}`, more = (a) => (a.length > 1 ? ` +${a.length - 1}` : ''), lead = st.main || { k: 'idle' };
2266
2284
  if (by.blocked.length) { cls = 'blocked'; to = by.blocked[0]; txt = '✋ ' + what(to) + more(by.blocked); }
2267
2285
  else if (by.stalled.length) { cls = 'stalled'; to = by.stalled[0]; txt = '⚠ ' + what(to) + more(by.stalled); }
2268
- else if (by.working.length || by.waiting.length) { // "2 working · Claude — waiting on Ken, Mira", or what the only one at work is doing
2286
+ else if (by.working.length || by.waiting.length) { // "2 working · Margot — waiting on Ken, Mira", or what the only one at work is doing
2269
2287
  const w = by.working, wt = lead.k === 'waiting' ? 'main' : w.length ? '' : by.waiting[0]; cls = 'working'; to = wt || (w.length === 1 ? w[0] : 'main');
2270
2288
  txt = (w.length === 1 && !wt ? '⚙ ' + what(w[0]) : w.length ? `⚙ ${w.length} ${L('au travail', 'working')}` : '') + (wt ? (w.length ? ' · ' : '🔗 ') + what(wt) : '');
2271
2289
  }
@@ -42,6 +42,7 @@
42
42
  "Copy as Markdown": "Kopiér som Markdown",
43
43
  "Copy failed": "Kopiering mislykkedes",
44
44
  "custom character(s) loaded": "egne figurer indlæst",
45
+ "Demo": "Demo",
45
46
  "Demo — a team fixes a slow login": "Demo — et team retter et langsomt login",
46
47
  "done": "færdig",
47
48
  "Done": "Færdig",
@@ -62,7 +63,8 @@
62
63
  "Files touched": "Berørte filer",
63
64
  "Final report": "Slutrapport",
64
65
  "first error": "første fejl",
65
- "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.",
66
+ "French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "Fransk læses af dine systemstemmer: at installere en “forbedret” eller “premium” stemme gør en stor forskel.",
67
+ "Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "Lokale stemmer af god kvalitet, på engelsk (fransk bruger dine systemstemmer). Gratis, intet forlader din computer.",
66
68
  "Hide code & file names in videos": "Skjul kode og filnavne i videoer",
67
69
  "Highlights": "Højdepunkter",
68
70
  "Human": "Menneske",
@@ -71,6 +73,7 @@
71
73
  "Installing…": "Installerer…",
72
74
  "is sharing their screen": "deler sin skærm",
73
75
  "Join the call": "Deltag i opkaldet",
76
+ "Keep my agents’ own names": "Behold mine agenters egne navne",
74
77
  "Keyboard shortcuts": "Tastaturgenveje",
75
78
  "Language": "Sprog",
76
79
  "Lead": "Leder",
@@ -80,6 +83,7 @@
80
83
  "lines": "linjer",
81
84
  "Listing ": "Viser ",
82
85
  "Little lines & animations": "Små replikker og animationer",
86
+ "Live": "Live",
83
87
  "Loading the model (downloaded on first use, about 305 MB)…": "Indlæser modellen (hentes ved første brug, ca. 305 MB)…",
84
88
  "longest wait ": "længste ventetid ",
85
89
  "Looking for a tool": "Leder efter et værktøj",
@@ -121,6 +125,8 @@
121
125
  "Reload": "Genindlæs",
122
126
  "Remove finished agents (2 min)": "Fjern færdige agenter (2 min)",
123
127
  "Repeated failures": "Gentagne fejl",
128
+ "Replay": "Afspilning",
129
+ "REPLAY": "AFSPILNING",
124
130
  "report": "rapport",
125
131
  "report to": "rapport til",
126
132
  "Role": "Rolle",
@@ -58,6 +58,7 @@
58
58
  "Files touched": "Bearbeitete Dateien",
59
59
  "Final report": "Abschlussbericht",
60
60
  "first error": "erster Fehler",
61
+ "French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "Französisch wird von deinen Systemstimmen gelesen: Eine „erweiterte“ oder „Premium“-Stimme zu installieren macht viel aus.",
61
62
  "Hide code & file names in videos": "Code & Dateinamen in Videos ausblenden",
62
63
  "Highlights": "Highlights",
63
64
  "Human": "Mensch",
@@ -65,6 +66,7 @@
65
66
  "input": "Eingabe",
66
67
  "is sharing their screen": "teilt den Bildschirm",
67
68
  "Join the call": "Dem Videoanruf beitreten",
69
+ "Keep my agents’ own names": "Eigene Namen meiner Agenten behalten",
68
70
  "Language": "Sprache",
69
71
  "Lead": "Hauptagent",
70
72
  "Lead context": "Kontext Hauptagent",
@@ -72,6 +74,7 @@
72
74
  "lines": "Zeilen",
73
75
  "Listing ": "Liste auf ",
74
76
  "Little lines & animations": "Kleine Sprüche & Animationen",
77
+ "Live": "Live",
75
78
  "longest wait ": "längstes Warten ",
76
79
  "Looking for a tool": "Sucht ein Werkzeug",
77
80
  "looping?": "Schleife?",
@@ -106,6 +109,8 @@
106
109
  "Recent activity": "Letzte Aktivität",
107
110
  "Remove finished agents (2 min)": "Fertige Agenten entfernen (2 Min.)",
108
111
  "Repeated failures": "Wiederholte Fehler",
112
+ "Replay": "Wiedergabe",
113
+ "REPLAY": "WIEDERGABE",
109
114
  "report": "Bericht",
110
115
  "report to": "Bericht an",
111
116
  "Role": "Rolle",
@@ -167,10 +172,11 @@
167
172
  "Reload": "Neu laden",
168
173
  "Open folder": "Ordner öffnen",
169
174
  "custom character(s) loaded": "eigene Figur(en) geladen",
175
+ "Demo": "Demo",
170
176
  "Drop your pixel art (PNG) in this folder to replace the characters:": "Leg deine Pixel-Art (PNG) in diesen Ordner, um die Figuren zu ersetzen:",
171
177
  "lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = Hauptagent · you.png = du · andere = Subagenten · name-talk.png = offener Mund",
172
178
  "Downloading, you can carry on: system voices are used meanwhile.": "Wird heruntergeladen, du kannst weitermachen: bis dahin kommen die Systemstimmen zum Einsatz.",
173
- "Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "Lokale Stimmen in guter Qualität, vor allem Englisch (nur eine französische Stimme). Kostenlos, nichts verlässt deinen Rechner.",
179
+ "Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "Lokale Stimmen in guter Qualität, auf Englisch (Französisch nutzt deine Systemstimmen). Kostenlos, nichts verlässt deinen Rechner.",
174
180
  "Installing…": "Installation läuft…",
175
181
  "Loading the model (downloaded on first use, about 305 MB)…": "Modell wird geladen (beim ersten Mal heruntergeladen, etwa 305 MB)…",
176
182
  "npm not found: install Node.js to enable this option.": "npm nicht gefunden: Installiere Node.js, um diese Option zu nutzen.",
@@ -43,6 +43,7 @@
43
43
  "Copy as Markdown": "Copy as Markdown",
44
44
  "Copy failed": "Copy failed",
45
45
  "custom character(s) loaded": "custom character(s) loaded",
46
+ "Demo": "Demo",
46
47
  "Demo — a team fixes a slow login": "Demo — a team fixes a slow login",
47
48
  "Display": "Display",
48
49
  "done": "done",
@@ -67,8 +68,9 @@
67
68
  "Find a session": "Find a session",
68
69
  "Find a session…": "Find a session…",
69
70
  "first error": "first error",
71
+ "French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.",
70
72
  "Go to a session": "Go to a session",
71
- "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.",
73
+ "Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.",
72
74
  "Hide code & file names in videos": "Hide code & file names in videos",
73
75
  "Highlights": "Highlights",
74
76
  "Human": "Human",
@@ -77,6 +79,7 @@
77
79
  "Installing…": "Installing…",
78
80
  "is sharing their screen": "is sharing their screen",
79
81
  "Join the call": "Join the call",
82
+ "Keep my agents’ own names": "Keep my agents’ own names",
80
83
  "Keyboard shortcuts": "Keyboard shortcuts",
81
84
  "Language": "Language",
82
85
  "Laptop in front of your own characters": "Laptop in front of your own characters",
@@ -87,6 +90,7 @@
87
90
  "lines": "lines",
88
91
  "Listing ": "Listing ",
89
92
  "Little lines & animations": "Little lines & animations",
93
+ "Live": "Live",
90
94
  "Lively": "Lively",
91
95
  "Loading the model (downloaded on first use, about 305 MB)…": "Loading the model (downloaded on first use, about 305 MB)…",
92
96
  "longest wait ": "longest wait ",
@@ -137,6 +141,8 @@
137
141
  "Reload": "Reload",
138
142
  "Remove finished agents (2 min)": "Remove finished agents (2 min)",
139
143
  "Repeated failures": "Repeated failures",
144
+ "Replay": "Replay",
145
+ "REPLAY": "REPLAY",
140
146
  "Replay: next / previous highlight": "Replay: next / previous highlight",
141
147
  "Replay: previous / next event": "Replay: previous / next event",
142
148
  "report": "report",
@@ -58,6 +58,7 @@
58
58
  "Files touched": "Archivos tocados",
59
59
  "Final report": "Informe final",
60
60
  "first error": "primer error",
61
+ "French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "El francés lo leen las voces de tu sistema: instalar una «mejorada» o «premium» cambia mucho.",
61
62
  "Hide code & file names in videos": "Ocultar código y archivos en los vídeos",
62
63
  "Highlights": "Destacados",
63
64
  "Human": "Humano",
@@ -65,6 +66,7 @@
65
66
  "input": "entrada",
66
67
  "is sharing their screen": "está compartiendo pantalla",
67
68
  "Join the call": "Unirse a la videollamada",
69
+ "Keep my agents’ own names": "Mantener los nombres de mis agentes",
68
70
  "Language": "Idioma",
69
71
  "Lead": "Principal",
70
72
  "Lead context": "Contexto del agente principal",
@@ -72,6 +74,7 @@
72
74
  "lines": "líneas",
73
75
  "Listing ": "Listando ",
74
76
  "Little lines & animations": "Frases y animaciones",
77
+ "Live": "En vivo",
75
78
  "longest wait ": "espera máxima ",
76
79
  "Looking for a tool": "Buscando una herramienta",
77
80
  "looping?": "¿en bucle?",
@@ -106,6 +109,8 @@
106
109
  "Recent activity": "Actividad reciente",
107
110
  "Remove finished agents (2 min)": "Quitar agentes terminados (2 min)",
108
111
  "Repeated failures": "Fallos repetidos",
112
+ "Replay": "Repetición",
113
+ "REPLAY": "REPETICIÓN",
109
114
  "report": "informe",
110
115
  "report to": "informe a",
111
116
  "Role": "Rol",
@@ -167,10 +172,11 @@
167
172
  "Reload": "Recargar",
168
173
  "Open folder": "Abrir carpeta",
169
174
  "custom character(s) loaded": "personaje(s) propio(s) cargado(s)",
175
+ "Demo": "Demo",
170
176
  "Drop your pixel art (PNG) in this folder to replace the characters:": "Suelta tu pixel art (PNG) en esta carpeta para reemplazar los personajes:",
171
177
  "lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = el agente principal · you.png = tú · el resto = los subagentes · name-talk.png = boca abierta",
172
178
  "Downloading, you can carry on: system voices are used meanwhile.": "Descargando, puedes seguir: mientras tanto se usan las voces del sistema.",
173
- "Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "Voces locales de buena calidad, sobre todo en inglés (una sola voz en francés). Gratis, nada sale de tu equipo.",
179
+ "Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "Voces locales de buena calidad, en inglés (el francés usa las voces de tu sistema). Gratis, nada sale de tu equipo.",
174
180
  "Installing…": "Instalando…",
175
181
  "Loading the model (downloaded on first use, about 305 MB)…": "Cargando el modelo (se descarga la primera vez, unos 305 MB)…",
176
182
  "npm not found: install Node.js to enable this option.": "No se encontró npm: instala Node.js para activar esta opción.",
@@ -58,6 +58,7 @@
58
58
  "Files touched": "触れたファイル",
59
59
  "Final report": "最終レポート",
60
60
  "first error": "最初のエラー",
61
+ "French is read by your system voices: installing an “enhanced” or “premium” one makes a big difference.": "フランス語はシステム音声で読み上げます。“拡張”または“プレミアム”の音声を入れると大きく変わります。",
61
62
  "Hide code & file names in videos": "動画でコードとファイル名を隠す",
62
63
  "Highlights": "ハイライト",
63
64
  "Human": "人間",
@@ -65,6 +66,7 @@
65
66
  "input": "入力",
66
67
  "is sharing their screen": "が画面を共有中",
67
68
  "Join the call": "ビデオ通話に参加",
69
+ "Keep my agents’ own names": "自分のエージェント名をそのまま使う",
68
70
  "Language": "言語",
69
71
  "Lead": "メインエージェント",
70
72
  "Lead context": "メインエージェントのコンテキスト",
@@ -72,6 +74,7 @@
72
74
  "lines": "行",
73
75
  "Listing ": "一覧表示中: ",
74
76
  "Little lines & animations": "ひとことセリフとアニメーション",
77
+ "Live": "ライブ",
75
78
  "longest wait ": "最長待ち時間 ",
76
79
  "Looking for a tool": "ツールを探し中",
77
80
  "looping?": "ループ中?",
@@ -106,6 +109,8 @@
106
109
  "Recent activity": "最近のアクティビティ",
107
110
  "Remove finished agents (2 min)": "完了したエージェントを削除(2分後)",
108
111
  "Repeated failures": "くり返しの失敗",
112
+ "Replay": "リプレイ",
113
+ "REPLAY": "リプレイ",
109
114
  "report": "報告",
110
115
  "report to": "報告先",
111
116
  "Role": "役割",
@@ -167,10 +172,11 @@
167
172
  "Reload": "再読み込み",
168
173
  "Open folder": "フォルダを開く",
169
174
  "custom character(s) loaded": "件のカスタムキャラクターを読み込みました",
175
+ "Demo": "デモ",
170
176
  "Drop your pixel art (PNG) in this folder to replace the characters:": "このフォルダにドット絵(PNG)を入れると、キャラクターを差し替えられます:",
171
177
  "lead.png = the lead · you.png = you · others = the sub-agents · name-talk.png = open mouth": "lead.png = メインエージェント · you.png = あなた · それ以外 = サブエージェント · name-talk.png = 口を開けた状態",
172
178
  "Downloading, you can carry on: system voices are used meanwhile.": "ダウンロード中です。そのまま作業を続けられます。それまではシステム音声を使います。",
173
- "Good-quality local voices, mostly English (one French voice). Free, nothing leaves your machine.": "品質の良いローカル音声(主に英語、フランス語は1種類のみ)。無料で、データがPCの外に出ることはありません。",
179
+ "Good-quality local voices, in English (French lines use your system voices). Free, nothing leaves your machine.": "品質の良いローカル音声(英語。フランス語はシステム音声を使います)。無料で、データがPCの外に出ることはありません。",
174
180
  "Installing…": "インストール中…",
175
181
  "Loading the model (downloaded on first use, about 305 MB)…": "モデルを読み込み中(初回使用時にダウンロード、約305 MB)…",
176
182
  "npm not found: install Node.js to enable this option.": "npm が見つかりません。このオプションを使うには Node.js をインストールしてください。",