2026-11-06 08:28:04 +00:00
function history ( key ) {
const known = ( people ()[ key ] || { names : [ nameOf ( key )]}). names ;
const radio = state . radio . filter (( row ) => row . source === key || row . targets . includes ( key )). map (( row ) => ({ ts : row . ts , mine : row . from === HUMAN , who : row . from , text : row . text }));
2026-11-07 19:59:47 +00:00
const sent = state . messages . filter (( message ) => known . includes ( message . to ) || message . to === "*" ). map (( message ) => ({ ts : message . created_ts , mine : true , who : HUMAN , text : message . upload ? ` ${ message . text } \n📎 ${ message . upload } ` : message . text , id : message . id , voice : message . voice }));
2026-11-06 08:28:04 +00:00
const hubIds = new Set ( sent . map (( entry ) => entry . id ));
2026-11-06 22:20:06 +00:00
const relayed = radio . filter (( entry ) => ! ( entry . who === HUMAN && [... hubIds ]. some (( id ) => entry . text . includes ( `[kvb-orchestrator msg ${ id } ]` ))));
2026-11-06 08:28:04 +00:00
return [... relayed , ... sent ]. sort (( a , b ) => a . ts - b . ts ). slice ( - 80 );
}
2026-11-08 21:18:55 +00:00
function drawAgent () {
if ( ! agentDom ) {
2026-11-06 08:28:04 +00:00
return ;
}
const node = state . graph . nodes . find (( entry ) => entry . id === ui . agent );
const tasks = node ? node . tasks : [];
const claims = node ? node . claims : [];
2026-11-11 07:45:42 +00:00
const part = state . night_parts [ ui . agent ] || state . night ;
agentDom . night . replaceChildren ( pill (...( NIGHT [ part ] || [ part , "neutral" ])));
2026-11-08 21:18:55 +00:00
agentDom . info . replaceChildren (
h ( "p" , { class : "muted" }, claims . length ? `Offices: ${ claims . map (( claim ) => claim . office ). join ( ", " ) } ` : "No office" ),
2026-11-08 17:26:51 +00:00
h ( "h3" , {}, "Tasks" , counter ( tasks . length )),
2026-11-06 08:28:04 +00:00
tasks . length ? h ( "ul" , { class : "mini" }, tasks . map (( task ) => h ( "li" , {},
h ( "div" , { class : "row" }, h ( "code" , {}, task . task ), status ( task . status )),
h ( "span" , {}, dash ( task . title )),
2026-11-08 17:26:51 +00:00
h ( "span" , { class : "roles" }, [ task . worker ? "Worker" : "" , task . letters ? `Roles ${ task . letters } ` : "" ]. filter ( Boolean ). join ( " · " ))))) : h ( "p" , { class : "muted" }, "No open tasks." ),
2026-11-06 08:28:04 +00:00
h ( "h3" , {}, "Claims" , counter ( claims . length )),
claims . length ? h ( "ul" , { class : "mini" }, claims . map (( claim ) => h ( "li" , {},
2026-11-08 17:26:51 +00:00
h ( "div" , { class : "row" }, h ( "code" , {}, claim . office ), claim . beat === null ? null : h ( "span" , { class : "muted" }, "Pulse " , when ( claim . beat ))),
2026-11-06 08:28:04 +00:00
h ( "span" , {}, dash ( claim . work )),
2026-11-08 17:26:51 +00:00
h ( "span" , { class : "muted" }, ` ${ dash ( claim . runtime ) } · since ${ dash ( claim . since ) } ` )))) : h ( "p" , { class : "muted" }, "No claims." ));
2026-11-06 08:28:04 +00:00
const rows = history ( ui . agent );
2026-11-08 21:18:55 +00:00
agentDom . history . replaceChildren ( rows . length
2026-11-07 19:59:47 +00:00
? h ( "div" , {}, rows . map (( row ) => h ( "div" , { class : `bubble ${ row . mine ? "mine" : "theirs" } ` }, h ( "span" , { class : "muted" }, ` ${ row . who } · ` , when ( row . ts )), row . text , voiceReplay ( row . voice ))))
2026-11-08 17:26:51 +00:00
: h ( "p" , { class : "muted" }, "No history in the chosen time range." ));
2026-11-08 21:18:55 +00:00
placePopup ();
2026-11-06 08:28:04 +00:00
}
async function loadSubagents ( key , holder , life ) {
if ( ! SESSION_ID . test ( key )) {
2026-11-08 17:26:51 +00:00
holder . replaceChildren ( h ( "p" , { class : "muted" }, "No session known." ));
2026-11-06 08:28:04 +00:00
return ;
}
let data = null ;
try {
const response = await fetch ( `/api/subagents?session= ${ encodeURIComponent ( key ) } ` , { cache : "no-store" });
data = response . ok ? await response . json () : null ;
} catch {
data = null ;
}
if ( ! data ) {
2026-11-08 17:26:51 +00:00
holder . replaceChildren ( h ( "p" , { class : "muted" }, "Subagents unreadable." ));
2026-11-06 08:28:04 +00:00
return ;
}
life . textContent = stamp ( data );
const rows = data . subagents . map (( row ) => ({... row , session : key })). sort (( a , b ) => a . started - b . started );
holder . replaceChildren ( rows . length ? h ( "ul" , { class : "mini" }, rows . map (( row ) => {
const item = h ( "li" , {},
2026-11-08 17:26:51 +00:00
h ( "div" , { class : "row" }, h ( "button" , { type : "button" , class : "linkish" , title : "Show transcript in the stream" , on : { click : () => focusSub ( row . session , row . id )}}, ` ${ row . type } : ${ row . description } ` ), row . finished ? pill ( "finished" , "neutral" ) : pill ( "running" , "ok" )),
h ( "span" , { class : "muted" }, `Depth ${ row . depth } · ` , when ( row . started ), stamp ( row ) ? ` · ${ stamp ( row ) } ` : "" ));
2026-11-06 08:28:04 +00:00
item . style . marginLeft = ` ${ ( row . depth - 1 ) * 18 } px` ;
return item ;
2026-11-08 17:26:51 +00:00
})) : h ( "p" , { class : "muted" }, "No subagents." ));
2026-11-08 21:18:55 +00:00
placePopup ();
2026-11-06 08:28:04 +00:00
}
2026-11-08 21:18:55 +00:00
async function deliver ( key , text , file , cls ) {
2026-11-06 08:28:04 +00:00
let upload = "" ;
if ( file ) {
if ( file . size > UPLOAD_LIMIT ) {
2026-11-08 17:26:51 +00:00
toast ( "File too large" , [ ` ${ file . name } : at most 50 MiB` ], false );
2026-11-06 08:28:04 +00:00
return false ;
}
const stored = await send ( `/api/uploads?name= ${ encodeURIComponent ( file . name ) } ` , file , "application/octet-stream" );
if ( ! stored . ok || ! stored . data ) {
2026-11-08 17:26:51 +00:00
toast ( "Upload failed" , [ ` ${ stored . status } ${ stored . text } ` . trim ()], false );
2026-11-06 08:28:04 +00:00
return false ;
}
upload = stored . data . id ;
}
2026-11-08 21:18:55 +00:00
return message ( nameOf ( key ), text , upload , cls );
2026-11-06 08:28:04 +00:00
}
2026-11-07 19:59:47 +00:00
function message ( to , text , upload , cls ) {
return radioSend ({ to , text , upload , cls , voice : "" });
2026-11-06 08:28:04 +00:00
}
2026-11-08 21:18:55 +00:00
function openAgent ( who , anchor , closed ) {
2026-11-06 08:28:04 +00:00
const key = agentKey ( who );
const name = nameOf ( key );
2026-11-08 17:26:51 +00:00
const text = h ( "textarea" , { "aria-label" : `Message to ${ name } ` , placeholder : `Message to ${ name } …` });
const file = h ( "input" , { type : "file" , class : "field" , "aria-label" : "Attach file" });
const button = h ( "button" , { type : "submit" , class : "btn primary icon-only" , ... named ( "Send" )}, icon ( "bi-send" ));
2026-11-06 22:20:06 +00:00
const classes = choice ( "to-one" , TO_ONE , "post" );
2026-11-08 21:18:55 +00:00
const form = h ( "form" , { class : "popup-form" }, text , file , classes ,
2026-11-08 17:26:51 +00:00
h ( "p" , { class : "muted" }, "Post arrives at the end of the turn, Call at once with a read receipt; your passkey signs, verifiable with operator_message." ),
2026-11-06 22:20:06 +00:00
h ( "div" , { class : "actions" }, h ( "span" , { class : "spacer" }), micButton ( text ), button ));
2026-11-06 08:28:04 +00:00
form . addEventListener ( "submit" , async ( event ) => {
event . preventDefault ();
if ( ! text . value . trim ()) {
return ;
}
button . disabled = true ;
2026-11-08 21:18:55 +00:00
if ( await deliver ( key , text . value , file . files [ 0 ] || null , picked ( classes ))) {
2026-11-06 08:28:04 +00:00
text . value = "" ;
file . value = "" ;
}
button . disabled = false ;
});
const info = h ( "div" , {});
2026-11-08 17:26:51 +00:00
const subs = h ( "div" , {}, h ( "p" , { class : "muted" }, "Loading subagents …" ));
2026-11-06 08:28:04 +00:00
const log = h ( "div" , { "aria-live" : "polite" });
const life = h ( "span" , { class : "muted" });
2026-11-11 07:45:42 +00:00
const night = h ( "span" , {});
const claimed = nightAgents (). some (([ sid ]) => sid === key );
const nightRow = h ( "div" , { class : "actions agent-night" , role : "group" , "aria-label" : "Day/Night" }, night , h ( "span" , { class : "spacer" }),
claimed ? ACTIONS . slice ( 1 ). map (([ id ]) => actionButton ( id , "btn small" , null , () => [ key ])) : null );
2026-11-06 08:28:04 +00:00
const former = ( people ()[ key ] || { names : []}). names . filter (( entry ) => entry !== name );
2026-11-08 21:18:55 +00:00
openPopup ( name , [
popupHead ( face ( name , true ), name , h ( "span" , { class : "muted" }, former . length ? `Agent · formerly ${ former . join ( ", " ) } ` : "Agent" ), " " , life ),
2026-11-11 07:45:42 +00:00
h ( "div" , { class : "popup-body" }, nightRow , info , h ( "h3" , {}, "Subagents" ), subs , h ( "h3" , {}, "History" ), log ),
2026-11-08 21:18:55 +00:00
form ,
], anchor , () => {
agentDom = null ;
ui . agent = null ;
if ( closed ) {
closed ();
}
});
ui . agent = key ;
2026-11-11 07:45:42 +00:00
agentDom = { info , history : log , night };
2026-11-08 21:18:55 +00:00
drawAgent ();
2026-11-06 08:28:04 +00:00
loadSubagents ( key , subs , life );
text . focus ();
}
function agentNames () {
2026-11-07 17:46:39 +00:00
const names = new Set ([... state . graph . nodes . filter (( node ) => node . kind === "session" || node . kind === "chair" ). map (( node ) => nameOf ( node . id )), ... state . agent_order ]);
2026-11-06 08:28:04 +00:00
return [... names ]. sort (( a , b ) => a . localeCompare ( b ));
}