feat(miniapp): add heartbeat pigeon to Orch canvas

- Add 🕊️ as a permanent heartbeat character at MAP_POSITIONS.heartbeat (x=230, y=58)
- Separate heartbeat from secretary: secretary now represents plan mode,
  pigeon represents periodic heartbeat sessions
- Pigeon is always alive (permanent); GC returns it to idle without hiding
- Pigeon flips direction periodically when stationary (720ms idle /
  480ms waiting / 280ms toolcall) and faces movement direction when moving
- Add orch-badge-heartbeat (HB) to left panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-25 11:23:20 +09:00
parent 9be809210f
commit ec0a90ef02
2 changed files with 38 additions and 6 deletions

View file

@ -889,6 +889,11 @@
<div class="orch-badge-label">SEC</div> <div class="orch-badge-label">SEC</div>
<div class="orch-badge-dot"></div> <div class="orch-badge-dot"></div>
</div> </div>
<div class="orch-badge alive" id="orch-badge-heartbeat">
<div class="orch-badge-emoji">🕊️</div>
<div class="orch-badge-label">HB</div>
<div class="orch-badge-dot"></div>
</div>
</div> </div>
<div class="orch-canvas-wrap"> <div class="orch-canvas-wrap">
<canvas id="orch-canvas" width="320" height="320"></canvas> <canvas id="orch-canvas" width="320" height="320"></canvas>
@ -1810,7 +1815,7 @@ var _orchLastTs = null;
var _orchBOB = [0, -1, -2, -1]; var _orchBOB = [0, -1, -2, -1];
var _orchFRAME_MS = {idle:450, waiting:650, toolcall:90, talking:280, entering:220, exiting:220}; var _orchFRAME_MS = {idle:450, waiting:650, toolcall:90, talking:280, entering:220, exiting:220};
var _orchWALK = 55; var _orchWALK = 55;
var _orchConductor, _orchSecretary, _orchSubagents, _orchSlots, _orchFreeSlots; var _orchConductor, _orchSecretary, _orchHeartbeat, _orchSubagents, _orchSlots, _orchFreeSlots;
function _orchMakeChar(id, emoji, home) { function _orchMakeChar(id, emoji, home) {
return {id:id, emoji:emoji, x:home.x, y:home.y, home:home, target:null, state:'idle', return {id:id, emoji:emoji, x:home.x, y:home.y, home:home, target:null, state:'idle',
@ -1819,7 +1824,9 @@ function _orchMakeChar(id, emoji, home) {
function _orchInitChars() { function _orchInitChars() {
_orchConductor = _orchMakeChar('conductor', '👑', MAP_POSITIONS.conductor); _orchConductor = _orchMakeChar('conductor', '👑', MAP_POSITIONS.conductor);
_orchSecretary = _orchMakeChar('secretary', '👩‍💼', MAP_POSITIONS.secretary); _orchSecretary = _orchMakeChar('secretary', '👩‍💼', MAP_POSITIONS.secretary);
_orchConductor.alive = true; _orchSecretary.alive = true; _orchHeartbeat = _orchMakeChar('heartbeat', '🕊️', MAP_POSITIONS.heartbeat);
_orchConductor.alive = true; _orchSecretary.alive = true; _orchHeartbeat.alive = true;
_orchHeartbeat.facing = 1; _orchHeartbeat.flipTimer = 0;
var ps = [{id:'s0',emoji:'🔍'},{id:'s1',emoji:'📊'},{id:'s2',emoji:'💻'}, var ps = [{id:'s0',emoji:'🔍'},{id:'s1',emoji:'📊'},{id:'s2',emoji:'💻'},
{id:'s3',emoji:'🔧'},{id:'s4',emoji:'🎯'}]; {id:'s3',emoji:'🔧'},{id:'s4',emoji:'🎯'}];
_orchSubagents = ps.map(function(p,i){ _orchSubagents = ps.map(function(p,i){
@ -1828,7 +1835,7 @@ function _orchInitChars() {
}); });
_orchSlots = {}; _orchFreeSlots = _orchSubagents.slice(); _orchSlots = {}; _orchFreeSlots = _orchSubagents.slice();
} }
function _orchAllChars() { return [_orchConductor, _orchSecretary].concat(_orchSubagents); } function _orchAllChars() { return [_orchConductor, _orchSecretary, _orchHeartbeat].concat(_orchSubagents); }
function _orchSyncBadge(id, state, alive) { function _orchSyncBadge(id, state, alive) {
var el = document.getElementById('orch-badge-' + id); if (!el) return; var el = document.getElementById('orch-badge-' + id); if (!el) return;
@ -1843,7 +1850,7 @@ function _orchMoveTo(c, pos, cb) { c.target=pos; c._onArrive=cb||null; }
function _orchSay(c, text, ttl) { c.bubble={text:text, ttl:ttl||2200}; } function _orchSay(c, text, ttl) { c.bubble={text:text, ttl:ttl||2200}; }
function _orchCharForId(id) { function _orchCharForId(id) {
if (id === 'heartbeat') return _orchSecretary; if (id === 'heartbeat') return _orchHeartbeat;
if (_orchSlots[id]) return _orchSlots[id]; if (_orchSlots[id]) return _orchSlots[id];
return _orchConductor; return _orchConductor;
} }
@ -1865,7 +1872,13 @@ function _orchGC(id) {
_orchSetState(c,'exiting'); _orchSetState(c,'exiting');
_orchMoveTo(c, MAP_POSITIONS.door, function(){ c.alive=false; _orchSetState(c,'idle'); }); _orchMoveTo(c, MAP_POSITIONS.door, function(){ c.alive=false; _orchSetState(c,'idle'); });
} else { } else {
var ch=_orchCharForId(id); ch.alive=false; _orchSetState(ch,'idle'); var ch=_orchCharForId(id);
if (ch === _orchHeartbeat) {
// Heartbeat pigeon is permanent — keep alive, just return to idle.
_orchSetState(ch,'idle');
} else {
ch.alive=false; _orchSetState(ch,'idle');
}
} }
} }
function _orchConverse(fromId, toId, text) { function _orchConverse(fromId, toId, text) {
@ -1894,6 +1907,17 @@ function _orchUpdate(dt) {
else { c.x=c.target.x; c.y=c.target.y; c.target=null; if(c._onArrive){c._onArrive();c._onArrive=null;} } else { c.x=c.target.x; c.y=c.target.y; c.target=null; if(c._onArrive){c._onArrive();c._onArrive=null;} }
} }
if (c.bubble){ c.bubble.ttl-=dt; if(c.bubble.ttl<=0) c.bubble=null; } if (c.bubble){ c.bubble.ttl-=dt; if(c.bubble.ttl<=0) c.bubble=null; }
// Heartbeat pigeon: face movement direction; flip periodically when stationary.
if (c === _orchHeartbeat) {
if (c.target) {
var pdx = c.target.x - c.x;
if (Math.abs(pdx) > 1) c.facing = pdx > 0 ? 1 : -1;
} else {
var flipRate = c.state==='toolcall' ? 280 : c.state==='waiting' ? 480 : 720;
c.flipTimer += dt;
if (c.flipTimer >= flipRate) { c.flipTimer -= flipRate; c.facing = -c.facing; }
}
}
}); });
} }
function _orchDrawBubble(c) { function _orchDrawBubble(c) {
@ -1919,7 +1943,14 @@ function _orchDrawChar(c) {
orchCtx.arc(cx,cy,11,0,Math.PI*2); orchCtx.fill(); orchCtx.arc(cx,cy,11,0,Math.PI*2); orchCtx.fill();
} }
orchCtx.font='18px serif'; orchCtx.textAlign='center'; orchCtx.textBaseline='middle'; orchCtx.font='18px serif'; orchCtx.textAlign='center'; orchCtx.textBaseline='middle';
orchCtx.fillText(c.emoji, cx, cy); if (c.facing === -1) {
orchCtx.save();
orchCtx.translate(cx, cy); orchCtx.scale(-1, 1);
orchCtx.fillText(c.emoji, 0, 0);
orchCtx.restore();
} else {
orchCtx.fillText(c.emoji, cx, cy);
}
orchCtx.font='6px Silkscreen,monospace'; orchCtx.textAlign='center'; orchCtx.textBaseline='top'; orchCtx.font='6px Silkscreen,monospace'; orchCtx.textAlign='center'; orchCtx.textBaseline='top';
orchCtx.fillStyle=c.state==='talking'?'#facc15':'#3a4a7a'; orchCtx.fillStyle=c.state==='talking'?'#facc15':'#3a4a7a';
orchCtx.fillText(c.id.toUpperCase(), cx, cy+11); orchCtx.fillText(c.id.toUpperCase(), cx, cy+11);

View file

@ -27,6 +27,7 @@ var MAP_POSITIONS = {
door: { x: 160, y: 314 }, // entry / exit point door: { x: 160, y: 314 }, // entry / exit point
conductor: { x: 160, y: 58 }, conductor: { x: 160, y: 58 },
secretary: { x: 108, y: 58 }, secretary: { x: 108, y: 58 },
heartbeat: { x: 230, y: 58 }, // pigeon messenger — periodic heartbeat agent
meeting: { x: 160, y: 161 }, // neutral zone for conversations meeting: { x: 160, y: 161 }, // neutral zone for conversations
stations: [ stations: [
{ x: 40, y: 106 }, // S0 scout { x: 40, y: 106 }, // S0 scout