2026-10-15 07:23:17 -07:00
import { get , post } from "./api.js" ;
import { errorText , h , replace , showAlert } from "./dom.js" ;
import { markdown } from "./markdown.js" ;
import { models } from "./task.js" ;
2026-10-16 08:37:24 -07:00
import { currentSession , focusedProject , focusTask , on , onEach , state , taskState } from "./state.js" ;
2026-10-15 13:30:09 -07:00
import { taskStyle } from "./taskcolor.js" ;
2026-10-15 07:23:17 -07:00
const refreshKinds = new Set ([ "task.merged" , "project.created" ]);
2026-10-15 10:36:32 -07:00
export function mountProject ( root ) {
2026-10-15 07:23:17 -07:00
const todos = new Map ();
const starting = new Set ();
let disposed = false ;
const render = () => {
2026-10-15 10:36:32 -07:00
const scrollTop = root . querySelector ( ".project-view" ) ? . scrollTop ?? 0 ;
2026-10-15 09:59:13 -07:00
const p = focusedProject ();
2026-10-15 10:36:32 -07:00
replace ( root , h ( "div" , { class : "project-view" }, p ? projectBlock ( p , todos . get ( p . id ), starting , start ) : h ( "div" , { class : "empty" }, "No project selected." )));
root . querySelector ( ".project-view" ). scrollTop = scrollTop ;
2026-10-15 07:23:17 -07:00
};
const load = async ( projectId ) => {
try {
const todo = await get ( "/api/projects/{projectId}/todo" , { path : { projectId } });
todos . set ( projectId , todo );
}
catch ( err ) {
todos . set ( projectId , `Couldn't read TODO.md: ${ errorText ( err ) } ` );
}
if ( ! disposed ) {
render ();
}
};
const refresh = () => {
2026-10-15 09:59:13 -07:00
const p = focusedProject ();
2026-10-15 09:40:30 -07:00
if ( p ) {
2026-10-15 07:23:17 -07:00
void load ( p . id );
}
};
const start = async ( project , item ) => {
const key = ` ${ project . id } \n ${ item . title } ` ;
starting . add ( key );
render ();
try {
2026-10-15 19:40:44 -07:00
await post ( "/api/projects/{projectId}/todo/tasks" , { path : { projectId : project . id }, body : { title : item . title , model : models [ 0 ] } });
2026-10-15 07:23:17 -07:00
}
catch ( err ) {
void showAlert ( errorText ( err ));
}
starting . delete ( key );
if ( ! disposed ) {
render ();
}
};
const offs = [
on ( "event" , ( detail ) => {
const ev = detail ;
if ( ! refreshKinds . has ( ev . kind )) {
return ;
}
if ( ev . project_id ) {
void load ( ev . project_id );
}
}),
on ( "connection" , () => {
if ( state . connected ) {
refresh ();
}
}),
on ( "projects" , refresh ),
2026-10-15 09:59:13 -07:00
onEach ([ "focus" , "selection" ], () => {
2026-10-15 09:40:30 -07:00
render ();
2026-10-15 09:59:13 -07:00
const p = focusedProject ();
2026-10-15 09:40:30 -07:00
if ( p && ! todos . has ( p . id )) {
void load ( p . id );
}
}),
2026-10-15 10:36:32 -07:00
onEach ([ "tasks" , "sessions" , "inbox" ], render ),
2026-10-15 07:23:17 -07:00
];
render ();
refresh ();
return () => {
disposed = true ;
for ( const off of offs ) {
off ();
}
};
}
function projectBlock ( project , todo , starting , start ) {
2026-10-15 10:36:32 -07:00
const active = ( state . tasks . get ( project . id ) ?? []). filter (( t ) => t . status === "active" );
return h ( "section" , { class : "project" }, h ( "h2" , {}, project . name , " " , h ( "span" , { class : "repo" }, project . default_branch )), h ( "h3" , {}, "Tasks" ), tasksBlock ( active ), h ( "h3" , {}, "TODO" ), ... todoBlock ( project , todo , active , starting , start ));
}
function tasksBlock ( active ) {
if ( active . length === 0 ) {
return h ( "p" , { class : "empty-row" }, "No active tasks." );
}
return h ( "ul" , { class : "task-list" }, ... active . map (( task ) => {
2026-10-16 08:37:24 -07:00
const ts = taskState ( task );
2026-10-15 10:36:32 -07:00
const count = state . inbox . filter (( item ) => item . task_id === task . id ). length ;
2026-10-15 13:30:09 -07:00
return h ( "li" , { class : task . id === state . selectedTaskId ? "selected" : "" , style : taskStyle ( task . id ) }, h ( "a" , {
2026-10-15 10:36:32 -07:00
href : `#/task/ ${ task . id } ` ,
onclick : ( e ) => {
e . preventDefault ();
focusTask ( task . id );
},
2026-10-16 08:37:24 -07:00
}, h ( "span" , { class : `dot ${ ts ?? "none" } ` , title : ts ?? "no session" }), h ( "span" , { class : "task-title 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" }, ts ?? "no agent" )));
2026-10-15 10:36:32 -07:00
}));
}
function todoBlock ( project , todo , active , starting , start ) {
2026-10-15 07:23:17 -07:00
if ( todo === undefined ) {
2026-10-15 10:36:32 -07:00
return [ h ( "p" , { class : "empty-row" }, "Loading…" )];
2026-10-15 07:23:17 -07:00
}
if ( typeof todo === "string" ) {
2026-10-15 10:36:32 -07:00
return [ h ( "p" , { class : "empty-row error-text" }, todo )];
2026-10-15 07:23:17 -07:00
}
if ( ! todo . exists ) {
2026-10-15 10:36:32 -07:00
return [ h ( "p" , { class : "empty-row" }, `No TODO.md on ${ project . default_branch } .` )];
2026-10-15 07:23:17 -07:00
}
if ( todo . items . length === 0 ) {
2026-10-15 10:36:32 -07:00
return [ h ( "p" , { class : "empty-row" }, "TODO.md has no items." )];
2026-10-15 07:23:17 -07:00
}
const children = [];
let section = "" ;
let list ;
for ( const item of todo . items ) {
if ( ! list || item . section !== section ) {
section = item . section ;
if ( section ) {
2026-10-15 10:36:32 -07:00
children . push ( h ( "h4" , { class : "todo-section" }, section ));
2026-10-15 07:23:17 -07:00
}
list = h ( "ul" , { class : "todo-list" });
children . push ( list );
}
const task = active . find (( t ) => t . todo_item === item . title );
let action ;
if ( task ) {
const sess = currentSession ( task . id );
2026-10-15 09:40:30 -07:00
action = h ( "a" , {
class : "todo-task" ,
href : `#/task/ ${ task . id } ` ,
onclick : ( e ) => {
e . preventDefault ();
focusTask ( task . id );
},
}, h ( "span" , { class : `dot ${ sess ? . state ?? "none" } ` , title : sess ? . state ?? "no session" }), sess ? . state ?? "no agent" );
2026-10-15 07:23:17 -07:00
}
else {
const busy = starting . has ( ` ${ project . id } \n ${ item . title } ` );
const button = h ( "button" , { disabled : busy }, busy ? "Starting…" : "Start" );
button . onclick = () => void start ( project , item );
action = button ;
}
2026-10-15 13:30:09 -07:00
list . append ( h ( "li" , { class : task && task . id === state . selectedTaskId ? "selected" : "" , style : task ? taskStyle ( task . id ) : "" }, h ( "div" , { class : "todo-head" }, h ( "span" , { class : task ? "todo-title task-name" : "todo-title" }, item . title ), action ), item . body && markdown ( item . body , "md todo-body" )));
2026-10-15 07:23:17 -07:00
}
2026-10-15 10:36:32 -07:00
return children ;
2026-10-15 07:23:17 -07:00
}
2026-10-15 10:36:32 -07:00
//# sourceMappingURL=project.js.map