2026-10-14 22:28:25 -07:00
import { post } from "./api.js" ;
2026-10-14 22:42:46 -07:00
import { errorText , h , replace , showAlert } from "./dom.js" ;
2026-10-15 09:40:30 -07:00
import { openSettings } from "./settings.js" ;
2026-10-15 12:03:01 -07:00
import { activeTasks , currentSession , focusedProject , focusProject , focusTab , focusTask , lastVisits , onEach , state } from "./state.js" ;
2026-10-15 09:40:30 -07:00
import { openStatus } from "./status.js" ;
2026-10-15 12:30:10 -07:00
import { confirmMerge } from "./task.js" ;
2026-10-15 13:30:09 -07:00
import { taskStyle } from "./taskcolor.js" ;
2026-10-15 09:40:30 -07:00
export function mountNavbar ( root ) {
const status = h ( "button" , { class : "nav-link" }, "Status" );
status . onclick = openStatus ;
const settings = h ( "button" , { class : "nav-link" }, "Settings" );
settings . onclick = openSettings ;
2026-10-14 09:40:12 -07:00
const conn = h ( "span" , { class : "connection down" , title : "connecting" });
2026-10-14 22:28:25 -07:00
const connLabel = h ( "span" , { class : "connection-label" }, "reconnecting…" );
2026-10-14 09:40:12 -07:00
const me = h ( "span" , { class : "me" });
const render = () => {
conn . className = state . connected ? "connection up" : "connection down" ;
conn . title = state . connected ? "live" : "reconnecting" ;
2026-10-14 22:28:25 -07:00
connLabel . hidden = state . connected ;
2026-10-14 09:40:12 -07:00
replace ( me , state . me ? . display_name ?? "" );
};
2026-10-15 12:03:01 -07:00
onEach ([ "me" , "connection" ], render );
replace ( root , h ( "a" , { href : "#/" , class : "brand" }, "byre" ), h ( "span" , { class : "spacer" }), status , settings , connLabel , conn , me );
2026-10-14 09:40:12 -07:00
render ();
addEventListener ( "keydown" , ( e ) => {
2026-10-15 11:49:18 -07:00
if ( ! ( e . metaKey || e . ctrlKey ) || document . querySelector ( "dialog[open]" )) {
return ;
}
if ( e . key === "k" ) {
2026-10-14 09:40:12 -07:00
e . preventDefault ();
openSwitcher ();
}
2026-10-15 11:49:18 -07:00
else if ( e . key === "p" ) {
e . preventDefault ();
focusTab ( "project" );
}
2026-10-15 19:41:26 -07:00
else if ( e . key === "l" ) {
e . preventDefault ();
document . querySelector ( ".view-lead .composer textarea" ) ? . focus ();
}
2026-10-15 12:30:10 -07:00
else if ( e . key === "s" ) {
e . preventDefault ();
if ( state . selectedTaskId ) {
void confirmMerge ( state . selectedTaskId );
}
}
2026-10-14 09:40:12 -07:00
});
}
2026-10-15 12:03:01 -07:00
export function openSwitcher () {
2026-10-14 09:40:12 -07:00
if ( document . querySelector ( ".switcher" )) {
return ;
}
2026-10-14 22:28:25 -07:00
const input = h ( "input" , { placeholder : "Go to a task, or type a title to create one…" , autocomplete : "off" });
2026-10-14 09:40:12 -07:00
const list = h ( "ul" , { class : "switcher-list" });
const backdrop = h ( "div" , { class : "switcher-backdrop" });
const box = h ( "div" , { class : "switcher" }, input , list );
let selected = 0 ;
2026-10-14 22:28:25 -07:00
let rows = [];
const offs = onEach ([ "projects" , "tasks" , "sessions" , "inbox" ], () => render ());
const onEscape = ( e ) => {
if ( e . key !== "Escape" ) {
return ;
}
e . preventDefault ();
e . stopPropagation ();
close ();
};
addEventListener ( "keydown" , onEscape , { capture : true });
2026-10-14 09:40:12 -07:00
const close = () => {
2026-10-14 22:28:25 -07:00
offs ();
removeEventListener ( "keydown" , onEscape , { capture : true });
2026-10-14 09:40:12 -07:00
backdrop . remove ();
box . remove ();
};
const go = ( taskId ) => {
close ();
2026-10-15 19:00:18 -07:00
focusTask ( taskId , state . inbox . some (( item ) => item . task_id === taskId ) ? "inbox" : "transcript" );
2026-10-14 22:28:25 -07:00
};
const create = async ( projectId , title ) => {
close ();
try {
const task = await post ( "/api/projects/{projectId}/tasks" , { path : { projectId }, body : { title } });
go ( task . id );
}
catch ( err ) {
2026-10-14 22:42:46 -07:00
void showAlert ( errorText ( err ));
2026-10-14 22:28:25 -07:00
}
2026-10-14 09:40:12 -07:00
};
const render = () => {
2026-10-14 22:28:25 -07:00
const title = input . value . trim ();
const q = title . toLowerCase ();
const inbox = ( taskId ) => state . inbox . filter (( item ) => item . task_id === taskId ). length ;
2026-10-14 23:23:25 -07:00
const visits = lastVisits ();
2026-10-14 22:28:25 -07:00
const matches = activeTasks ()
. filter (({ project , task }) => ` ${ project . name } ${ task . title } ` . toLowerCase (). includes ( q ))
2026-10-14 23:23:25 -07:00
. map (( m ) => ({ ... m , count : inbox ( m . task . id ), visited : visits [ m . task . id ] ?? 0 }))
. sort (( a , b ) => Number ( b . count > 0 ) - Number ( a . count > 0 ) || a . visited - b . visited );
2026-10-14 22:28:25 -07:00
rows = matches . map (({ project , task , count }) => {
const sess = currentSession ( task . id );
2026-10-15 13:30:09 -07:00
const el = h ( "li" , { style : taskStyle ( task . id ) }, h ( "span" , { class : `dot ${ sess ? . state ?? "none" } ` , title : sess ? . state ?? "no session" }), h ( "span" , { class : "task-title" }, h ( "span" , { class : "project-name" }, project . name ), " " , h ( "span" , { class : "task-name" }, task . title )), count > 0 && h ( "span" , { class : "badge" , title : "inbox items" }, count ), task . merge_queued_at !== undefined && h ( "span" , { class : "task-state warn" , title : "Waiting for the merge in progress in this project to finish" }, "merge held" ), h ( "span" , { class : "task-state" }, sess ? . state ?? "no agent" ));
2026-10-14 22:28:25 -07:00
return { el , pick : () => go ( task . id ) };
});
2026-10-15 10:36:32 -07:00
for ( const p of state . projects . filter (( p ) => ` ${ p . name } project` . toLowerCase (). includes ( q ))) {
const el = h ( "li" , {}, h ( "span" , { class : "dot none" }), h ( "span" , { class : "task-title" }, h ( "span" , { class : "project-name" }, p . name )), h ( "span" , { class : "task-state" }, "project" ));
2026-10-15 09:59:13 -07:00
rows . push ({
el ,
pick : () => {
close ();
focusProject ( p . id );
},
});
}
const project = focusedProject () ?? state . projects [ 0 ];
2026-10-14 22:28:25 -07:00
if ( title && project ) {
const el = h ( "li" , { class : "new-task" }, h ( "span" , { class : "task-title" }, "New task in " , h ( "span" , { class : "project-name" }, project . name ), ": " , title ));
rows . push ({ el , pick : () => void create ( project . id , title ) });
}
selected = Math . min ( selected , Math . max ( rows . length - 1 , 0 ));
rows . forEach (( row , i ) => {
row . el . classList . toggle ( "selected" , i === selected );
row . el . onmousedown = ( e ) => {
2026-10-14 09:40:12 -07:00
e . preventDefault ();
2026-10-14 22:28:25 -07:00
row . pick ();
2026-10-14 09:40:12 -07:00
};
2026-10-14 22:28:25 -07:00
});
replace ( list , ... rows . map (( r ) => r . el ), rows . length === 0 && h ( "li" , { class : "empty-row" }, "No active tasks. Type a title to create one." ));
2026-10-14 09:40:12 -07:00
};
input . oninput = () => {
selected = 0 ;
render ();
};
input . onkeydown = ( e ) => {
2026-10-14 22:28:25 -07:00
if ( e . key === "ArrowDown" ) {
2026-10-14 09:40:12 -07:00
e . preventDefault ();
2026-10-14 22:28:25 -07:00
selected = Math . min ( selected + 1 , rows . length - 1 );
2026-10-14 09:40:12 -07:00
render ();
}
else if ( e . key === "ArrowUp" ) {
e . preventDefault ();
selected = Math . max ( selected - 1 , 0 );
render ();
}
else if ( e . key === "Enter" ) {
2026-10-14 22:28:25 -07:00
e . preventDefault ();
rows [ selected ] ? . pick ();
2026-10-14 09:40:12 -07:00
}
};
backdrop . onclick = close ;
2026-10-15 12:03:01 -07:00
const anchor = document . querySelector ( ".task-picker" ). getBoundingClientRect ();
box . style . top = ` ${ anchor . bottom + 4 } px` ;
2026-10-14 09:40:12 -07:00
document . body . append ( backdrop , box );
2026-10-15 22:15:36 -07:00
const viewport = document . documentElement . clientWidth ;
const left = anchor . left + anchor . width / 2 - box . offsetWidth / 2 ;
box . style . left = ` ${ Math . max ( 8 , Math . min ( left , viewport - box . offsetWidth - 8 )) } px` ;
2026-10-14 09:40:12 -07:00
render ();
input . focus ();
}
//# sourceMappingURL=navbar.js.map