@double_geste/agent-standup 1.25.7 → 1.25.9
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/kokoro-worker.js +10 -2
- package/package.json +1 -1
- package/public/index.html +86 -15
- package/public/locales/da.json +0 -1
- package/public/locales/de.json +0 -1
- package/public/locales/en.json +0 -1
- package/public/locales/es.json +0 -1
- package/public/locales/ja.json +0 -1
- package/public/locales/pt-BR.json +0 -1
- package/public/locales/zh-CN.json +0 -1
- package/public/locales/zh-TW.json +0 -1
- package/server.js +8 -6
- package/examples/characters/README.txt +0 -5
- package/examples/characters/buddy.png +0 -0
- package/examples/characters/lead-talk.png +0 -0
- package/examples/characters/lead.png +0 -0
package/kokoro-worker.js
CHANGED
|
@@ -11,11 +11,19 @@ function wav(samples, rate) { // Float32 in [-1, 1] → 16-bit mono WAV
|
|
|
11
11
|
for (let i = 0; i < n; i++) { const s = Math.max(-1, Math.min(1, samples[i])); buf.writeInt16LE(Math.round(s < 0 ? s * 32768 : s * 32767), 44 + i * 2); }
|
|
12
12
|
return buf;
|
|
13
13
|
}
|
|
14
|
+
const FILE = { q8: 'model_quantized', q4: 'model_q4', fp32: 'model', fp16: 'model_fp16', q4f16: 'model_q4f16' }; // the file transformers.js keeps for each dtype, under models/<MODEL>/onnx/
|
|
14
15
|
async function main() {
|
|
15
|
-
const dir = process.env.AGENT_STANDUP_KOKORO_DIR, dtype = process.env.AGENT_STANDUP_KOKORO_DTYPE || '
|
|
16
|
+
const dir = process.env.AGENT_STANDUP_KOKORO_DIR, dtype = process.env.AGENT_STANDUP_KOKORO_DTYPE || 'q4';
|
|
16
17
|
const req = createRequire(path.join(dir, 'node_modules', 'x.js')); // resolve from the install folder, not from the extension
|
|
17
18
|
const tf = req('@huggingface/transformers'); tf.env.cacheDir = path.join(dir, 'models');
|
|
18
|
-
const { KokoroTTS } = req('kokoro-js')
|
|
19
|
+
const { KokoroTTS } = req('kokoro-js'), load = () => KokoroTTS.from_pretrained(MODEL, { dtype, device: 'cpu' });
|
|
20
|
+
let tts; try { tts = await load(); } catch (e) {
|
|
21
|
+
// transformers.js writes the model straight to its final name and never checks it again: a download cut short (the worker killed while the model
|
|
22
|
+
// was being fetched, e.g. by a reload of the window) leaves a file that fails to load every time after. Drop this model's files and fetch them once more.
|
|
23
|
+
const onnx = path.join(dir, 'models', ...MODEL.split('/'), 'onnx'), name = FILE[dtype] || 'model'; let dropped = 0;
|
|
24
|
+
for (const ext of ['.onnx', '.onnx_data']) { try { fs.unlinkSync(path.join(onnx, name + ext)); dropped++; } catch {} }
|
|
25
|
+
if (!dropped) throw e; console.error('kokoro: the ' + dtype + ' model could not be loaded (' + String((e && e.message) || e).slice(0, 80) + '); fetching it again'); tts = await load();
|
|
26
|
+
}
|
|
19
27
|
send({ ready: true });
|
|
20
28
|
for await (const line of readline.createInterface({ input: process.stdin })) {
|
|
21
29
|
let m; try { m = JSON.parse(line); } catch { continue; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@double_geste/agent-standup",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.9",
|
|
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
|
@@ -160,6 +160,8 @@
|
|
|
160
160
|
.tile.speaking, .tile.bye { overflow:visible; z-index:4 } .tile > svg:first-child { border-radius:13px } .tile.speaking .prop { left:auto; right:8px; top:52px }
|
|
161
161
|
.tile.focus.speaking:has(.status:not(:empty)) .bubble { top:32px } /* the spotlight has room: there the badge stays visible above the bubble */
|
|
162
162
|
body:not(.tiny) #center:has(#pcap:not(:empty)) .tile.presenter:not(.bye) .bubble { display:none } /* the presenter's line is the caption of the shared screen: not said twice */
|
|
163
|
+
.tile.musing:not(.speaking):not(.bye) .bubble { display:block; top:auto; bottom:36px; max-width:min(80%, 320px); font-size:12px; font-style:italic; color:#40455c; background:rgba(255,255,255,.88); border-radius:14px; box-shadow:0 2px 8px rgba(0,0,0,.28); animation:fadein .5s } /* an inner thought: not said, so lower, lighter, and never over the status at the top */
|
|
164
|
+
.tile.focus.musing:not(.speaking):not(.bye) .bubble { bottom:46px; font-size:13px } .tile.musing:not(.speaking):not(.bye) .bubble small { display:inline; margin-right:5px; font-style:normal }
|
|
163
165
|
.tile.bye .bubble { display:block; font-size:13.5px; animation:fadein .25s } .tile.speaking .bubble { display:block; font-size:13px } .tile.focus.speaking .bubble { font-size:14.5px } .bubble small { display:block; color:#6a6f85; font-size:10.5px }
|
|
164
166
|
.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 }
|
|
165
167
|
.ticker.on { display:block } .ticker i { font-style:normal; color:#ffd24a } .ticker.flash { animation:flash .25s } @keyframes flash { from { background:#3b5bdb } }
|
|
@@ -291,7 +293,7 @@
|
|
|
291
293
|
<select id="liveProj" style="display:none;margin-top:6px" title="Project to follow"></select>
|
|
292
294
|
<select id="sessions" style="display:none"></select></div>
|
|
293
295
|
<h4 data-fr="Personnages" data-en="Characters">Personnages</h4>
|
|
294
|
-
<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="
|
|
296
|
+
<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>
|
|
295
297
|
<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>
|
|
296
298
|
<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>
|
|
297
299
|
<h4 data-fr="Outils" data-en="Tools">Outils</h4>
|
|
@@ -385,7 +387,7 @@ const mix = (a, b, t) => { const A = hexRGB(a), B = hexRGB(b); return '#' + A.ma
|
|
|
385
387
|
let CHARS = { lead: null, you: null, pool: [], dir: '', n: 0 };
|
|
386
388
|
const toData = (url) => fetch(url).then((r) => r.blob()).then((b) => new Promise((res) => { const f = new FileReader(); f.onload = () => res(f.result); f.readAsDataURL(b); }));
|
|
387
389
|
async function loadChars() {
|
|
388
|
-
if (!['none', 'folder', '
|
|
390
|
+
if (!['none', 'folder', 'cats'].includes(prefs.charPack)) { // drawn characters by default; someone who already had images in their folder keeps them (this is also where a choice of the starter pack, no longer shipped, ends up)
|
|
389
391
|
let n = 0; try { n = ((await (await fetch('/api/characters')).json()).chars || []).length; } catch {} prefs.charPack = n ? 'folder' : 'none'; savePrefs(); const sel = $('#charPack'); if (sel) sel.value = prefs.charPack;
|
|
390
392
|
}
|
|
391
393
|
const pack = prefs.charPack, next = { lead: null, you: null, pool: [], dir: '', n: 0, pack };
|
|
@@ -634,10 +636,10 @@ const AUD = {
|
|
|
634
636
|
},
|
|
635
637
|
join() { this.tone(523, .14, 'triangle', .1); this.tone(784, .2, 'triangle', .1, .12); },
|
|
636
638
|
ding() { this.tone(988, .5, 'triangle', .14); this.tone(1318, .5, 'sine', .06, .02); },
|
|
637
|
-
murmur(pitch, dur) { // cartoon mumble, used when there is no real voice
|
|
639
|
+
murmur(pitch, dur, vol = 1) { // cartoon mumble, used when there is no real voice (vol: lower for a thought that escapes)
|
|
638
640
|
if (!this.ctx || !prefs.voice) return; const c = this.ctx, t = c.currentTime, o = c.createOscillator(); o.type = 'sawtooth';
|
|
639
641
|
const bp = c.createBiquadFilter(); bp.type = 'bandpass'; bp.Q.value = 3; const g = c.createGain();
|
|
640
|
-
g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(0.08, t + 0.05); g.gain.setValueAtTime(0.08, t + dur - 0.1); g.gain.linearRampToValueAtTime(0, t + dur);
|
|
642
|
+
g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(0.08 * vol, t + 0.05); g.gain.setValueAtTime(0.08 * vol, t + dur - 0.1); g.gain.linearRampToValueAtTime(0, t + dur);
|
|
641
643
|
const base = 85 + pitch * 70; for (let x = 0; x < dur; x += 0.11) { o.frequency.setValueAtTime(base * (0.8 + Math.random() * 0.55), t + x); bp.frequency.setValueAtTime(500 + Math.random() * 900, t + x); }
|
|
642
644
|
o.connect(bp).connect(g).connect(this.voice); o.start(t); o.stop(t + dur + 0.05); this.live.add(o); o.onended = () => this.live.delete(o); return o;
|
|
643
645
|
},
|
|
@@ -781,7 +783,7 @@ function warmTTS(i) { // called by run() before each line (i is that line) and
|
|
|
781
783
|
else if (isSub(e.agent)) { while (j < Math.min(len, k + 28) && group.length < 3) { const e2 = S.events[j]; if (quiet(e2)) j++; else if (e2.kind !== 'order' && isSub(e2.agent) && !group.some((g) => g.agent === e2.agent)) { group.push(e2); j++; } else break; } }
|
|
782
784
|
const pl = paceAt(k), many = e.kind === 'order' && group.length > 1, rate = TTS.rateAt(many || !S.live ? k : j - 1, pl.boost);
|
|
783
785
|
for (const g of many ? [e] : group) { // orders in a row: only the first is read out
|
|
784
|
-
n++; const p = S.agents[g.agent], bt = baseText(g); if (pl.hurry || !p || (FR() && !trMem.has(bt) && langOf(bt) !== 'fr')) continue; // not translated yet: what will be said is not known
|
|
786
|
+
n++; const p = S.agents[g.agent], bt = baseText(g); if ((pl.hurry && g.kind !== 'user') || !p || (FR() && !trMem.has(bt) && langOf(bt) !== 'fr')) continue; // not translated yet: what will be said is not known
|
|
785
787
|
const said = clipSpeech(talkLine(g), pl.pace), text = speakable(said), voice = TTS.voiceFor(p, sessFr(said, g)); if (text && voice) jobs.push({ k, text, voice, rate });
|
|
786
788
|
}
|
|
787
789
|
k = j;
|
|
@@ -930,10 +932,10 @@ function sessFr(text, ev) { // for the lines that come from the session (ev: t
|
|
|
930
932
|
}
|
|
931
933
|
return (lo ?? sessLang() ?? (FR() ? 'fr' : 'en')) === 'fr';
|
|
932
934
|
}
|
|
933
|
-
async function speakLine(text, p, primary, token, fr = frVoice(text)) {
|
|
935
|
+
async function speakLine(text, p, primary, token, fr = frVoice(text), always = false) { // always: your own lines are voiced even when the call is behind
|
|
934
936
|
captionRun(p.id, text, estMs(text));
|
|
935
937
|
if (AUD.off() || !prefs.voice) { if (!AUD.off() && prefs.office) AUD.murmur(0.5 + (p.vi % 5) / 10, estMs(text) / 1000); return sleep(estMs(text)); }
|
|
936
|
-
if (S.hurry) return sleep(estMs(text));
|
|
938
|
+
if (S.hurry && !always) return sleep(estMs(text));
|
|
937
939
|
if ((prefs.vstyle === 'animal' || prefs.vstyle === 'sims') && AUD.ctx) { try { await speakBabble(text, p, primary ? 1 : 0.5, token); return; } catch {} }
|
|
938
940
|
if (TTS.server && AUD.ctx) { try { await speakServer(text, p, primary ? 1 : 0.5, token, fr); return; } catch {} }
|
|
939
941
|
if (primary && synth && !EMBED && (await speakBrowser(text, p, token, fr))) return;
|
|
@@ -1009,7 +1011,7 @@ const applyStatus = (ev) => {
|
|
|
1009
1011
|
if (ev.kind === 'user' || (ev.agent === 'main' && (ev.kind === 'say' || ev.kind === 'tools' || (ev.kind === 'order' && !ev.wf)))) S.turnOver = !!ev.end; // Claude Code marks the message that closes the lead's turn; anything after it opens the next one (not the agents a workflow launches by itself)
|
|
1010
1012
|
};
|
|
1011
1013
|
function recomputeStatus(upTo) {
|
|
1012
|
-
S.term = {}; S.todos = {}; S.run = {}; S.act = {}; S.usage = {}; S.errs = {}; S.lastAct = {}; S.waitOn = {}; S.orderTs = {}; S.prevState = {}; S.lastTool = {}; S.mainBusyAt = 0; S.turnOver = false; S.ctx = {}; S.model = {}; S.compacts = {}; S.ctxWarned = {}; S.rep = {}; S.loops = {}; S.recomputing = true; document.querySelectorAll('.tile').forEach((t) => { clearTimeout(t._byeT); t.classList.remove('bye'); }); hidePresent();
|
|
1014
|
+
S.term = {}; S.todos = {}; S.run = {}; S.act = {}; S.usage = {}; S.errs = {}; S.lastAct = {}; S.waitOn = {}; S.orderTs = {}; S.prevState = {}; S.lastTool = {}; S.mainBusyAt = 0; S.turnOver = false; S.th = {}; S.ctx = {}; S.model = {}; S.compacts = {}; S.ctxWarned = {}; S.rep = {}; S.loops = {}; S.recomputing = true; document.querySelectorAll('.tile').forEach((t) => { clearTimeout(t._byeT); t.classList.remove('bye'); }); hidePresent();
|
|
1013
1015
|
S.status = {}; S.tc = {}; S.doneAt = {}; S.gone.clear(); S.leaving.clear(); document.querySelectorAll('.tile.vanishing').forEach((t) => t.classList.remove('vanishing')); const doneIdx = {}; document.querySelectorAll('.ticker').forEach((x) => { x.classList.remove('on'); x.innerHTML = ''; });
|
|
1014
1016
|
for (let i = 0; i < upTo; i++) {
|
|
1015
1017
|
const e = S.events[i]; applyStatus(e); tally(e); if (e.kind === 'tools') S.tc[e.agent] = (S.tc[e.agent] || 0) + e.tools.length;
|
|
@@ -1049,6 +1051,7 @@ function tally(ev) { // numbers that pile up over a session: token usage, erro
|
|
|
1049
1051
|
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); }
|
|
1050
1052
|
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); });
|
|
1051
1053
|
else if (ev.kind === 'tools' || ev.kind === 'say') S.lastAct[ev.agent] = ev.ts;
|
|
1054
|
+
if (ev.th) S.th[ev.agent] = { text: ev.th, ts: ev.ts }; else if (ev.kind === 'user') delete S.th.main; // your order starts a new chain of thought: what the lead thought before is no longer what it is on
|
|
1052
1055
|
if (ev.kind === 'results') S.lastAct[ev.agent] = ev.ts; else if (ev.kind === 'user') S.lastAct.main = ev.ts; // a result coming back and your own order are signs of life too: the lead is thinking about them
|
|
1053
1056
|
}
|
|
1054
1057
|
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)}`;
|
|
@@ -1322,7 +1325,7 @@ setInterval(() => {
|
|
|
1322
1325
|
// ---------- speaking tiles ----------
|
|
1323
1326
|
const mouths = new Map();
|
|
1324
1327
|
function startTalk(id, text, label) {
|
|
1325
|
-
const t = $('#t-' + id); if (!t) return;
|
|
1328
|
+
const t = $('#t-' + id); if (!t) return; endThought(id); stopMumble(); TH.next[id] = Date.now() + 8000; // a spoken line takes the bubble and the floor
|
|
1326
1329
|
if (S.present && S.present !== id) hidePresent();
|
|
1327
1330
|
S.spokeAt = performance.now(); S.fill = 0; S.mused = false;
|
|
1328
1331
|
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);
|
|
@@ -1358,6 +1361,73 @@ function stopTalk(id) {
|
|
|
1358
1361
|
const pc = $('#pcam .cimg-talk'), pi = $('#pcam .cimg'); if (pc && pi && S.present === id) { pc.style.display = 'none'; pi.style.display = ''; }
|
|
1359
1362
|
}
|
|
1360
1363
|
const clearSpeaking = () => [...S.speakers].forEach(stopTalk);
|
|
1364
|
+
// =====================================================================
|
|
1365
|
+
// inner thoughts: an agent that has been at work for a while thinks to itself, in a bubble it does not say. What it thinks is real: its own reasoning when the session kept it
|
|
1366
|
+
// (rarely: Claude Code mostly stores a signature), otherwise what it is on right now (the command it waits for, the files it goes between, its to-do item, a failure, its task).
|
|
1367
|
+
// One time in five the thought escapes as a mumble: its own voice, low and muffled. Live only, never while it speaks, off with the little lines or in Quiet.
|
|
1368
|
+
// =====================================================================
|
|
1369
|
+
const TH = { since: {}, next: {}, said: {}, last: 0, voice: null };
|
|
1370
|
+
const THINK = {
|
|
1371
|
+
en: { run: ['Still running… {x}.', 'Come on… {x}.', '{x}. Any second now.'], err: ['Why did that fail?', 'That error again… what am I missing?', 'Not what I expected from “{x}”.'], two: ['{a}, then {b}… what links them?', '{a} and {b}. It has to be in one of them.'], one: ['Something in {a} does not add up.', '{a}… where was that part again?'], again: ['Back to {a}. Again.', '{a}, once more. I am missing something.'],
|
|
1372
|
+
todo: ['One thing at a time: {x}.', '{x}… then the next one.'], next: ['After this: {x}.'], grep: ['Where is “{x}” hiding?', '“{x}”… it has to be somewhere.'], web: ['“{x}”… let’s see what comes up.'], task: ['{x}. Stay on it.', '{x}… getting there, I think.'], many: ['{n} calls in. What do I know so far?'] },
|
|
1373
|
+
fr: { run: ['Toujours en cours… {x}.', 'Allez… {x}.', '{x}. Ça arrive.'], err: ['Pourquoi ça a planté ?', 'Encore cette erreur… qu’est-ce que je rate ?', 'Pas ce que j’attendais de « {x} ».'], two: ['{a}, puis {b}… quel est le lien ?', '{a} et {b}. C’est forcément dans l’un des deux.'], one: ['Il y a un truc dans {a} qui ne colle pas.', '{a}… c’était où, déjà ?'], again: ['Retour à {a}. Encore.', '{a}, une fois de plus. Je rate quelque chose.'],
|
|
1374
|
+
todo: ['Une chose à la fois : {x}.', '{x}… puis la suite.'], next: ['Après ça : {x}.'], grep: ['Où se cache « {x} » ?', '« {x} »… c’est forcément quelque part.'], web: ['« {x} »… voyons ce que ça donne.'], task: ['{x}. On reste dessus.', '{x}… j’y arrive, je crois.'], many: ['{n} appels. Qu’est-ce que je sais, à ce stade ?'] },
|
|
1375
|
+
};
|
|
1376
|
+
function thoughtFor(id) {
|
|
1377
|
+
const now = Date.now(), W = THINK[FR() ? 'fr' : 'en'], seen = (TH.said[id] = TH.said[id] || []), cut = (x, n) => { x = String(x || '').replace(/\s+/g, ' ').trim(); return (x.length > n ? x.slice(0, n).replace(/\s+\S*$/, '') + '…' : x).replace(/[.\s]+$/, ''); };
|
|
1378
|
+
const real = realThought(id); if (real) return real; // its own words first, always: the rest is only for when there are none (left)
|
|
1379
|
+
const buf = (S.term[id] || []).slice(-8), last = buf[buf.length - 1], r = S.run[id], hot = [], out = [], add = (to, k, v) => { for (const tpl of W[k]) to.push(tpl.replace(/\{(\w+)\}/g, (m, key) => v[key])); };
|
|
1380
|
+
const what = (e) => { const d = e.tool.d || {}; return cut(d.note || (d.kind === 'cmd' ? String(d.text || '').trim().split(/\s+/).slice(0, 3).join(' ') : base(d.file) || e.tool.name), 60); };
|
|
1381
|
+
if (last && last.k === 'cmd' && r && r.id === last.id && now - (r.ts || now) > 20000) add(hot, 'run', { x: what(last) }); // a command that takes its time
|
|
1382
|
+
if (last && last.err) add(hot, 'err', { x: what(last) });
|
|
1383
|
+
const files = buf.filter((e) => /^(read|edit|write)$/.test(e.k)).map((e) => base((e.tool.d || {}).file)).filter(Boolean), uniq = [...new Set(files)], f = uniq[uniq.length - 1];
|
|
1384
|
+
if (f && files.filter((x) => x === f).length >= 3) add(out, 'again', { a: f }); else if (uniq.length > 1) add(out, 'two', { a: uniq[uniq.length - 2], b: f }); else if (f) add(out, 'one', { a: f });
|
|
1385
|
+
const td = S.todos[id] || [], cur = td.find((x) => x.s === 'in_progress'), nx = td.find((x) => x.s === 'pending'); if (cur) add(out, 'todo', { x: cut(cur.c, 70) }); if (nx) add(out, 'next', { x: cut(nx.c, 70) });
|
|
1386
|
+
const g = buf.slice(-3).reverse().find((e) => e.k === 'grep' && (e.tool.d || {}).pattern); if (g) add(out, 'grep', { x: cut(g.tool.d.pattern, 30) });
|
|
1387
|
+
const w = buf.slice(-3).reverse().find((e) => e.k === 'web' && (e.tool.d || {}).query); if (w) add(out, 'web', { x: cut(w.tool.d.query, 50) });
|
|
1388
|
+
const p = S.agents[id]; if (isSub(id) && p && p.role && !/^agent$/i.test(p.role)) add(out, 'task', { x: cut(p.role, 60) });
|
|
1389
|
+
if ((S.tc[id] || 0) >= 15) add(out, 'many', { n: S.tc[id] });
|
|
1390
|
+
const fresh = (a) => a.filter((x) => !seen.includes(x)), h = fresh(hot), o = fresh(out), pool = h.length && (!o.length || Math.random() < 0.6) ? h : o; if (!pool.length) return null;
|
|
1391
|
+
const line = pool[Math.floor(Math.random() * pool.length)]; seen.push(line); if (seen.length > 6) seen.shift(); return line;
|
|
1392
|
+
}
|
|
1393
|
+
function realThought(id, peek) { // the next sentence of the agent's own reasoning for the work it is on (kept by the session for some of its steps); null when there is none, or when all of it has been shown
|
|
1394
|
+
const th = S.th[id]; if (!th || Date.now() - th.ts > 600000) return null;
|
|
1395
|
+
if (!th.ss) { th.ss = (th.text.match(/[^.!?]+(?:[.!?]+|$)/g) || []).map((x) => x.trim()).filter((x) => x.length >= 12).map((x) => (x.length > 150 ? x.slice(0, 150).replace(/\s+\S*$/, '') + '…' : x)); th.i = 0; }
|
|
1396
|
+
return th.i < th.ss.length ? (peek ? th.ss[th.i] : th.ss[th.i++]) : null;
|
|
1397
|
+
}
|
|
1398
|
+
function showThought(id, text) {
|
|
1399
|
+
const t = $('#t-' + id); if (!t || t.style.display === 'none' || S.speakers.has(id) || t.classList.contains('bye')) return false;
|
|
1400
|
+
t.querySelector('.bubble').innerHTML = `<small>💭</small>${esc(text)}`; t.classList.add('musing'); clearTimeout(t._thT); t._thT = setTimeout(() => endThought(id), Math.min(9000, 3500 + text.length * 55)); return true;
|
|
1401
|
+
}
|
|
1402
|
+
function endThought(id) {
|
|
1403
|
+
const t = $('#t-' + id); if (!t || !t.classList.contains('musing')) return; clearTimeout(t._thT); t.classList.remove('musing'); if (TH.voice && TH.voice.id === id) stopMumble();
|
|
1404
|
+
if (!S.speakers.has(id) && !t.classList.contains('bye')) t.querySelector('.bubble').innerHTML = '';
|
|
1405
|
+
}
|
|
1406
|
+
const clearThoughts = () => { document.querySelectorAll('.tile.musing').forEach((t) => endThought(t.id.slice(2))); stopMumble(); };
|
|
1407
|
+
function stopMumble() { const v = TH.voice; if (v) { TH.voice = null; try { v.src.stop(); } catch {} } }
|
|
1408
|
+
async function mumble(id, text) { // lips closed: the mouth does not move, the music does not make room, and anyone who starts to speak cuts it
|
|
1409
|
+
const p = S.agents[id], t = $('#t-' + id); if (!p || !t || !prefs.voice || AUD.off() || !AUD.ctx || S.speakers.size || TH.voice) return;
|
|
1410
|
+
if (!TTS.server || prefs.vstyle === 'animal' || prefs.vstyle === 'sims') { AUD.murmur(0.5 + (p.vi % 5) / 10, Math.min(2.2, estMs(text) / 1000), 0.4); return; }
|
|
1411
|
+
const voice = TTS.voiceFor(p, frVoice(text)), token = S.token; if (!voice) return; let buf; try { buf = await TTS.buf(speakable(text), voice, TTS.rate(), true); } catch { return; } // made like a line prepared ahead: it never delays a real one
|
|
1412
|
+
if (token !== S.token || S.speakers.size || TH.voice || !t.classList.contains('musing')) return; // someone spoke meanwhile, or the thought is gone
|
|
1413
|
+
const c = AUD.ctx, src = c.createBufferSource(), lp = c.createBiquadFilter(), g = c.createGain(); src.buffer = buf; lp.type = 'lowpass'; lp.frequency.value = 1100; g.gain.value = 0.2; let stops = [];
|
|
1414
|
+
if (prefs.vstyle === 'natural' || prefs.vstyle === 'kokoro') src.connect(lp); else { const ch = fxChain(c, src, fxFor(p)); stops = ch.stops; ch.node.connect(lp); }
|
|
1415
|
+
lp.connect(g); g.connect(AUD.voice); AUD.live.add(src); TH.voice = { id, src };
|
|
1416
|
+
src.onended = () => { AUD.live.delete(src); stops.forEach((o) => { try { o.stop(); } catch {} }); if (TH.voice && TH.voice.src === src) TH.voice = null; }; src.start();
|
|
1417
|
+
}
|
|
1418
|
+
setInterval(() => { // who gets a thought: someone at work for a while and silent; two at most on screen, a few seconds apart, each agent every half minute or so
|
|
1419
|
+
const now = Date.now(); if (!S.live || !S.playing || S.locked || S.recomputing || !prefs.banter || prefs.chat === 'quiet' || S.hurry || S.boost > 1 || (document.hidden && !S.exporting) || !atTail()) return;
|
|
1420
|
+
const ready = [];
|
|
1421
|
+
for (const p of Object.values(S.agents)) {
|
|
1422
|
+
const id = p.id, t = $('#t-' + id); if (id === 'user' || !t) continue;
|
|
1423
|
+
if (t.style.display === 'none' || S.gone.has(id) || agentState(id, now).k !== 'working') { delete TH.since[id]; continue; }
|
|
1424
|
+
TH.since[id] = TH.since[id] || now; if (!S.speakers.has(id) && !t.classList.contains('musing') && now - TH.since[id] > 15000 && now > (TH.next[id] || 0)) ready.push(id);
|
|
1425
|
+
}
|
|
1426
|
+
if (!ready.length || now - TH.last < 5000 || document.querySelectorAll('.tile.musing').length >= 2) return;
|
|
1427
|
+
const own = ready.filter((id) => realThought(id, true)), from = own.length ? own : ready; // whoever has reasoning of its own to show goes first
|
|
1428
|
+
const id = from[Math.floor(Math.random() * from.length)], text = thoughtFor(id); TH.next[id] = now + (text ? 28000 + Math.random() * 24000 : 8000) / chatMul(); if (!text || !showThought(id, text)) return;
|
|
1429
|
+
TH.last = now; if (Math.random() < 0.2) mumble(id, text);
|
|
1430
|
+
}, 1500);
|
|
1361
1431
|
|
|
1362
1432
|
// ---------- hierarchy tree ----------
|
|
1363
1433
|
function renderTree() {
|
|
@@ -1712,7 +1782,8 @@ async function run(i) {
|
|
|
1712
1782
|
const rem = S.events.length - i;
|
|
1713
1783
|
if (rem > 300 || aheadSpeakable(i, 16) > 16 || lagMs(i) > 60000) { // only a really big backlog is skipped; before that, lines are shortened instead
|
|
1714
1784
|
let k = S.events.length, back = 0; while (k > i && back < 8) { k--; if (SPK.has(S.events[k].kind)) back++; }
|
|
1715
|
-
|
|
1785
|
+
let to = rem > 300 ? Math.max(k, S.events.length - 14) : k; for (let u = i; u < to; u++) if (S.events[u].kind === 'user') { to = u; break; } // never over a line of yours: what you asked is always said, shortened if need be
|
|
1786
|
+
if (to > i) { i = to; recomputeStatus(i); paintStatus(); }
|
|
1716
1787
|
}
|
|
1717
1788
|
}
|
|
1718
1789
|
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
|
|
@@ -1749,7 +1820,7 @@ async function run(i) {
|
|
|
1749
1820
|
if (g.kind === 'order') { S.status[g.to] = 'working'; AUD.join(); setTimeout(() => { paintStatus(); fly(g.agent, g.to, '📨'); popTile(g.to); }, 30); }
|
|
1750
1821
|
if (g.kind === 'report') { AUD.ding(); setTimeout(() => { fly(g.agent, g.to, '✅'); popTile(g.agent); }, 30); }
|
|
1751
1822
|
const said = clipSpeech(line, S.pace); if (g.agent === 'main' && g.kind === 'say') { const full = plain(g.text), key = said.replace(/…$/, '').trim().slice(-30), pos = key ? full.indexOf(key) : -1, n = pos >= 0 ? pos + key.length : said.length; S.mainSaid = { ev: g, said: n, full: n >= full.length - 5 }; }
|
|
1752
|
-
return speakLine(said, S.agents[g.agent], true, token, sessFr(said, g)).then(() => { if (token === S.token) stopTalk(g.agent); });
|
|
1823
|
+
return speakLine(said, S.agents[g.agent], true, token, sessFr(said, g), g.kind === 'user').then(() => { if (token === S.token) stopTalk(g.agent); });
|
|
1753
1824
|
};
|
|
1754
1825
|
let plays;
|
|
1755
1826
|
if (ev.kind === 'order' && group.length > 1) { // the first order in full, then "Also Remy on X, Iris on Y"
|
|
@@ -1772,7 +1843,7 @@ function clipSpeech(t, cap) { // the bubble keeps the full text; the voice rea
|
|
|
1772
1843
|
const m = t.slice(0, cap + 40).match(/^.{20,}?[.!?](?=\s|$)/); if (m && m[0].length <= cap + 30) return m[0];
|
|
1773
1844
|
return t.slice(0, cap).replace(/\s+\S*$/, '') + '…';
|
|
1774
1845
|
}
|
|
1775
|
-
function pause() { S.playing = false; S.hurry = false; S.boost = 1; S.token++; synth && synth.cancel(); AUD.stopAll(); clearSpeaking(); $('#card').classList.remove('on'); syncUI(); }
|
|
1846
|
+
function pause() { S.playing = false; S.hurry = false; S.boost = 1; S.token++; synth && synth.cancel(); AUD.stopAll(); clearSpeaking(); clearThoughts(); $('#card').classList.remove('on'); syncUI(); }
|
|
1776
1847
|
function togglePlay() {
|
|
1777
1848
|
if (S.locked) return;
|
|
1778
1849
|
if (S.playing) { S.userPaused = true; pause(); } else { claimAudio(); S.userPaused = false; run(S.idx >= S.events.length - 1 && !S.live ? (recomputeStatus(0), 0) : S.live ? S.pos : S.idx); } // live: from the first line not said yet (the one that was cut, if any)
|
|
@@ -1799,7 +1870,7 @@ async function loadSession(proj, id) {
|
|
|
1799
1870
|
const data = await (await fetch(`/api/session?proj=${encodeURIComponent(proj)}&id=${id}`)).json();
|
|
1800
1871
|
if (mine !== S.loadN || data.error) return false;
|
|
1801
1872
|
S.term = {}; S.camFor = null; S.focus = null; S.agents = {}; for (const k in nameBook) delete nameBook[k]; [{ id: 'user' }, ...data.agents].forEach((a) => (S.agents[a.id] = persona(a)));
|
|
1802
|
-
S.events = data.events; stampT(0); S.byK = new Map(data.events.map((e) => [e.k, e])); S.seq = data.seq || 0; S.ep = data.ep || ''; S.sawEnd = data.events.some((e) => e.end); S.late = []; S.pos = 0; S.joinAt = Date.now(); S.idx = 0; S.status = {}; S.tc = {}; S.cur = { proj, id }; S.logTo = 0; S.detail = null; $('#detail').classList.remove('on');
|
|
1873
|
+
clearThoughts(); Object.assign(TH, { since: {}, next: {}, said: {}, last: 0 }); S.events = data.events; stampT(0); S.byK = new Map(data.events.map((e) => [e.k, e])); S.seq = data.seq || 0; S.ep = data.ep || ''; S.sawEnd = data.events.some((e) => e.end); S.late = []; S.pos = 0; S.joinAt = Date.now(); S.idx = 0; S.status = {}; S.tc = {}; S.cur = { proj, id }; S.logTo = 0; S.detail = null; $('#detail').classList.remove('on');
|
|
1803
1874
|
const opt = $('#sessions').selectedOptions[0]; S.title = (S.live ? niceProj(proj) : opt && opt.dataset.title) || '';
|
|
1804
1875
|
if (proj === '_demo') S.events.forEach((e) => { if (e.fr) trMem.set(baseText(e), e.fr); }); // the demo ships its own French
|
|
1805
1876
|
$('#subtitle').textContent = S.title; recomputeStatus(0); buildStage(); renderHighlights(); setFocus('main'); renderLog(true); computeGantt(); layout(); paintStatus(); prefetchTr(0); return true;
|
|
@@ -2062,7 +2133,7 @@ function setView(v) { prefs.view = v; savePrefs(); document.body.classList.toggl
|
|
|
2062
2133
|
|
|
2063
2134
|
$('#viewSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setView(b.dataset.v); };
|
|
2064
2135
|
$('#langSel').onchange = () => setLang($('#langSel').value, true);
|
|
2065
|
-
$('#charPack').value = ['none', 'folder', '
|
|
2136
|
+
$('#charPack').value = ['none', 'folder', 'cats'].includes(prefs.charPack) ? prefs.charPack : 'none'; $('#charPack').onchange = () => { prefs.charPack = $('#charPack').value; savePrefs(); $('#charReload').onclick(); };
|
|
2066
2137
|
$('#charReload').onclick = async () => { await loadChars(); for (const k in imgCache) delete imgCache[k]; for (const id in S.agents) S.agents[id].custom = null; for (const id in S.agents) S.agents[id].custom = customFor(id); buildStage(); paintStatus(); renderCharInfo(); };
|
|
2067
2138
|
$('#charOpen').onclick = () => window.parent.postMessage({ agentStandup: 'openCharacters' }, '*');
|
|
2068
2139
|
$('#modeSeg').onclick = (e) => { const b = e.target.closest('button'); if (b) setMode(b.dataset.m); };
|
|
@@ -2197,7 +2268,7 @@ addEventListener('keydown', (e) => { // on the way down: Escape also works fro
|
|
|
2197
2268
|
// ---------- a bubble that is not whole on screen (last row of tiles, a row of the compact list below the fold) brings the stage to it ----------
|
|
2198
2269
|
new MutationObserver((ms) => {
|
|
2199
2270
|
for (const m of ms) {
|
|
2200
|
-
const b = m.target; if (b.className !== 'bubble' || !b.offsetHeight) continue; const st = $('#stage'), r = b.getBoundingClientRect(), s = st.getBoundingClientRect();
|
|
2271
|
+
const b = m.target; if (b.className !== 'bubble' || !b.offsetHeight || b.parentNode.classList.contains('musing')) continue; const st = $('#stage'), r = b.getBoundingClientRect(), s = st.getBoundingClientRect(); // (a thought does not move the stage)
|
|
2201
2272
|
let room = r.top - s.top - 10; for (const o of st.querySelectorAll('.bubble')) { const q = o.getBoundingClientRect(); if (o !== b && q.height && q.top >= s.top && q.bottom <= s.bottom) room = Math.min(room, q.top - s.top - 10); } // never at the cost of another line that is on screen
|
|
2202
2273
|
if (r.bottom > s.bottom - 6) { if (room > 0) st.scrollBy({ top: Math.min(r.bottom - s.bottom + 10, room), behavior: 'smooth' }); } else if (r.top < s.top + 4) st.scrollBy({ top: r.top - s.top - 10, behavior: 'smooth' });
|
|
2203
2274
|
}
|
package/public/locales/da.json
CHANGED
|
@@ -214,7 +214,6 @@
|
|
|
214
214
|
"Cat pack": "Kattepakke",
|
|
215
215
|
"characters from the pack": "figurer fra pakken",
|
|
216
216
|
"Drawn characters": "Tegnede figurer",
|
|
217
|
-
"Starter pack": "Startpakke",
|
|
218
217
|
"To tweak them, copy them into your folder and pick “My folder”.": "For at tilpasse dem: kopiér dem til din mappe og vælg “Min mappe”.",
|
|
219
218
|
"My folder": "Min mappe",
|
|
220
219
|
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "Figurerne tegnet af appen. Vælg “Min mappe” for dine egne billeder, eller en pakke."
|
package/public/locales/de.json
CHANGED
|
@@ -215,7 +215,6 @@
|
|
|
215
215
|
"Cat pack": "Katzen-Pack",
|
|
216
216
|
"characters from the pack": "Figuren aus dem Pack",
|
|
217
217
|
"Drawn characters": "Gezeichnete Figuren",
|
|
218
|
-
"Starter pack": "Starter-Pack",
|
|
219
218
|
"To tweak them, copy them into your folder and pick “My folder”.": "Zum Anpassen in deinen Ordner kopieren und „Mein Ordner“ wählen.",
|
|
220
219
|
"My folder": "Mein Ordner",
|
|
221
220
|
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "Die von der App gezeichneten Figuren. Wähle „Mein Ordner“ für eigene Bilder oder ein Pack."
|
package/public/locales/en.json
CHANGED
|
@@ -168,7 +168,6 @@
|
|
|
168
168
|
"standing by": "standing by",
|
|
169
169
|
"Start Claude Code in {0} — the call will start by itself.": "Start Claude Code in {0} — the call will start by itself.",
|
|
170
170
|
"Start Claude Code in this folder — the call will start by itself.": "Start Claude Code in this folder — the call will start by itself.",
|
|
171
|
-
"Starter pack": "Starter pack",
|
|
172
171
|
"sub-agents active": "sub-agents active",
|
|
173
172
|
"task": "task",
|
|
174
173
|
"Tasks": "Tasks",
|
package/public/locales/es.json
CHANGED
|
@@ -215,7 +215,6 @@
|
|
|
215
215
|
"Cat pack": "Pack de gatos",
|
|
216
216
|
"characters from the pack": "personajes del pack",
|
|
217
217
|
"Drawn characters": "Personajes dibujados",
|
|
218
|
-
"Starter pack": "Pack inicial",
|
|
219
218
|
"To tweak them, copy them into your folder and pick “My folder”.": "Para retocarlos, cópialos en tu carpeta y elige «Mi carpeta».",
|
|
220
219
|
"My folder": "Mi carpeta",
|
|
221
220
|
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "Los personajes dibujados por la aplicación. Elige «Mi carpeta» para tus propias imágenes, o un pack."
|
package/public/locales/ja.json
CHANGED
|
@@ -215,7 +215,6 @@
|
|
|
215
215
|
"Cat pack": "ネコパック",
|
|
216
216
|
"characters from the pack": "パックのキャラクター",
|
|
217
217
|
"Drawn characters": "描画キャラクター",
|
|
218
|
-
"Starter pack": "スターターパック",
|
|
219
218
|
"To tweak them, copy them into your folder and pick “My folder”.": "調整するには、自分のフォルダーにコピーして「自分のフォルダー」を選んでください。",
|
|
220
219
|
"My folder": "自分のフォルダー",
|
|
221
220
|
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "アプリが描画するキャラクターです。自分の画像を使うには「自分のフォルダー」、またはパックを選んでください。"
|
|
@@ -215,7 +215,6 @@
|
|
|
215
215
|
"Cat pack": "Pacote de gatos",
|
|
216
216
|
"characters from the pack": "personagens do pacote",
|
|
217
217
|
"Drawn characters": "Personagens desenhados",
|
|
218
|
-
"Starter pack": "Pacote inicial",
|
|
219
218
|
"To tweak them, copy them into your folder and pick “My folder”.": "Para ajustá-los, copie-os para sua pasta e escolha “Minha pasta”.",
|
|
220
219
|
"My folder": "Minha pasta",
|
|
221
220
|
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "Os personagens desenhados pelo aplicativo. Escolha “Minha pasta” para suas próprias imagens, ou um pacote."
|
|
@@ -215,7 +215,6 @@
|
|
|
215
215
|
"Cat pack": "猫咪包",
|
|
216
216
|
"characters from the pack": "来自角色包的角色",
|
|
217
217
|
"Drawn characters": "绘制的角色",
|
|
218
|
-
"Starter pack": "入门包",
|
|
219
218
|
"To tweak them, copy them into your folder and pick “My folder”.": "若要修改,请将它们复制到你的文件夹并选择“我的文件夹”。",
|
|
220
219
|
"My folder": "我的文件夹",
|
|
221
220
|
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "由应用绘制的角色。选择“我的文件夹”使用你自己的图片,或选择一个角色包。"
|
|
@@ -215,7 +215,6 @@
|
|
|
215
215
|
"Cat pack": "貓咪包",
|
|
216
216
|
"characters from the pack": "來自角色包的角色",
|
|
217
217
|
"Drawn characters": "繪製的角色",
|
|
218
|
-
"Starter pack": "入門包",
|
|
219
218
|
"To tweak them, copy them into your folder and pick “My folder”.": "若要修改,請將它們複製到你的資料夾並選擇「我的資料夾」。",
|
|
220
219
|
"My folder": "我的資料夾",
|
|
221
220
|
"The characters drawn by the app. Pick “My folder” for your own images, or a pack.": "由應用程式繪製的角色。選擇「我的資料夾」使用你自己的圖片,或選擇一個角色包。"
|
package/server.js
CHANGED
|
@@ -302,7 +302,7 @@ function parseRecord(st, r) {
|
|
|
302
302
|
if (r.type === 'attachment' && r.attachment && r.attachment.type === 'queued_command') { // an order typed while the lead was working: Claude Code hands it over in the middle of the turn, as an attachment and not as a user message
|
|
303
303
|
const q = r.attachment, human = q.origin ? q.origin.kind === 'human' : q.commandMode === 'prompt'; if (agent !== 'main' || !human) return; // the same record also carries task notifications
|
|
304
304
|
const txt = (Array.isArray(q.prompt) ? q.prompt.filter((b) => b && b.type === 'text').map((b) => b.text).join('\n') : typeof q.prompt === 'string' ? q.prompt : '').replace(HARNESS, '').trim();
|
|
305
|
-
if (txt && !/^<(system-reminder|command-|local-command|task-notification)/.test(txt)) push({ ts, agent: 'user', kind: 'user', text: txt });
|
|
305
|
+
if (txt && !/^<(system-reminder|command-|local-command|task-notification)/.test(txt)) push({ ts: Math.max(ts, st.lastTs || 0), agent: 'user', kind: 'user', text: txt }); // its place in the call is when the lead got it, not when it was typed (minutes earlier, during a long command): the call hears of it only now, and a line dated in the past looks "far behind" and was shown without a voice
|
|
306
306
|
return;
|
|
307
307
|
}
|
|
308
308
|
if (r.type === 'user') {
|
|
@@ -325,12 +325,14 @@ function parseRecord(st, r) {
|
|
|
325
325
|
const u = m.usage, prev = st.msgUsage.get(m.id);
|
|
326
326
|
st.msgUsage.set(m.id, { ts: prev ? prev.ts : ts, u: { i: u.input_tokens || 0, o: u.output_tokens || 0, r: u.cache_read_input_tokens || 0, w: u.cache_creation_input_tokens || 0 }, model: m.model });
|
|
327
327
|
}
|
|
328
|
+
const th = () => { const t = st.th; st.th = ''; return t ? { th: t } : {}; }; // what the agent was thinking before this line or call, when Claude Code kept it (most of the time it only keeps a signature): the call shows it as an inner thought
|
|
328
329
|
for (const b of blocks(r)) {
|
|
329
|
-
if (b.type === '
|
|
330
|
+
if (b.type === 'thinking' && typeof b.thinking === 'string' && b.thinking.trim()) st.th = clip(b.thinking.replace(/\s+/g, ' ').trim(), 900);
|
|
331
|
+
else if (b.type === 'text' && typeof b.text === 'string' && b.text.trim()) { said = push({ ts, agent, kind: 'say', text: b.text.trim(), ...th() }); st.act = st.raw.length - 1; st.sayOf = m.id; st.sayAt = st.act; }
|
|
330
332
|
else if (b.type === 'tool_use') {
|
|
331
333
|
st.toolName[b.id] = b.name; if (b.input && b.input.run_in_background) (st.bg = st.bg || {})[b.id] = true;
|
|
332
334
|
if (b.name === 'Agent' || b.name === 'Task') push({ ts, agent, kind: 'agentcall', toolId: b.id, bg: !!(b.input && b.input.run_in_background), text: (b.input && b.input.prompt) || '', title: (b.input && b.input.description) || '' });
|
|
333
|
-
else { push({ ts, agent, kind: 'tool', tool: toolLabel(b, ts) }); st.act = st.raw.length - 1; }
|
|
335
|
+
else { push({ ts, agent, kind: 'tool', tool: toolLabel(b, ts), ...th() }); st.act = st.raw.length - 1; }
|
|
334
336
|
}
|
|
335
337
|
}
|
|
336
338
|
if (agent === 'main' && m.stop_reason === 'end_turn') { // the lead's turn is over: Claude Code writes one record per content block, each with the message's stop reason, so the mark goes on the line this record carries (a thinking block carries none)
|
|
@@ -391,7 +393,7 @@ function buildTimeline(proj, id, files = []) {
|
|
|
391
393
|
const p = lastReal >= 0 ? merged[lastReal] : null;
|
|
392
394
|
if (e.kind === 'usage' || e.kind === 'finish' || e.kind === 'compact') { merged.push(e); continue; }
|
|
393
395
|
if (e.kind === 'tool' && p && p.kind === 'tools' && p.agent === e.agent && !p.end) { p.tools.push(e.tool); if (e.end) p.end = 1; } // a beat keeps the key of its first call; one that closed a turn takes no more calls
|
|
394
|
-
else { merged.push(e.kind === 'tool' ? { ts: e.ts, agent: e.agent, kind: 'tools', tools: [e.tool], ...(e.end ? { end: 1 } : {}), k: e.k } : e); lastReal = merged.length - 1; }
|
|
396
|
+
else { merged.push(e.kind === 'tool' ? { ts: e.ts, agent: e.agent, kind: 'tools', tools: [e.tool], ...(e.end ? { end: 1 } : {}), ...(e.th ? { th: e.th } : {}), k: e.k } : e); lastReal = merged.length - 1; }
|
|
395
397
|
}
|
|
396
398
|
const real = merged.find((e) => e.kind !== 'usage' && e.kind !== 'finish' && e.kind !== 'compact'), first = real ? real.ts : 0, last = merged.length ? merged[merged.length - 1].ts : 0; // time zero = the first thing said or done
|
|
397
399
|
return { id, proj, agents: Object.values(agents), events: merged.map((e) => ({ ...e, t: Math.max(0, e.ts - first) })), durationMs: last - first, recentUntil };
|
|
@@ -570,8 +572,8 @@ const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css
|
|
|
570
572
|
const CHAR_NAME = /^[\w][\w .()\-]{0,78}\.(png|gif|webp|jpe?g)$/i, CHAR_MAX = 300 * 1024, CHAR_TYPES = { png: 'image/png', gif: 'image/gif', webp: 'image/webp', jpg: 'image/jpeg', jpeg: 'image/jpeg' };
|
|
571
573
|
const charactersDir = () => process.env.AGENT_STANDUP_CHARACTERS || path.join(os.homedir(), '.agent-standup', 'characters');
|
|
572
574
|
// lstat, not stat: a symlink is not a regular file, so it is skipped and never served
|
|
573
|
-
// Built-in character packs, shipped with the app (examples/<folder>): picked in ⚙ → Characters
|
|
574
|
-
const PACKS = {
|
|
575
|
+
// Built-in character packs, shipped with the app (examples/<folder>): picked in ⚙ → Characters. examples/characters stays in the repository as a model for one's own folder; it is not shipped
|
|
576
|
+
const PACKS = { cats: 'cats' }, packDir = (name) => (PACKS[name] ? path.join(__dirname, 'examples', PACKS[name]) : null);
|
|
575
577
|
function listCharacters(pack) {
|
|
576
578
|
const dir = pack ? packDir(pack) : charactersDir(); let names = []; if (!dir) return { dir: '', max: CHAR_MAX, chars: [] }; try { names = fs.readdirSync(dir); } catch {}
|
|
577
579
|
const ok = names.filter((f) => CHAR_NAME.test(f)).filter((f) => { try { const st = fs.lstatSync(path.join(dir, f)); return st.isFile() && st.size <= CHAR_MAX; } catch { return false; } }).slice(0, 80);
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
Example pixel characters (public domain, made for Agent Standup).
|
|
2
|
-
Copy them into your characters folder to try the feature, then draw your own:
|
|
3
|
-
- 32x32 or 64x64 px PNG with a transparent background, facing front, feet at the bottom
|
|
4
|
-
- lead.png = the lead (Claude), you.png = you, any other name = a sub-agent
|
|
5
|
-
- name-talk.png = optional open-mouth frame
|
|
Binary file
|
|
Binary file
|
|
Binary file
|