2026-10-16 11:00:09 -07:00
import { get , post } from "./api.js" ;
2026-10-14 22:42:46 -07:00
import { errorText , h , replace , showAlert } from "./dom.js" ;
2026-10-15 22:21:20 -07:00
import { focusLead } from "./panes.js" ;
2026-10-16 11:08:57 -07:00
import { summaryMark } from "./project.js" ;
2026-10-17 09:04:32 -07:00
import { activeTasks , findTask , focusComposer , focusedProject , focusProject , focusTab , focusTask , lastVisits , onEach , openSummary , state , taskState , unreadSummary } from "./state.js" ;
2026-10-16 09:13:28 -07:00
import { confirmMerge , models } from "./task.js" ;
2026-10-15 13:30:09 -07:00
import { taskStyle } from "./taskcolor.js" ;
2026-10-15 22:21:20 -07:00
export function bindShortcuts () {
2026-10-14 09:40:12 -07:00
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 ();
2026-10-15 22:21:20 -07:00
focusLead ();
2026-10-15 19:41:26 -07:00
}
2026-10-15 12:30:10 -07:00
else if ( e . key === "s" ) {
e . preventDefault ();
if ( state . selectedTaskId ) {
void confirmMerge ( state . selectedTaskId );
}
}
2026-10-16 08:39:17 -07:00
else if ( e . key === "/" ) {
e . preventDefault ();
showShortcuts ();
}
2026-10-14 09:40:12 -07:00
});
}
2026-10-16 08:39:17 -07:00
const shortcuts = [
[
"Anywhere" ,
[
[ "⌘K" , "Switch task, or create one" ],
[ "⌘P" , "Open the Project tab" ],
[ "⌘L" , "Open the Lead tab and its composer" ],
[ "⌘S" , "Merge the right pane's task" ],
[ "⌘/" , "Show these shortcuts" ],
[ "Esc" , "Close the switcher or a dialog" ],
],
],
[
"Task switcher" ,
[
[ "↑ ↓" , "Move the highlight" ],
[ "Enter" , "Pick the highlighted row" ],
2026-10-16 09:13:28 -07:00
[ "name: title" , "Create the task in the project whose name starts with name" ],
2026-10-16 11:00:09 -07:00
[ "Esc" , "Back from the workflow list to the task list" ],
2026-10-16 08:39:17 -07:00
],
],
[
"Composer and inbox answers" ,
[
[ "Enter" , "Send" ],
[ "Shift+Enter" , "New line" ],
[ "Enter / Space" , "Pick the focused choice" ],
],
],
[
"Dialogs" ,
[[ "Enter" , "Confirm" ]],
],
[
"Terminal" ,
[[ "Ctrl+…" , "Goes to the shell, not to byre" ]],
],
];
function showShortcuts () {
void showAlert ([
h ( "h2" , { class : "shortcuts-title" }, "Keyboard shortcuts" ),
... shortcuts . map (([ group , rows ]) => h ( "section" , { class : "shortcuts-group" }, h ( "h3" , {}, group ), h ( "dl" , {}, ... rows . flatMap (([ keys , what ]) => [ h ( "dt" , {}, h ( "kbd" , {}, keys )), h ( "dd" , {}, what )])))),
h ( "p" , { class : "hint" }, "Ctrl works in place of ⌘." ),
]);
}
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-16 11:00:09 -07:00
const placeholder = "Go to a task, or type a title to create one…" ;
const input = h ( "input" , { placeholder , 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 = [];
2026-10-16 11:00:09 -07:00
let picking = null ;
2026-10-16 11:08:57 -07:00
const offs = onEach ([ "projects" , "tasks" , "sessions" , "inbox" , "summaries" ], () => render ());
2026-10-14 22:28:25 -07:00
const onEscape = ( e ) => {
if ( e . key !== "Escape" ) {
return ;
}
e . preventDefault ();
e . stopPropagation ();
2026-10-16 11:00:09 -07:00
if ( picking ) {
input . value = picking . typed ;
input . placeholder = placeholder ;
picking = null ;
selected = 0 ;
render ();
return ;
}
2026-10-14 22:28:25 -07:00
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-16 11:08:57 -07:00
const task = findTask ( taskId );
if ( state . inbox . some (( item ) => item . task_id === taskId )) {
focusTask ( taskId , "inbox" );
}
else if ( task && unreadSummary ( task )) {
2026-10-17 09:04:32 -07:00
focusComposer ( taskId );
2026-10-16 11:08:57 -07:00
openSummary ( task );
}
else {
2026-10-17 09:04:32 -07:00
focusComposer ( taskId );
2026-10-16 11:08:57 -07:00
focusTask ( taskId , "transcript" );
}
2026-10-14 22:28:25 -07:00
};
2026-10-17 08:34:49 -07:00
const create = async ( projectId , title , body = {}) => {
2026-10-14 22:28:25 -07:00
close ();
try {
2026-10-17 08:34:49 -07:00
const task = await post ( "/api/projects/{projectId}/tasks" , { path : { projectId }, body : { title , ... body } });
2026-10-14 22:28:25 -07:00
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
};
2026-10-16 11:00:09 -07:00
const pickWorkflow = async ( project , title ) => {
const p = { project , title , typed : input . value , workflows : undefined };
picking = p ;
input . value = "" ;
input . placeholder = `Pick a workflow to start in ${ project . name } : ${ title } ` ;
selected = 0 ;
render ();
input . focus ();
try {
p . workflows = await get ( "/api/workflows" );
}
catch ( err ) {
close ();
void showAlert ( errorText ( err ));
return ;
}
if ( picking === p ) {
render ();
}
};
const renderWorkflows = ( p ) => {
const q = input . value . trim (). toLowerCase ();
rows = ( p . workflows ?? [])
. filter (( w ) => ` ${ w . name } ${ w . description } ` . toLowerCase (). includes ( q ))
. map (( w ) => {
const el = h ( "li" , { class : "new-task" }, h ( "span" , { class : "task-title" }, "Start " , h ( "span" , { class : "workflow-name" }, w . name ), " in " , h ( "span" , { class : "project-name" }, p . project . name ), ": " , p . title , w . description && h ( "span" , { class : "workflow-description" }, w . description )), h ( "span" , { class : "task-state" }, "workflow" ));
2026-10-17 08:34:49 -07:00
return { el , pick : () => void create ( p . project . id , p . title , { workflow : w . name }) };
2026-10-16 11:00:09 -07:00
});
if ( p . workflows === undefined ) {
return "Loading workflows…" ;
}
return p . workflows . length === 0 ? "No saved workflows." : "No workflow matches." ;
};
2026-10-14 09:40:12 -07:00
const render = () => {
2026-10-16 11:00:09 -07:00
const empty = picking ? renderWorkflows ( picking ) : renderTasks ();
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 ) => {
e . preventDefault ();
row . pick ();
};
});
replace ( list , ... rows . map (( r ) => r . el ), rows . length === 0 && h ( "li" , { class : "empty-row" }, empty ));
2026-10-17 08:24:22 -07:00
rows [ selected ] ? . el . scrollIntoView ({ block : "nearest" });
2026-10-16 11:00:09 -07:00
};
const renderTasks = () => {
2026-10-16 09:13:28 -07:00
const typed = input . value . trim ();
const prefix = /^([^:\s]+):\s*(.*)$/ . exec ( typed );
const named = prefix ? state . projects . filter (( p ) => p . name . toLowerCase (). startsWith ( prefix [ 1 ]. toLowerCase ())) : [];
const targets = named . length > 0 ? named : undefined ;
const title = targets ? prefix [ 2 ] : typed ;
2026-10-14 22:28:25 -07:00
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 ()
2026-10-16 09:13:28 -07:00
. filter (({ project , task }) => targets ? targets . some (( p ) => p . id === project . id ) && task . title . toLowerCase (). includes ( q ) : ` ${ project . name } ${ task . title } ` . toLowerCase (). includes ( q ))
2026-10-16 18:11:30 -07:00
. map (( m ) => ({ ... m , count : inbox ( m . task . id ), visited : m . task . id === state . selectedTaskId ? Infinity : ( visits [ m . task . id ] ?? 0 ) }))
2026-10-14 23:23:25 -07:00
. 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 }) => {
2026-10-16 08:37:24 -07:00
const ts = taskState ( task );
2026-10-16 11:08:57 -07:00
const el = h ( "li" , { style : taskStyle ( task . id ) }, h ( "span" , { class : `dot ${ ts . kind } ` , title : ts . title }), h ( "span" , { class : "task-title" }, h ( "span" , { class : "project-name" }, project . name ), " " , task . parent_task_id !== undefined && h ( "span" , { class : "subtask-mark" }, ` ${ findTask ( task . parent_task_id ) ? . title ?? task . parent_task_id } ↳ ` ), h ( "span" , { class : "task-name" }, task . title )), count > 0 && h ( "span" , { class : "badge" , title : "inbox items" }, count ), summaryMark ( task ), h ( "span" , { class : `task-state ${ ts . kind } ` , title : ts . title }, ts . label ));
2026-10-14 22:28:25 -07:00
return { el , pick : () => go ( task . id ) };
});
2026-10-16 09:13:28 -07:00
for ( const p of ( targets ?? state . projects ). filter (( p ) => ` ${ p . name } project` . toLowerCase (). includes ( q ))) {
2026-10-15 10:36:32 -07:00
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 );
},
});
}
2026-10-16 09:13:28 -07:00
const fallback = focusedProject () ?? state . projects [ 0 ];
for ( const project of title ? ( targets ?? ( fallback ? [ fallback ] : [])) : []) {
const el = h ( "li" , { class : "new-task" }, h ( "span" , { class : "task-title" }, "New task in " , h ( "span" , { class : "project-name" }, project . name ), ": " , title ), h ( "span" , { class : "task-state" , title : "Model for the first message" }, models [ 0 ]));
2026-10-14 22:28:25 -07:00
rows . push ({ el , pick : () => void create ( project . id , title ) });
2026-10-17 08:34:49 -07:00
const start = h ( "li" , { class : "new-task" }, h ( "span" , { class : "task-title" }, "Start in " , h ( "span" , { class : "project-name" }, project . name ), ": " , title ), h ( "span" , { class : "task-state" , title : "The title is the agent's first message" }, models [ 0 ]));
rows . push ({ el : start , pick : () => void create ( project . id , title , { prompt : title , model : models [ 0 ] }) });
2026-10-16 11:00:09 -07:00
const wf = h ( "li" , { class : "new-task" }, h ( "span" , { class : "task-title" }, "Start workflow… in " , h ( "span" , { class : "project-name" }, project . name ), ": " , title ), h ( "span" , { class : "task-state" }, "workflow" ));
rows . push ({ el : wf , pick : () => void pickWorkflow ( project , title ) });
2026-10-14 22:28:25 -07:00
}
2026-10-16 11:00:09 -07:00
return "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 ();
}
2026-10-15 22:21:20 -07:00
//# sourceMappingURL=switcher.js.map