2026-11-06 08:28:04 +00:00
function paletteItems () {
return [
2026-11-08 17:26:51 +00:00
... Nav . TABS . flatMap (([ id , label ], index ) => [{ label : `View: ${ label } ` , hint : String ( index + 1 ), run : () => go ( id , null )}, ...( Nav . SUBS [ id ] || []). map (([ sub , name ]) => ({ label : `View: ${ label } › ${ name } ` , hint : Nav . hashOf ( sub , null ), run : () => go ( sub , null )}))]),
2026-11-07 23:43:26 +00:00
...[ "agents" , "artifacts" , "tasks" ]. flatMap (( screen ) => Nav . viewsOf ( screen ). map (([ id , name ]) => ({ label : ` ${ Nav . TABS . find (([ tab ]) => tab === screen )[ 1 ] } : ${ name } ` , hint : Nav . hashOf ( screen , id ), run : () => go ( screen , id )}))),
2026-11-08 17:26:51 +00:00
... Object . entries ( state . views ). map (([ id , view ]) => ({ label : `Stream: ${ view . label } ` , hint : "Category" , run : () => useView ( id )})),
... userViews . map (( view ) => ({ label : `Stream: ${ view . name } ` , hint : "saved view" , run : () => useSaved ( view )})),
2026-11-08 22:06:31 +00:00
... agentNames (). map (( name ) => ({ label : name , hint : "Agent" , face : name , run : () => openAgent ( name )})),
2026-11-09 22:22:46 +00:00
{ label : "Radio …" , hint : "Post, Call, Newsletter, Broadcast, Pan-Pan, Security, Mayday; the gear holds tones and read aloud" , run : openRadio },
2026-11-08 17:26:51 +00:00
... ACTIONS . map (([ id , label ]) => ({ label , hint : id === "clear-mayday" ? "Mayday" : "Day/Night" , run : () => confirmAct ( id )})),
{ label : "Priority …" , hint : "agent order in the hierarchy" , run : openAgentOrder },
{ label : "Firewall …" , hint : "automatic grants" , run : () => go ( "rules" , null )},
2026-11-09 22:22:46 +00:00
{ label : "Settings …" , hint : "Ambience, location, colors, music, notifications, internet radio" , run : settingsSheet },
2026-11-08 17:26:51 +00:00
{ label : settings . music . on ? "Music off" : "Music on" , hint : "Ambience" , run : toggleMusic },
{ label : space . look ? "Leave the sky" : "Look at the sky" , hint : "h" , run : () => skyLook ( ! space . look )},
{ label : "Toggle control room" , hint : "Ambience" , run : toggleLeitstand },
{ label : "Enable notifications" , hint : "Ambience" , run : askNotify },
{ label : "Keyboard shortcuts" , hint : "?" , run : helpSheet },
2026-11-06 08:28:04 +00:00
];
}
function drawPalette () {
if ( palette . plan ) {
return ;
}
const raw = palette . input . value . trim ();
const q = raw . toLowerCase ();
const items = paletteItems (). filter (( item ) => ! q || item . label . toLowerCase (). includes ( q ) || item . hint . toLowerCase (). includes ( q ));
if ( raw ) {
items . push ( state . llm
2026-11-08 17:26:51 +00:00
? { label : `AI command: “ ${ raw } ”` , hint : "Tab" , keep : true , run : () => askModel ( raw )}
: { label : "AI command is off" , hint : "set KVB_ORCHESTRATOR_LLM_URL and KVB_ORCHESTRATOR_LLM_MODEL in orchestrator/.env" , keep : true , run : () => toast ( "AI command is off" , [ "Set KVB_ORCHESTRATOR_LLM_URL, KVB_ORCHESTRATOR_LLM_MODEL and KVB_ORCHESTRATOR_LLM_KEY_FILE in orchestrator/.env and reinstall." ], false )});
2026-11-06 08:28:04 +00:00
}
palette . items = items ;
palette . index = Math . min ( palette . index , Math . max ( items . length - 1 , 0 ));
palette . list . replaceChildren (... items . map (( item , index ) => h ( "li" , { role : "option" , "aria-selected" : String ( index === palette . index ), on : {
click : () => runPalette ( index ),
mousemove : () => {
if ( palette . index !== index ) {
palette . index = index ;
drawPalette ();
}
},
2026-11-08 22:06:31 +00:00
}}, h ( "span" , { class : item . face ? "who" : null }, item . face ? face ( item . face , false ) : null , item . label ), h ( "span" , { class : "muted" }, item . hint ))));
2026-11-06 08:28:04 +00:00
const current = palette . list . children [ palette . index ];
if ( current ) {
current . scrollIntoView ({ block : "nearest" });
}
}
function runPalette ( index ) {
const item = palette . items [ index ];
if ( ! item ) {
return ;
}
if ( ! item . keep ) {
closePalette ();
}
item . run ();
}
async function askModel ( text ) {
palette . plan = { actions : [], rejected : [], busy : true };
2026-11-08 17:26:51 +00:00
palette . list . replaceChildren ( h ( "li" , {}, h ( "span" , { class : "muted" }, "Asking the model … (only the command, ids and titles leave the hub)" )));
2026-11-06 08:28:04 +00:00
const result = await sendJson ( "/api/nl" , { text });
if ( ! palette . plan ) {
return ;
}
if ( ! result . ok || ! result . data ) {
palette . plan = null ;
2026-11-08 17:26:51 +00:00
palette . list . replaceChildren ( h ( "li" , {}, h ( "span" , {}, `AI command failed: ${ result . status } ${ result . text } ` . trim ())));
2026-11-06 08:28:04 +00:00
return ;
}
palette . plan = result . data ;
const actions = result . data . actions . map (( action ) => h ( "pre" , {}, action . summary ));
2026-11-08 17:26:51 +00:00
const rejected = result . data . rejected . map (( entry ) => h ( "p" , { class : "muted" }, `discarded: ${ entry . reason } · ${ entry . action } ` ));
2026-11-06 08:28:04 +00:00
palette . list . replaceChildren ( h ( "li" , { class : "palette-plan" },
2026-11-08 17:26:51 +00:00
actions . length ? actions : h ( "p" , { class : "muted" }, "The model proposed no valid action." ),
2026-11-06 08:28:04 +00:00
rejected ,
2026-11-08 17:26:51 +00:00
h ( "p" , { class : "muted" }, actions . length ? "Enter runs exactly these actions · Esc cancels" : "Esc closes" )));
2026-11-06 08:28:04 +00:00
}
async function executePlan ( plan ) {
for ( const action of plan . actions ) {
const args = action . args ;
if ( action . name === "approve" || action . name === "reject" || action . name === "choose" ) {
await submit ( args . items , action . summary . split ( "\n" )[ 0 ]);
} else if ( action . name === "snooze" || action . name === "pin" ) {
for ( const key of args . keys ) {
const item = state . inbox . find (( entry ) => entry . kind === key . kind && entry . id === key . id );
if ( item ) {
await mark ( item , action . name === "snooze" ? nowSeconds () + args . hours * 3600 : item . until , action . name === "pin" ? true : item . pinned );
}
}
} else if ( action . name === "view" ) {
useView ( args . name );
} else if ( action . name === "open" ) {
go ( args . view , null );
} else if ( action . name === "message" ) {
2026-11-06 22:20:06 +00:00
await message ( args . to , args . text , "" , "post" );
2026-11-06 08:28:04 +00:00
} else if ( action . name === "filter" ) {
filter . q = args . q ;
if ( ui . screen === "stream" && stream . dom ) {
stream . dom . search . value = args . q ;
refilter ();
} else {
go ( "stream" , null );
}
}
}
}
function openPalette () {
const box = document . getElementById ( "palette" );
2026-11-08 17:26:51 +00:00
palette . input = h ( "input" , { type : "text" , placeholder : "View, agent, action, or a command in words …" , "aria-label" : "Command" , role : "combobox" , "aria-expanded" : "true" });
2026-11-06 08:28:04 +00:00
palette . list = h ( "ul" , { class : "palette-list" , role : "listbox" });
palette . index = 0 ;
palette . plan = null ;
palette . input . addEventListener ( "input" , () => {
palette . index = 0 ;
palette . plan = null ;
drawPalette ();
});
palette . input . addEventListener ( "keydown" , ( event ) => {
if ( palette . plan && event . key === "Enter" ) {
event . preventDefault ();
const plan = palette . plan ;
if ( ! plan . busy && plan . actions . length ) {
closePalette ();
executePlan ( plan );
}
} else if ( event . key === "ArrowDown" || event . key === "ArrowUp" ) {
event . preventDefault ();
const size = Math . max ( palette . items . length , 1 );
palette . index = ( palette . index + ( event . key === "ArrowDown" ? 1 : size - 1 )) % size ;
drawPalette ();
} else if ( event . key === "Tab" && palette . input . value . trim () && ! palette . plan ) {
event . preventDefault ();
palette . index = palette . items . length - 1 ;
runPalette ( palette . index );
} else if ( event . key === "Enter" ) {
event . preventDefault ();
runPalette ( palette . index );
}
});
2026-11-08 17:26:51 +00:00
box . replaceChildren ( h ( "div" , { class : "palette-box" , role : "dialog" , "aria-label" : "Commands" }, h ( "div" , { class : "palette-row" }, palette . input , micButton ( palette . input )), palette . list ));
2026-11-06 08:28:04 +00:00
box . hidden = false ;
drawPalette ();
palette . input . focus ();
}
function closePalette () {
document . getElementById ( "palette" ). hidden = true ;
palette . plan = null ;
}