2026-10-15 18:34:12 -07:00
import { get , post , type ByreEvent , type Schemas } from "./api.js" ;
import { ago , errorText , h , replace , showAlert } from "./dom.js" ;
2026-10-15 16:33:17 -07:00
import { markdown } from "./markdown.js" ;
2026-10-15 17:46:41 -07:00
import { onEach , pickReview , state } from "./state.js" ;
2026-10-15 16:33:17 -07:00
type Review = Schemas [ "Review" ];
const severities = [ "critical" , "high" , "medium" , "low" , "info" ];
export function isReviewEvent ( ev : ByreEvent ) : boolean {
return ev . kind . startsWith ( "review." );
}
2026-10-15 18:34:12 -07:00
function findingsOf ( r : Review ) : Record < string , unknown >[] {
2026-10-15 16:33:17 -07:00
const res = ( r . result ?? {}) as Record < string , unknown >;
2026-10-15 18:34:12 -07:00
return Array . isArray ( res [ "findings" ]) ? ( res [ "findings" ] as Record < string , unknown >[]) : [];
}
export function findingCount ( r : Review ) : number {
return r . state === "done" ? findingsOf ( r ). length : 0 ;
}
export function latestFindingCount ( events : ByreEvent []) : number {
const finished = events . filter (( ev ) => ev . kind === "review.finished" ). map (( ev ) => ev . payload as unknown as Review );
const top = finished . reduce < Review | undefined >(( best , r ) => ( ! best || r . snapshot_n >= best . snapshot_n ? r : best ), undefined );
return top ? findingCount ( top ) : 0 ;
}
export function reviewCounts ( r : Review ) : string {
const findings = findingsOf ( r );
2026-10-15 16:33:17 -07:00
const parts = severities . map (( s ) => [ s , findings . filter (( f ) => f [ "severity" ] === s ). length ] as const ). filter (([, n ]) => n > 0 );
return parts . length > 0 ? parts . map (([ s , n ]) => ` ${ n } ${ s } ` ). join ( ", " ) : "no findings" ;
}
function stateText ( r : Review ) : string {
switch ( r . state ) {
case "running" :
return `reviewing on ${ r . vendor } ( ${ r . model } )…` ;
case "done" :
return reviewCounts ( r );
case "failed" :
return "failed" ;
}
}
export function reviewCard ( r : Review , taskId : string | undefined ) : HTMLElement {
return h (
"article" ,
{ class : `review ${ r . state } ` },
h (
"header" ,
{},
h ( "span" , { class : "review-snapshot" }, `snapshot # ${ r . snapshot_n } ` ),
h ( "span" , { class : `review-state ${ r . state } ` }, stateText ( r )),
h ( "time" , { dateTime : r.created_at , title : r.created_at }, ago ( r . created_at )),
),
r . state === "failed" ? h ( "p" , { class : "error-text" }, r . error ) : null ,
r . state === "done" ? reviewResult ( r , taskId ) : null ,
);
}
function reviewResult ( r : Review , taskId : string | undefined ) : HTMLElement {
const res = ( r . result ?? {}) as Record < string , unknown >;
2026-10-15 18:34:12 -07:00
const findings = findingsOf ( r );
2026-10-15 16:33:17 -07:00
const md = { taskId };
return h (
"div" ,
{ class : "review-result" },
markdown ( String ( res [ "summary" ] ?? "" ), "md review-summary" , md ),
findings . length > 0
? h (
"ol" ,
{ class : "findings" },
... findings . map (( f ) => {
const severity = String ( f [ "severity" ] ?? "" );
const where = typeof f [ "path" ] === "string" && f [ "path" ] ? ` ${ f [ "path" ] }${ typeof f [ "line" ] === "number" && f [ "line" ] > 0 ? `: ${ f [ "line" ] } ` : "" } ` : "" ;
return h (
"li" ,
{ class : `finding ${ severity } ` },
h (
"div" ,
{ class : "finding-head" },
h ( "span" , { class : `severity ${ severity } ` }, severity ),
typeof f [ "category" ] === "string" && f [ "category" ] ? h ( "span" , { class : "finding-category" }, f [ "category" ]) : null ,
h ( "span" , { class : "finding-title" }, String ( f [ "title" ] ?? "" )),
where ? h ( "code" , { class : "finding-where" }, where ) : null ,
2026-10-15 18:34:12 -07:00
taskId ? fixButton ( taskId , r , f , where ) : null ,
2026-10-15 16:33:17 -07:00
),
markdown ( String ( f [ "detail" ] ?? "" ), "md finding-detail" , md ),
);
}),
)
: null ,
);
}
2026-10-15 18:34:12 -07:00
function fixButton ( taskId : string , r : Review , f : Record < string , unknown >, where : string ) : HTMLElement {
const button = h ( "button" , { type : "button" , class : "finding-fix" , title : "Send this finding to the task's agent to fix" }, "fix" );
button . onclick = async () => {
const text = `Fix this finding from the review of snapshot # ${ r . snapshot_n } : \ n \ n**[ ${ String ( f [ "severity" ] ?? "" ) } ] ${ String ( f [ "title" ] ?? "" ) } ** ${ where ? ` (\` ${ where } \`)` : "" } \ n \ n ${ String ( f [ "detail" ] ?? "" ) } ` ;
button . disabled = true ;
try {
const sessions = await get ( "/api/tasks/{taskId}/sessions" , { path : { taskId } });
const live = sessions . findLast (( s ) => s . state !== "completed" );
if ( live && live . state !== "idle" ) {
button . disabled = false ;
void showAlert ( "The agent is mid-turn. Send this once its turn ends." );
return ;
}
if ( live ) {
await post ( "/api/sessions/{sessionId}/messages" , { path : { sessionId : live.id }, body : { text } });
} else {
await post ( "/api/tasks/{taskId}/sessions" , { path : { taskId }, body : { prompt : text } });
}
replace ( button , "sent" );
} catch ( err ) {
button . disabled = false ;
void showAlert ( errorText ( err ));
}
};
return button ;
}
2026-10-15 16:33:17 -07:00
export function mountReviews ( root : HTMLElement ) : () => void {
2026-10-15 17:46:41 -07:00
const head = h ( "header" , { class : "reviews-header" });
const body = h ( "div" , { class : "review-list" });
2026-10-15 16:33:17 -07:00
let reviews : Review [] | undefined ;
let error = "" ;
let target = 0 ;
let disposed = false ;
const load = async () => {
const taskId = state . selectedTaskId ;
if ( ! taskId ) {
render ();
return ;
}
const started = ++ target ;
try {
const fresh = await get ( "/api/tasks/{taskId}/reviews" , { path : { taskId } });
if ( started === target ) {
reviews = fresh ;
error = "" ;
}
} catch ( err ) {
if ( started === target ) {
reviews = undefined ;
error = errorText ( err );
}
}
if ( ! disposed && started === target ) {
render ();
}
};
const render = () => {
const taskId = state . selectedTaskId ;
2026-10-15 17:46:41 -07:00
replace ( head );
head . hidden = true ;
2026-10-15 16:33:17 -07:00
if ( ! taskId ) {
2026-10-15 17:46:41 -07:00
replace ( body , h ( "div" , { class : "empty" }, "No task selected." ));
2026-10-15 16:33:17 -07:00
return ;
}
if ( error ) {
2026-10-15 17:46:41 -07:00
replace ( body , h ( "div" , { class : "empty error-text" }, error ));
2026-10-15 16:33:17 -07:00
return ;
}
if ( ! reviews ) {
2026-10-15 17:46:41 -07:00
replace ( body , h ( "div" , { class : "empty" }, "Loading…" ));
2026-10-15 16:33:17 -07:00
return ;
}
if ( reviews . length === 0 ) {
2026-10-15 17:46:41 -07:00
replace ( body , h ( "div" , { class : "empty" }, "No reviews yet. Each new snapshot of the task is reviewed by an agent from the other vendor." ));
2026-10-15 16:33:17 -07:00
return ;
}
2026-10-15 17:46:41 -07:00
const n = state . reviewed ?? reviews . at ( - 1 ) ! . snapshot_n ;
const shown = reviews . findLast (( r ) => r . snapshot_n === n );
const ns = [... new Set ([ n , ... reviews . map (( r ) => r . snapshot_n )])]. sort (( a , b ) => b - a );
const picker = h ( "select" , { title : "Snapshot whose review to show" }, ... ns . map (( s ) => h ( "option" , { value : String ( s ) }, `snapshot # ${ s } ` )));
picker . value = String ( n );
picker . onchange = () => pickReview ( Number ( picker . value ));
const step = ( to : number , label : string ) => {
const b = h ( "button" , { type : "button" , class : "snap-step" , disabled : ! ns . includes ( to ) }, label );
b . onclick = () => pickReview ( to );
return b ;
};
const next = ns . filter (( s ) => s > n ). at ( - 1 );
const prev = ns . find (( s ) => s < n );
head . hidden = false ;
replace ( head , picker , step ( prev ?? n - 1 , `← # ${ prev ?? n - 1 } ` ), step ( next ?? n + 1 , `# ${ next ?? n + 1 } →` ));
replace ( body , shown ? reviewCard ( shown , taskId ) : h ( "div" , { class : "empty" }, `No review of snapshot # ${ n } yet.` ));
2026-10-15 16:33:17 -07:00
};
const offs = [
onEach ([ "selection" ], () => {
target ++ ;
reviews = undefined ;
error = "" ;
render ();
void load ();
}),
onEach ([ "transcript" ], ( fresh ) => {
if (( fresh as ByreEvent []). some ( isReviewEvent )) {
void load ();
}
}),
2026-10-15 17:46:41 -07:00
onEach ([ "review" ], render ),
2026-10-15 16:33:17 -07:00
];
2026-10-15 17:46:41 -07:00
replace ( root , h ( "div" , { class : "reviews-view" }, head , body ));
2026-10-15 16:33:17 -07:00
render ();
void load ();
return () => {
disposed = true ;
for ( const off of offs ) {
off ();
}
};
}