2026-10-16 11:01:53 -07:00
import { get } from "./api.js" ;
import { errorText , h , replace } from "./dom.js" ;
import { markdown } from "./markdown.js" ;
2026-10-16 11:25:53 -07:00
import { diagram , redraw } from "./mermaid.js" ;
2026-10-16 11:01:53 -07:00
import { findTask , focusTask , onEach , state } from "./state.js" ;
2026-10-16 11:25:53 -07:00
const stateColors = {
waiting : [ "--surface" , "--border" , "--muted" ],
running : [ "--accent-tint" , "--accent" , "--text" ],
idle : [ "--warn-tint" , "--warn" , "--text" ],
finished : [ "--ok-tint" , "--ok" , "--text" ],
merged : [ "--ok-tint" , "--ok" , "--text" ],
cleaned_up : [ "--ok-tint" , "--ok" , "--text" ],
discarded : [ "--danger-tint" , "--danger" , "--text" ],
};
2026-10-16 11:01:53 -07:00
function graph ( run ) {
2026-10-16 11:25:53 -07:00
const css = getComputedStyle ( document . documentElement );
const v = ( name ) => css . getPropertyValue ( name ). trim ();
2026-10-16 11:01:53 -07:00
const id = new Map ( run . stages . map (( s , i ) => [ s . name , `s ${ i } ` ]));
const lines = [ "flowchart TD" ];
2026-10-16 11:25:53 -07:00
for ( const [ state , [ fill , stroke , text ]] of Object . entries ( stateColors )) {
lines . push ( ` classDef ${ state } fill: ${ v ( fill ) } ,stroke: ${ v ( stroke ) } ,color: ${ v ( text ) } ` );
}
2026-10-16 11:01:53 -07:00
for ( const s of run . stages ) {
2026-10-16 11:25:53 -07:00
lines . push ( ` ${ id . get ( s . name ) } [" ${ s . name . replaceAll ( '"' , "#quot;" ) } "]::: ${ s . state } ` );
2026-10-16 11:01:53 -07:00
}
for ( const s of run . stages ) {
for ( const a of s . after ) {
lines . push ( ` ${ id . get ( a ) } --> ${ id . get ( s . name ) } ` );
}
}
return lines . join ( "\n" );
}
function attemptLabel ( a ) {
switch ( a . state ) {
case "running" :
return "running" ;
case "idle" :
return "idle, not finished" ;
case "finished" :
return "finished" ;
case "merged" :
return "merged" ;
case "discarded" :
return `discarded before finishing ${ a . discarded_by ? ` by ${ a . discarded_by } ` : "" } ` ;
case "cleaned_up" :
return a . discarded_by === "byre" ? "finished, then discarded by the run's cleanup" : `finished, then discarded ${ a . discarded_by ? ` by ${ a . discarded_by } ` : "" } ` ;
}
}
function stageLabel ( s ) {
if ( s . state !== "waiting" ) {
return attemptLabel ( s . attempts . findLast (( a ) => a . output !== undefined ) ?? s . attempts . at ( - 1 ));
}
if ( s . waiting_on . length > 0 ) {
return `waiting on ${ s . waiting_on . map (( n ) => `“ ${ n } ”` ). join ( ", " ) } ` ;
}
return "waiting to start" ;
}
function taskLink ( taskId , label ) {
const a = h ( "a" , { href : `#/task/ ${ taskId } ` , class : "workflow-task" }, label );
a . onclick = ( e ) => {
e . preventDefault ();
focusTask ( taskId );
};
return a ;
}
function attemptName ( run , taskId ) {
for ( const s of run . stages ) {
const i = s . attempts . findIndex (( a ) => a . task_id === taskId );
if ( i >= 0 ) {
return s . attempts . length > 1 ? `“ ${ s . name } ” attempt ${ i + 1 } ` : `“ ${ s . name } ”` ;
}
}
return findTask ( taskId ) ? . title ?? taskId ;
}
function attemptRow ( run , stage , a , index ) {
const fed = run . stages . flatMap (( s ) => s . attempts . filter (( d ) => d . inputs . some (( i ) => i . task_id === a . task_id )). map (( d ) => d . task_id ));
return h ( "li" , { class : `workflow-attempt ${ a . state }${ a . task_id === state . selectedTaskId ? " current" : "" } ` }, h ( "div" , { class : "workflow-attempt-head" }, stage . attempts . length > 1 ? h ( "span" , { class : "workflow-attempt-n" }, `# ${ index + 1 } ` ) : null , taskLink ( a . task_id , a . title ), h ( "span" , { class : `workflow-state ${ a . state } ` }, attemptLabel ( a )), a . discarding ? h ( "span" , { class : "workflow-state discarding" }, "discarding…" ) : null ), a . forked_from ? h ( "div" , { class : "workflow-line" }, "fork of " , taskLink ( a . forked_from , attemptName ( run , a . forked_from ))) : null , a . inputs . length > 0
? h ( "div" , { class : "workflow-line" }, "started from " , ... a . inputs . flatMap (( i , n ) => [ n > 0 ? ", " : null , taskLink ( i . task_id , attemptName ( run , i . task_id ))]))
: null , fed . length > 0 ? h ( "div" , { class : "workflow-line fed" }, "fed " , ... fed . flatMap (( id , n ) => [ n > 0 ? ", " : null , taskLink ( id , attemptName ( run , id ))])) : null , a . output !== undefined
? h ( "details" , { class : "workflow-output" }, h ( "summary" , {}, "finish_stage output" , a . commit ? h ( "code" , {}, ` ${ a . commit . slice ( 0 , 10 ) } ` ) : null ), markdown ( a . output , "md" , { taskId : a . task_id }))
: null );
}
function stageCard ( run , s ) {
return h ( "li" , { class : `workflow-stage ${ s . state } ` }, h ( "header" , {}, h ( "span" , { class : "workflow-stage-name" }, s . name ), h ( "span" , { class : `workflow-state ${ s . state } ` }, stageLabel ( s ))), s . after . length > 0 ? h ( "div" , { class : "workflow-line" }, `after ${ s . after . map (( n ) => `“ ${ n } ”` ). join ( ", " ) } ` ) : h ( "div" , { class : "workflow-line" }, "starts from the brief" ), s . attempts . length > 0 ? h ( "ol" , { class : "workflow-attempts" }, ... s . attempts . map (( a , i ) => attemptRow ( run , s , a , i ))) : null );
}
export function mountWorkflow ( root ) {
const head = h ( "header" , { class : "workflow-header" });
const graphBox = h ( "div" , { class : "workflow-graph" });
const list = h ( "div" , { class : "workflow-body" });
let run ;
let drawnRun = "" ;
2026-10-16 11:25:53 -07:00
let drawnSrc = "" ;
let drawn ;
2026-10-16 11:01:53 -07:00
let error = "" ;
let target = 0 ;
let loading = false ;
let again = false ;
let disposed = false ;
const load = async () => {
const taskId = state . selectedTaskId ;
if ( ! taskId || ! findTask ( taskId ) ? . workflow_run ) {
render ();
return ;
}
if ( loading ) {
again = true ;
return ;
}
loading = true ;
const started = ++ target ;
try {
const fresh = await get ( "/api/tasks/{taskId}/workflow/run" , { path : { taskId } });
if ( started === target ) {
run = fresh ;
error = "" ;
}
}
catch ( err ) {
if ( started === target ) {
error = errorText ( err );
}
}
loading = false ;
if ( disposed ) {
return ;
}
if ( started === target ) {
render ();
}
if ( again ) {
again = false ;
void load ();
}
};
const render = () => {
const task = state . selectedTaskId ? findTask ( state . selectedTaskId ) : undefined ;
if ( ! task ? . workflow_run ) {
replace ( head );
replace ( graphBox );
drawnRun = "" ;
2026-10-16 11:25:53 -07:00
drawn = undefined ;
2026-10-16 11:01:53 -07:00
replace ( list , h ( "div" , { class : "empty" }, task ? "This task isn't part of a workflow run." : "No task selected." ));
return ;
}
if ( error ) {
replace ( list , h ( "div" , { class : "empty error-text" }, error ));
return ;
}
if ( ! run || run . run !== task . workflow_run ) {
replace ( list , h ( "div" , { class : "empty" }, "Loading…" ));
return ;
}
replace ( head , h ( "span" , { class : "workflow-name" }, run . name ), h ( "span" , { class : "workflow-title" }, run . title ), h ( "span" , { class : `workflow-state ${ run . finished ? "finished" : "running" } ` }, run . finished ? "workflow finished" : "in progress" ));
2026-10-16 11:25:53 -07:00
const src = graph ( run );
if ( drawnRun !== run . run || ! drawn ) {
2026-10-16 11:01:53 -07:00
drawnRun = run . run ;
2026-10-16 11:25:53 -07:00
drawnSrc = src ;
drawn = diagram ( src );
replace ( graphBox , drawn );
}
else if ( drawnSrc !== src ) {
drawnSrc = src ;
void redraw ( drawn , src );
2026-10-16 11:01:53 -07:00
}
replace ( list , run . brief . trim () ? h ( "details" , { class : "workflow-brief" }, h ( "summary" , {}, "Brief" ), markdown ( run . brief , "md" , { taskId : task . id })) : null , h ( "ol" , { class : "workflow-stages" }, ... run . stages . map (( s ) => stageCard ( run , s ))));
};
const relevant = ( ev ) => {
if ( ev . seq === 0 || ! run ) {
return false ;
}
if ( ev . kind === "task.created" ) {
return ev . payload [ "workflow_run" ] === run . run ;
}
const ids = new Set ( run . stages . flatMap (( s ) => s . attempts . map (( a ) => a . task_id )));
return ev . task_id !== undefined && ids . has ( ev . task_id ) && ( ev . kind . startsWith ( "task." ) || ev . kind . startsWith ( "session." ));
};
const offs = [
onEach ([ "selection" ], () => {
target ++ ;
run = undefined ;
error = "" ;
render ();
void load ();
}),
onEach ([ "tasks" ], () => {
if ( ! run && ! loading && ! error ) {
void load ();
}
}),
onEach ([ "event" ], ( ev ) => {
if ( relevant ( ev )) {
void load ();
}
}),
];
replace ( root , h ( "div" , { class : "workflow-view" }, head , h ( "div" , { class : "workflow-scroll" }, graphBox , list )));
render ();
void load ();
return () => {
disposed = true ;
for ( const off of offs ) {
off ();
}
};
}
//# sourceMappingURL=workflow.js.map