@double_geste/agent-standup 1.24.0 → 1.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/index.html +53 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@double_geste/agent-standup",
3
- "version": "1.24.0",
3
+ "version": "1.24.2",
4
4
  "description": "Your Claude Code agents as a live video call, from the terminal: a local page in your browser, plus one-line status for status bars. Free, open source, zero token cost.",
5
5
  "bin": {
6
6
  "agent-standup": "bin/agent-standup.js"
package/public/index.html CHANGED
@@ -17,7 +17,9 @@
17
17
 
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
- .brand { display:flex; align-items:center; gap:9px; min-width:0 }
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 */
22
+ @media (max-width:480px) { header { gap:6px; padding:0 8px } .ctxp { display:none !important } } @media (max-width:420px) { .tok { display:none !important } }
21
23
  .logo { width:26px; height:26px; border-radius:8px; background:#d97757; display:grid; place-items:center; font-size:15px; flex:none }
22
24
  .brand b { font:700 14px "Comic Sans MS","Chalkboard SE",system-ui,sans-serif; white-space:nowrap }
23
25
  .brand small { display:block; color:var(--dim); font-size:11px; max-width:260px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
@@ -41,7 +43,7 @@
41
43
 
42
44
  /* ---------- layout ---------- */
43
45
  main { flex:1; display:flex; min-height:0; position:relative }
44
- #center { flex:1; display:flex; flex-direction:column; min-width:0; min-height:0; position:relative }
46
+ #center { flex:1; display:flex; flex-direction:column; min-width:0; min-height:0; position:relative; overflow:hidden }
45
47
  #stage { flex:1; overflow:auto; padding:14px; display:grid; gap:12px; align-content:start; grid-auto-flow:dense;
46
48
  grid-template-columns:repeat(var(--cols,4),1fr); grid-auto-rows:var(--rh,160px) }
47
49
  #tree { flex:1; overflow:auto; padding:16px 18px; display:none }
@@ -66,7 +68,7 @@
66
68
  .stc { font-size:11.5px; padding:2px 9px; border-radius:99px; background:rgba(14,15,24,.6); white-space:nowrap } .stc.working { color:var(--gold) } .stc.waiting { color:#8fd3ff } .stc.blocked { color:#fff; background:#c0392b; font-weight:700 } .stc.stalled { color:#1b1d27; background:#ffb347; font-weight:700 } .stc.done, .stc.turn { color:var(--ok) } .stc.idle { color:var(--dim) }
67
69
  .ctxp { font-size:11px; font-weight:600; padding:3px 9px; border-radius:99px; background:var(--panel2); white-space:nowrap; cursor:default } .ctxp.lo, .cx.lo { color:#7ee2a8 } .ctxp.mid, .cx.mid { color:#ffc857 } .ctxp.hi, .cx.hi { color:#fff; background:#c0392b }
68
70
  .cx:empty { display:none } .tag .cx { font-size:10px }
69
- #hl { display:none; flex-wrap:wrap; gap:6px; padding:5px 12px; border-top:1px solid var(--line); background:var(--panel) } body.replay #hl.has { display:flex } body.board #hl, body.tree #hl { display:none !important }
71
+ #hl { display:none; flex:0 0.01 auto; min-height:30px; max-height:66px; overflow-y:auto; scrollbar-width:thin; flex-wrap:wrap; gap:6px; padding:5px 12px; border-top:1px solid var(--line); background:var(--panel) } body.replay #hl.has { display:flex } body.board #hl, body.tree #hl { display:none !important }
70
72
  .hlc { font-size:11.5px; padding:3px 10px; border-radius:99px; background:var(--panel2); border:1px solid var(--line); color:var(--text) } .hlc:hover { border-color:var(--accent) }
71
73
  .tbadge .l { color:#ff9f1c } .tbadge span { font-size:11px } .tbadge .q { color:#b4bcd4 }
72
74
  .ic:active, .btn:active { transform:scale(.96) } .rr, .ic, .bcard { transition:background .15s var(--ease), border-color .15s var(--ease), transform .15s var(--ease) }
@@ -84,7 +86,7 @@
84
86
  .prop.waiting { animation:tick 2.4s ease-in-out infinite } @keyframes tick { 50% { transform:rotate(180deg) } }
85
87
  .prop.cheer { animation:cheer 1.1s ease-out both } @keyframes cheer { 0% { transform:scale(.3) translateY(10px); opacity:0 } 40% { transform:scale(1.25); opacity:1 } 100% { transform:translateY(-14px); opacity:0 } }
86
88
  /* ---------- screen sharing (presentation mode) ---------- */
87
- #present { display:none; flex:none; position:relative; height:clamp(190px,46vh,430px); background:radial-gradient(ellipse at 50% 0,#1b2033,#0b0d14); border-bottom:1px solid var(--line); padding:12px 16px; justify-content:center }
89
+ #present { display:none; flex:0 1 auto; min-height:96px; overflow:hidden; position:relative; height:clamp(190px,46vh,430px); background:radial-gradient(ellipse at 50% 0,#1b2033,#0b0d14); border-bottom:1px solid var(--line); padding:12px 16px; justify-content:center }
88
90
  #present.on { display:flex; animation:fadein .3s } #present.pic { height:clamp(220px,60vh,640px) }
89
91
  .pwin { flex:0 1 900px; display:flex; flex-direction:column; border-radius:10px; overflow:hidden; background:#0d1117; box-shadow:0 18px 50px rgba(0,0,0,.6), 0 0 0 2px rgba(240,160,32,.55) }
90
92
  .pbar { display:flex; align-items:center; gap:6px; padding:7px 10px; background:#1c1f2b; font-size:11.5px; color:#9aa0b4 } .pbar i { width:10px; height:10px; border-radius:50%; display:inline-block }
@@ -97,7 +99,7 @@
97
99
  .tl.new { animation:lineIn .28s ease-out } @keyframes lineIn { from { opacity:0; transform:translateY(5px) } }
98
100
  .cursor { display:inline-block; width:7px; height:14px; background:#c9d1d9; vertical-align:-2px; animation:blinkc 1s steps(1) infinite } @keyframes blinkc { 50% { opacity:0 } }
99
101
  #pstat { flex:none; padding:4px 12px; background:#161925; color:#8b95b0; font:11.5px ui-monospace,Menlo,monospace; border-top:1px solid #232637; white-space:nowrap; overflow:hidden; text-overflow:ellipsis }
100
- #ptodo { flex:0 0 230px; margin-left:12px; align-self:flex-start; max-height:calc(100% - 140px); overflow:hidden; background:rgba(20,22,32,.92); border:1px solid #2a2d3d; border-radius:10px; padding:8px 10px; font-size:12px }
102
+ #ptodo { flex:0 0 230px; margin-left:12px; align-self:flex-start; position:relative; max-height:100%; overflow-y:auto; overscroll-behavior:contain; scrollbar-width:thin; background:rgba(20,22,32,.92); border:1px solid #2a2d3d; border-radius:10px; padding:8px 10px; font-size:12px }
101
103
  #ptodo h5 { margin:0 0 6px; font-size:10.5px; letter-spacing:.08em; text-transform:uppercase; color:var(--dim) } .td { display:flex; gap:7px; padding:2px 0; color:#c3c6d4 } .td i { font-style:normal; flex:none } .td.completed { color:#6b7089; text-decoration:line-through } .td.completed i { color:var(--ok) } .td.in_progress { color:#fff } .td.in_progress i { color:var(--gold) }
102
104
  .tl.out { color:#aab2c5 } .tl.err { color:#ff7b72 } .tl.dim { color:#6b7089 } .tl.run { color:#ffd24a } .tl.cmd { margin-top:5px }
103
105
  .ln.ctx b { width:34px; text-align:right; color:#4b5069 } .ln.ctx { color:#d3d7e6 } .ln.err { color:#ff7b72 } .ln.dim { color:#6b7089 } .ln.run { color:#ffd24a }
@@ -108,6 +110,7 @@
108
110
  #present.has-todo #pcam { right:270px } #pcam { position:absolute; right:28px; bottom:22px; width:158px; height:118px; z-index:2; cursor:default; box-shadow:0 8px 24px rgba(0,0,0,.6) }
109
111
  #pcap { display:none; position:absolute; left:24px; right:200px; bottom:20px; z-index:2; background:rgba(10,11,16,.84); backdrop-filter:blur(6px); border-radius:10px; padding:6px 12px; font-size:13px } #pcap:not(:empty) { display:block }
110
112
  body.narrow #present { height:clamp(150px,36vh,260px) } body.narrow #pcam { width:110px; height:82px } body.narrow #pcap { right:150px }
113
+ body.narrow #ptodo { display:none } body.narrow #present.has-todo #pcam { right:28px } /* too narrow for a terminal and a task list side by side: the terminal keeps the room */
111
114
  #gantt { display:none; flex:none; width:100%; background:#0f1118; border-top:1px solid var(--line); cursor:pointer }
112
115
  body.replay #gantt { display:block }
113
116
  aside { display:none; width:320px; flex:none; border-left:1px solid var(--line); background:var(--panel); flex-direction:column; min-height:0 }
@@ -122,7 +125,7 @@
122
125
  body.replay footer { display:flex }
123
126
  input[type=range] { accent-color:var(--accent) } #seek { flex:1 }
124
127
  @media (max-width:760px) { aside { position:absolute; right:0; top:0; bottom:0; z-index:6; box-shadow:-10px 0 30px rgba(0,0,0,.5) } .brand small, #viewSeg { display:none } }
125
- @media (max-width:420px) { .brand b { display:none } }
128
+ @media (max-width:540px) { .brand b { display:none } }
126
129
  @media (max-width:360px) { .logo { display:none } }
127
130
  @media (max-height:640px) { #gantt { display:none !important } }
128
131
 
@@ -142,10 +145,11 @@
142
145
  .status.working { color:var(--gold) } .status.done { color:var(--ok) }
143
146
  .status.waiting { color:#8fd3ff } .status.blocked { color:#fff; background:#c0392b; font-weight:700 } .status.stalled { color:#1b1d27; background:#ffb347; font-weight:700 }
144
147
  .tile.waiting { border-color:rgba(110,190,255,.85) } .tile.blocked { border-color:#ff5252 !important; animation:slowp 1.2s ease-in-out infinite } .tile.stalled { border-color:#ffb347 !important }
145
- .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 }
146
- .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) }
147
- @media (max-width:1180px) { .board em { display:none } .meet { display:none } }
148
- @media (max-width:640px) { .board { display:none } }
148
+ /* 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 */
149
+ #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 }
150
+ #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) }
151
+ @media (max-width:1180px) { #board em { display:none } .meet { display:none } }
152
+ @media (max-width:640px) { #board { display:none } }
149
153
  .bubble { display:none; position:absolute; top:6px; left:6px; right:62px; 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); max-height:58%; overflow:hidden }
150
154
  .tile.bye .bubble { display:block; font-size:13.5px; animation:fadein .25s } .tile.speaking .bubble { display:block; font-size:14.5px; max-height:50% } .bubble small { display:block; color:#6a6f85; font-size:10.5px }
151
155
  .ticker { display:none; position:absolute; left:6px; right:6px; bottom:30px; font:10.5px/1.3 ui-monospace,Menlo,monospace; color:#fff; background:rgba(15,20,35,.8); border-radius:5px; padding:2px 7px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis }
@@ -182,7 +186,7 @@
182
186
  #gate h2, #empty h2 { margin:0; font:800 22px "Comic Sans MS","Chalkboard SE",system-ui,sans-serif } #gate p, #empty p { margin:0; color:var(--dim); max-width:380px }
183
187
  .cta { background:var(--accent); color:#0b1020; font-weight:700; padding:10px 22px; border-radius:99px; font-size:14px } .cta:hover { filter:brightness(1.1) }
184
188
  .btn { background:var(--panel2); border:1px solid var(--line); padding:6px 12px; border-radius:8px } .btn:hover { border-color:var(--accent) }
185
- #menu { position:absolute; top:52px; right:10px; z-index:12; width:280px; background:var(--panel); border:1px solid var(--line); border-radius:12px; box-shadow:0 14px 40px rgba(0,0,0,.55); padding:6px; display:none }
189
+ #menu { position:absolute; top:52px; right:10px; z-index:12; width:280px; background:var(--panel); border:1px solid var(--line); border-radius:12px; box-shadow:0 14px 40px rgba(0,0,0,.55); padding:6px; display:none; max-height:calc(100vh - 62px); max-height:calc(100dvh - 62px); overflow-y:auto; overscroll-behavior:contain; scrollbar-width:thin } /* never taller than the window: it scrolls instead of being cut off */
186
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 }
187
191
  .glink { color:var(--dim); font-size:12.5px; text-decoration:underline; text-underline-offset:3px } .glink:hover { color:#ffd24a }
188
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) }
@@ -221,7 +225,7 @@
221
225
  <body>
222
226
  <header>
223
227
  <div class="brand"><div class="logo">🎥</div><div><b>Agent Standup</b><small id="subtitle"></small></div></div>
224
- <span id="pill">REPLAY</span><button id="tok" class="tok" style="display:none"></button><button id="ctxp" class="ctxp" style="display:none"></button><span id="board" class="board" aria-live="polite"></span><span id="meet" class="meet"></span>
228
+ <span id="pill">REPLAY</span><button id="tok" class="tok" style="display:none"></button><button id="ctxp" class="ctxp" style="display:none"></button><span id="board" aria-live="polite"></span><span id="meet" class="meet"></span>
225
229
  <div class="sp"></div>
226
230
  <div class="seg" id="viewSeg"><button data-v="call" class="on" data-fr="Appel" data-en="Call">Appel</button><button data-v="tree" data-fr="Arbre" data-en="Tree">Arbre</button><button data-v="board" data-fr="Sessions" data-en="Sessions">Sessions</button></div>
227
231
  <button class="ic" id="bPlay" title="Pause / Lecture (espace)">⏸</button>
@@ -1116,8 +1120,12 @@ function renderPresent() {
1116
1120
  if (shown) { if (pb.dataset.img !== shown) pb.innerHTML = m.imgs.map((u) => `<img src="${esc(u)}" alt="">`).join('') + (m.more > 0 ? `<span class="pmore">+${m.more}</span>` : ''); } // the same pictures are left in place: redrawing them would make them blink
1117
1121
  else pb.innerHTML = (m.view === 'editor' ? `<div class="ptab"><span>${esc(m.tab.file)}</span><em>${esc(m.tab.path)}</em></div>` : '') + m.lines.map((l) => lineHtml(l, m.view)).join('') + (m.view === 'term' ? '<div class="tl"><span class="ps">❯</span> <span class="cursor"></span></div>' : '');
1118
1122
  pb.dataset.img = shown; pb.scrollTop = m.view === 'term' ? pb.scrollHeight : 0; $('#pstat').textContent = m.stat;
1119
- const td = S.todos[id], el = $('#ptodo'); el.style.display = td && td.length ? '' : 'none'; $('#present').classList.toggle('has-todo', !!(td && td.length));
1120
- if (td) el.innerHTML = `<h5>${L('Tâches', 'Tasks')} · ${td.filter((x) => x.s === 'completed').length}/${td.length}</h5>` + td.map((x) => `<div class="td ${x.s}"><i>${x.s === 'completed' ? '☑' : x.s === 'in_progress' ? '◐' : '☐'}</i>${esc(x.c)}</div>`).join('');
1123
+ const td = S.todos[id], el = $('#ptodo'), tk = td && td.length ? prefs.lang + '\n' + td.map((x) => x.s + ' ' + x.c).join('\n') : ''; el.style.display = tk ? '' : 'none'; $('#present').classList.toggle('has-todo', !!tk);
1124
+ if (tk && el.dataset.k !== tk) { // redrawn only when it changes, so it stays where you scrolled it; the task in progress is brought into view
1125
+ el.innerHTML = `<h5>${L('Tâches', 'Tasks')} · ${td.filter((x) => x.s === 'completed').length}/${td.length}</h5>` + td.map((x) => `<div class="td ${x.s}"><i>${x.s === 'completed' ? '☑' : x.s === 'in_progress' ? '◐' : '☐'}</i>${esc(x.c)}</div>`).join('');
1126
+ const cur = el.querySelector('.in_progress'); el.scrollTop = cur ? Math.max(0, cur.offsetTop - el.clientHeight / 2) : 0;
1127
+ }
1128
+ el.dataset.k = tk;
1121
1129
  $('#pbadge').textContent = `🖥 ${p.name} ${L('partage son écran', 'is sharing their screen')}`;
1122
1130
  if (S.camFor !== id) { $('#pcam').innerHTML = avatarSVG(p, undefined, 'c' + p.gid + '_') + `<div class="tag"><b>${esc(p.name)}</b></div>`; S.camFor = id; }
1123
1131
  $('#pcam').classList.toggle('speaking', S.speakers.has(id));
@@ -1184,7 +1192,7 @@ const mouths = new Map();
1184
1192
  function startTalk(id, text, label) {
1185
1193
  const t = $('#t-' + id); if (!t) return;
1186
1194
  if (S.present && S.present !== id) hidePresent();
1187
- S.spokeAt = performance.now(); S.fill = 0;
1195
+ S.spokeAt = performance.now(); S.fill = 0; S.mused = false;
1188
1196
  if (!S.speakers.size && !(S.focusLock > Date.now())) setFocus(id); S.gone.delete(id); if (S.doneAt[id] != null) S.doneAt[id] = S.clock; t.style.display = ''; t.classList.add('speaking'); S.speakers.add(id); S.say[id] = { text, label }; AUD.duck(true);
1189
1197
  const shown0 = pieces(text), first = shown0.length > 1 ? shown0[0] : text; const b = t.querySelector('.bubble'); b.innerHTML = (label ? `<small>${esc(label)}</small>` : '') + esc(first);
1190
1198
  if (S.present === id) { $('#pcap').textContent = first; $('#pcam').classList.add('speaking'); }
@@ -1385,11 +1393,16 @@ function ackLine(ev) {
1385
1393
  if (line === S.lastAck) line = pool[(seed + 1) % pool.length]; S.lastAck = line; return line;
1386
1394
  }
1387
1395
  async function react(ev, token) {
1388
- 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;
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;
1389
1397
  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]; }
1390
- else if (ev.kind === 'report') { if (!prefs.epi && seed % 100 > 35) return; if (seed % 100 > 60) return; const others = Object.keys(S.status).concat(['main']).filter((x) => x !== ev.agent && S.agents[x] && !S.gone.has(x)); who = others[(seed >>> 3) % others.length]; line = quipMem.get(baseText(ev, 160)) || R.done[seed % R.done.length]; }
1398
+ else if (ev.kind === 'report') {
1399
+ 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
1400
+ if (r <= joke) { const others = Object.keys(S.status).concat(['main']).filter((x) => x !== ev.agent && S.agents[x] && !S.gone.has(x)); who = others[(seed >>> 3) % others.length]; line = quipMem.get(baseText(ev, 160)) || R.done[seed % R.done.length]; }
1401
+ else if (prefs.banter && r <= joke + 25 && ev.to !== 'user' && S.agents[ev.to] && !S.gone.has(ev.to)) { who = ev.to; line = varied('thanks', seed >>> 3); thanks = true; }
1402
+ else return;
1403
+ }
1391
1404
  else return;
1392
- if (!S.agents[who]) return; startTalk(who, line, ev.kind === 'order' && prefs.banter ? '💬' : '🎬'); await speakLine(line, S.agents[who], true, token); stopTalk(who);
1405
+ if (!S.agents[who]) return; startTalk(who, line, thanks || (ev.kind === 'order' && prefs.banter) ? '💬' : '🎬'); await speakLine(line, S.agents[who], true, token); stopTalk(who);
1393
1406
  }
1394
1407
  // Small talk: when nobody has said anything for a while, someone fills the silence the way people do in video calls. At most three per silence, further and further apart; any other line starts the count again.
1395
1408
  const FILL = {
@@ -1400,13 +1413,29 @@ const FILL = {
1400
1413
  busy: ['Toujours là, je suis dessus.', 'Pardon, j’étais en muet.', 'Petit point : j’avance.', 'Pas de point bloquant de mon côté.', 'Vous m’entendez ?', 'C’est moi, ou c’est calme ?', 'Je vous fais un retour dans une minute.'],
1401
1414
  wait: ['Vous m’entendez ?', 'Vous êtes toujours là ?', 'Allô ? Je crois que ton micro est coupé.', 'On a perdu quelqu’un ?', 'C’est ma connexion ?', 'On en reparle en off ?', 'On refait un point quand tu veux.', 'Je vous rends une minute.', 'D’autres sujets ?', 'Pardon, vas-y. Non, toi d’abord.'] },
1402
1415
  };
1416
+ // A little more talk between agents: a word of thanks when a report comes in, a word from whoever has been thinking for a long time.
1417
+ const TALK = {
1418
+ en: { thanks: ['Thanks, that helps.', 'Got it, thanks.', 'Noted. I’ll take it from here.', 'Perfect, that’s what I needed.', 'Thanks, good work.', 'Good to know, thanks.', 'Understood, thank you.', 'Great, one less thing to worry about.'],
1419
+ think: ['Hmm, let me think.', 'Give me a second, I’m thinking.', 'Interesting… let me think this through.', 'One moment, I’m working it out.', 'Hang on, I want to get this right.', 'Let me double-check something.', 'Still thinking… almost there.', 'That one needs a bit of thought.'] },
1420
+ fr: { thanks: ['Merci, ça m’aide.', 'Bien reçu, merci.', 'C’est noté. Je prends la suite.', 'Parfait, c’est ce qu’il me fallait.', 'Merci, bon boulot.', 'Bon à savoir, merci.', 'Compris, merci.', 'Super, un souci de moins.'],
1421
+ think: ['Hmm, laissez-moi réfléchir.', 'Une seconde, je réfléchis.', 'Intéressant… je creuse ça.', 'Un instant, je fais le point.', 'Attendez, je veux être sûr.', 'Je vérifie un truc.', 'Je réfléchis encore… j’y suis presque.', 'Ça demande un peu de réflexion.'] },
1422
+ };
1423
+ function varied(kind, seed) { const P = TALK[FR() ? 'fr' : 'en'][kind]; let line = P[seed % P.length]; if (line === S.lastTalk) line = P[(seed + 1) % P.length]; return (S.lastTalk = line); } // never the same line twice in a row
1424
+ // Who is thinking once this event has happened: the agent whose tool just answered, the lead after your message or a report, a sub-agent that just got its order. After a line or a tool call, nobody is.
1425
+ const thinkerOf = (e) => (e.kind === 'results' ? e.agent : e.kind === 'user' ? 'main' : e.kind === 'report' || e.kind === 'finish' || e.kind === 'order' ? e.to : null);
1426
+ const before = (i) => { let k = i - 1; while (k >= 0 && (S.events[k].kind === 'usage' || S.events[k].kind === 'compact')) k--; return S.events[k]; };
1427
+ async function ponder(i, token) { // replay: an agent that really took a long time (40 s or more) before this line or call says so first, about every other time
1428
+ const ev = S.events[i], p = before(i); if (!prefs.banter || S.boost > 1 || S.hurry || !p || !S.agents[ev.agent] || thinkerOf(p) !== ev.agent || ev.ts - p.ts < 40000 || hash('t' + ev.ts) % 100 > 55) return;
1429
+ const line = varied('think', hash(String(ev.ts))); startTalk(ev.agent, line, '💭'); await speakLine(line, S.agents[ev.agent], true, token); stopTalk(ev.agent);
1430
+ }
1403
1431
  async function smallTalk(token) {
1404
1432
  const n = S.fill || 0, last = S.events[S.events.length - 1]; if (!prefs.banter || n > 2 || !last || S.speakers.size || S.hurry || S.boost > 1 || (document.hidden && !S.exporting)) return;
1405
- if (performance.now() - S.spokeAt < (S.live ? 45000 : 12000 / spd()) * [1, 2.5, 6][n] || (S.live && Date.now() - last.ts > 6e5)) return; // live: a session untouched for ten minutes is a call that is over
1406
- const here = Object.keys(S.agents).filter((id) => id !== 'user' && $('#t-' + id) && $('#t-' + id).style.display !== 'none'), busy = here.filter((id) => agentState(id, Date.now()).k === 'working'), pick = (a) => a[Math.floor(Math.random() * a.length)];
1407
- const who = S.present && here.includes(S.present) ? S.present : pick(busy.length ? busy : here), P = FILL[FR() ? 'fr' : 'en'][who === S.present ? 'share' : busy.length ? 'busy' : 'wait']; if (!who) return; // whoever shares their screen speaks, so the screen stays up
1408
- let line = pick(P); if (line === S.lastFill) line = P[(P.indexOf(line) + 1) % P.length]; S.lastFill = line;
1409
- startTalk(who, line, '💬'); S.fill = n + 1; await speakLine(line, S.agents[who], true, token); stopTalk(who);
1433
+ const quiet = performance.now() - S.spokeAt, here = Object.keys(S.agents).filter((id) => id !== 'user' && $('#t-' + id) && $('#t-' + id).style.display !== 'none'), pick = (a) => a[Math.floor(Math.random() * a.length)];
1434
+ const p = before(S.events.length), muser = S.live && !n && !S.mused && p ? thinkerOf(p) : null, think = !!muser && here.includes(muser) && quiet > 6000 && Date.now() - p.ts > 20000 && Date.now() - p.ts < 180000; // live: 20 s without a word from whoever is thinking, once per silence
1435
+ if (!think && (quiet < (S.live ? 45000 : 12000 / spd()) * [1, 2.5, 6][n] || (S.live && Date.now() - last.ts > 6e5))) return; // live: a session untouched for ten minutes is a call that is over
1436
+ const busy = here.filter((id) => agentState(id, Date.now()).k === 'working'), who = think ? muser : S.present && here.includes(S.present) ? S.present : pick(busy.length ? busy : here); if (!who) return; // whoever shares their screen speaks, so the screen stays up
1437
+ const P = FILL[FR() ? 'fr' : 'en'][who === S.present ? 'share' : busy.length ? 'busy' : 'wait']; let line = think ? varied('think', Math.floor(Math.random() * 97)) : pick(P); if (!think && line === S.lastFill) line = P[(P.indexOf(line) + 1) % P.length]; if (!think) S.lastFill = line;
1438
+ startTalk(who, line, think ? '💭' : '💬'); if (think) S.mused = true; else S.fill = n + 1; await speakLine(line, S.agents[who], true, token); stopTalk(who);
1410
1439
  }
1411
1440
  async function card(text, sub, ms = 2500) {
1412
1441
 
@@ -1438,6 +1467,7 @@ async function run(i) {
1438
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
1439
1468
  S.boost = S.hurry || (S.live && S.events.length - i > 60) ? 3 : 1;
1440
1469
  const ev = S.events[i]; S.idx = i; tally(ev);
1470
+ if (!S.live && (ev.kind === 'say' || ev.kind === 'report' || ev.kind === 'order' || ev.kind === 'tools')) { await ponder(i, token); if (token !== S.token) return; }
1441
1471
  if (ev.kind === 'usage' || ev.kind === 'compact') { if (ev.kind === 'compact' && S.live && prefs.banter) toast(`${nameOf(ev.agent)}: ${L('contexte compacté 🗜', 'context compacted 🗜')}`); i++; continue; }
1442
1472
  if (ev.kind === 'finish') { applyStatus(ev); paintStatus(); i++; continue; }
1443
1473
  if (ev.kind === 'results') { applyResults(ev, S.boost > 1); await sleep((S.boost === 1 && S.present === ev.agent && ev.items.some((it) => it.img) ? (S.live ? 900 : 1800) : S.demo ? 900 : S.live ? 30 : 80) / spd()); i++; continue; } // a picture gets a moment on screen