2026-10-14 09:40:12 -07:00
import { post } from "./api.js" ;
2026-10-14 19:35:34 -07:00
import { mountChanges } from "./changes.js" ;
2026-10-14 22:42:46 -07:00
import { ago , errorText , h , replace , showAlert } from "./dom.js" ;
2026-10-15 07:27:17 -07:00
import { image , inlineMarkdown , markdown } from "./markdown.js" ;
2026-10-16 08:42:48 -07:00
import { checkedAnswer , clearAnswer , dropTextAnswer , flushDrafts , hasTextAnswer , setCheckedAnswer , setTextAnswer , textAnswer } from "./drafts.js" ;
2026-10-16 22:49:06 -07:00
import { imageDrafts , imageTray , releaseImages , uploadImages } from "./images.js" ;
2026-10-15 07:27:17 -07:00
import { diagram } from "./mermaid.js" ;
2026-10-15 13:31:14 -07:00
import { focusTab , onEach , state } from "./state.js" ;
2026-10-15 13:30:09 -07:00
import { taskStyle } from "./taskcolor.js" ;
2026-10-16 10:57:34 -07:00
const delegateText = "Use your judgment for this decision and continue within the existing task instructions." ;
2026-10-14 09:40:12 -07:00
export function mountInbox ( root ) {
const list = h ( "div" , { class : "inbox-list" });
const cards = new Map ();
2026-10-15 09:40:30 -07:00
const nothing = h ( "div" , { class : "empty" }, "Nothing waiting on you in this task." );
const noTask = h ( "div" , { class : "empty" }, "No task selected." );
2026-10-15 22:00:12 -07:00
let shown ;
2026-10-14 09:40:12 -07:00
const render = () => {
2026-10-15 09:40:30 -07:00
const taskId = state . selectedTaskId ;
const items = state . inbox . filter (( i ) => i . task_id === taskId ). sort (( a , b ) => a . created_at . localeCompare ( b . created_at ));
2026-10-14 09:40:12 -07:00
const keep = new Set ( items . map (( i ) => i . id ));
for ( const id of cards . keys ()) {
if ( ! keep . has ( id )) {
cards . delete ( id );
}
}
2026-10-14 10:37:04 -07:00
const cardFor = ( item ) => {
2026-10-14 09:40:12 -07:00
const sig = JSON . stringify ( item );
const cached = cards . get ( item . id );
if ( cached && cached . sig === sig ) {
return cached . el ;
}
2026-10-14 23:34:39 -07:00
const el = card ( item );
2026-10-14 09:40:12 -07:00
cards . set ( item . id , { sig , el });
return el ;
2026-10-14 10:37:04 -07:00
};
2026-10-15 09:40:30 -07:00
if ( ! taskId ) {
reconcile ( list , [ noTask ]);
return ;
2026-10-14 23:54:13 -07:00
}
2026-10-15 22:00:12 -07:00
const top = items [ 0 ] ? . id ;
reconcile ( list , top ? [ cardFor ( items [ 0 ])] : [ nothing ]);
if ( top !== shown ) {
shown = top ;
list . scrollTop = 0 ;
}
2026-10-14 09:40:12 -07:00
};
2026-10-15 09:40:30 -07:00
const off = onEach ([ "inbox" , "selection" ], render );
2026-10-14 19:35:34 -07:00
const changes = h ( "section" , { class : "inbox-changes" });
2026-10-15 09:40:30 -07:00
replace ( root , h ( "div" , { class : "inbox-view" }, changes , list ));
2026-10-14 19:35:34 -07:00
const offChanges = mountChanges ( changes , true );
2026-10-14 09:40:12 -07:00
render ();
2026-10-14 19:35:34 -07:00
return () => {
off ();
offChanges ();
};
2026-10-14 09:40:12 -07:00
}
2026-10-14 23:54:13 -07:00
function reconcile ( parent , nodes ) {
nodes . forEach (( node , i ) => {
const at = parent . childNodes [ i ];
if ( at !== node ) {
parent . insertBefore ( node , at ?? null );
}
});
while ( parent . childNodes . length > nodes . length ) {
parent . lastChild . remove ();
}
}
2026-10-14 23:34:39 -07:00
function clickable ( el , go ) {
el . onclick = ( e ) => {
2026-10-16 07:04:24 -07:00
if ( ! e . target . closest ( "a, button" )) {
2026-10-14 23:34:39 -07:00
go ();
}
};
el . onkeydown = ( e ) => {
2026-10-16 07:04:24 -07:00
if ( e . target === el && ( e . key === "Enter" || e . key === " " )) {
2026-10-14 23:34:39 -07:00
e . preventDefault ();
go ();
}
};
return el ;
}
function card ( item ) {
2026-10-14 09:40:12 -07:00
const p = item . payload ;
const body = h ( "div" , { class : "card-body" });
const actions = h ( "div" , { class : "card-actions" });
2026-10-16 13:56:46 -07:00
const el = h ( "article" , { class : `card ${ item . kind } ` }, h ( "header" , {}, h ( "span" , { class : "kind" }, item . kind . replace ( "_" , " " )), h ( "time" , { dateTime : item . created_at , title : item . created_at }, ago ( item . created_at ))), body , thread ( item ), actions );
2026-10-14 23:34:39 -07:00
const busy = () => el . classList . contains ( "submitting" );
const lock = ( on ) => {
el . classList . toggle ( "submitting" , on );
for ( const c of el . querySelectorAll ( "button, input, textarea" )) {
c . disabled = on ;
}
2026-10-14 09:40:12 -07:00
};
2026-10-16 22:49:06 -07:00
const imageKey = `inbox: ${ item . id } ` ;
const submit = async ( response , tray ) => {
2026-10-14 23:34:39 -07:00
if ( busy ()) {
2026-10-14 10:37:04 -07:00
return ;
}
2026-10-14 23:34:39 -07:00
lock ( true );
2026-10-16 22:49:06 -07:00
const imgs = tray ? . take () ?? [];
2026-10-14 23:34:39 -07:00
try {
2026-10-16 22:49:06 -07:00
const images = await uploadImages ( imgs );
await post ( "/api/inbox/responses" , { body : { responses : [{ ... response , images }] } });
releaseImages ( imgs );
2026-10-16 08:42:48 -07:00
clearAnswer ( item . id );
2026-10-15 13:31:14 -07:00
const others = state . inbox . some (( i ) => i . task_id === item . task_id && i . id !== item . id );
if ( ! others && state . focus . tab === "inbox" && state . selectedTaskId === item . task_id ) {
focusTab ( "transcript" );
}
2026-10-14 23:34:39 -07:00
}
catch ( err ) {
2026-10-16 22:49:06 -07:00
tray ? . restore ( imgs );
2026-10-14 23:34:39 -07:00
void showAlert ( errorText ( err ));
lock ( false );
renderActions ();
}
};
const options = p [ "options" ] ?? [];
const recommended = p [ "recommended" ] ?? [];
const multiple = p [ "multiple" ] === true ;
const seen = new Set ();
const choice = ( text ) => {
if ( ! options . includes ( text ) || seen . has ( text )) {
return null ;
}
seen . add ( text );
2026-10-16 07:04:24 -07:00
const rec = recommended . includes ( text );
const tag = rec && h ( "span" , { class : "recommended" }, "recommended" );
const name = h ( "span" , { class : "choice-name" }, inlineMarkdown ( text ));
2026-10-14 23:34:39 -07:00
if ( ! multiple ) {
2026-10-16 07:04:24 -07:00
const head = h ( "div" , { class : "choice-head" }, name , tag );
return clickable ( h ( "div" , { class : rec ? "pick choice-box rec" : "pick choice-box" , role : "button" , tabIndex : 0 }, head ), () => {
2026-10-14 23:34:39 -07:00
void submit ({ item_id : item . id , type : "option" , options : [ text ] });
});
}
2026-10-16 08:42:48 -07:00
const input = h ( "input" , { type : "checkbox" , checked : checkedAnswer ( item . id ). includes ( text ) });
2026-10-14 23:34:39 -07:00
input . onchange = () => {
2026-10-16 08:42:48 -07:00
const now = checkedAnswer ( item . id ). filter (( o ) => o !== text );
2026-10-14 23:34:39 -07:00
if ( input . checked ) {
now . push ( text );
}
2026-10-16 08:42:48 -07:00
setCheckedAnswer ( item . id , options . filter (( o ) => now . includes ( o )));
2026-10-14 23:34:39 -07:00
renderActions ();
};
2026-10-16 07:04:24 -07:00
return h ( "label" , { class : rec ? "option choice-box rec" : "option choice-box" }, h ( "div" , { class : "choice-head" }, input , name , tag ));
2026-10-14 23:34:39 -07:00
};
const textEntry = ( label , placeholder , shared ) => {
2026-10-16 22:49:06 -07:00
const open = hasTextAnswer ( item . id ) || imageDrafts . has ( imageKey );
2026-10-16 08:42:48 -07:00
const text = h ( "textarea" , { rows : 3 , placeholder , value : textAnswer ( item . id ) ?? "" });
2026-10-16 22:49:06 -07:00
const send = h ( "button" , { type : "button" , class : "primary" }, "Submit" );
const editor = h ( "div" , { class : "text-editor" , hidden : ! open });
const empty = () => ! text . value . trim () && ! imageDrafts . has ( imageKey );
const tray = imageTray ( editor , text , imageKey , () => {
send . disabled = empty ();
shared ? . onInput ();
});
editor . append ( tray . el , text , shared ? "" : h ( "div" , { class : "submit-row" }, send ));
2026-10-14 23:34:39 -07:00
const block = clickable ( h ( "div" , { class : "pick other" , role : "button" , tabIndex : 0 , hidden : open }, label ), () => {
2026-10-16 08:42:48 -07:00
setTextAnswer ( item . id , "" );
2026-10-14 23:34:39 -07:00
block . hidden = true ;
editor . hidden = false ;
text . focus ();
});
const go = () => {
2026-10-14 23:54:13 -07:00
if ( shared ) {
2026-10-16 22:49:06 -07:00
shared . onSubmit ( tray );
2026-10-14 23:54:13 -07:00
return ;
}
2026-10-16 22:49:06 -07:00
if ( ! empty ()) {
void submit ({ item_id : item . id , type : "other" , text : text . value }, tray );
2026-10-14 23:34:39 -07:00
}
};
send . onclick = go ;
text . oninput = () => {
2026-10-16 08:42:48 -07:00
setTextAnswer ( item . id , text . value );
2026-10-16 22:49:06 -07:00
send . disabled = empty ();
2026-10-14 23:34:39 -07:00
shared ? . onInput ();
};
text . onblur = () => {
2026-10-16 22:49:06 -07:00
if ( empty () && ! busy ()) {
2026-10-16 08:42:48 -07:00
dropTextAnswer ( item . id );
2026-10-14 23:34:39 -07:00
editor . hidden = true ;
block . hidden = false ;
shared ? . onInput ();
}
2026-10-16 08:42:48 -07:00
flushDrafts ();
2026-10-14 23:34:39 -07:00
};
text . onkeydown = ( e ) => {
if ( e . key === "Enter" && ! e . shiftKey && ! e . isComposing ) {
e . preventDefault ();
go ();
}
};
2026-10-16 22:49:06 -07:00
return { el : h ( "div" , { class : "text-entry" }, block , editor ), tray };
2026-10-14 23:34:39 -07:00
};
2026-10-16 22:18:00 -07:00
const delegate = () => {
const button = h ( "button" , { type : "button" , class : "delegate" }, "Let the agent decide" );
button . onclick = () => void submit ({ item_id : item . id , type : "other" , text : delegateText });
return button ;
};
2026-10-14 23:34:39 -07:00
const renderActions = () => {
switch ( item . kind ) {
case "question" : {
const placeholder = "Your answer, a question back, or new instructions" ;
const send = h ( "button" , { type : "button" , class : "primary" }, "Submit" );
2026-10-16 08:42:48 -07:00
const typed = () => ( textAnswer ( item . id ) ?? "" ). trim ();
2026-10-14 23:34:39 -07:00
const refresh = () => {
2026-10-16 22:49:06 -07:00
send . disabled = ! typed () && ! imageDrafts . has ( imageKey ) && ( ! multiple || checkedAnswer ( item . id ). length === 0 );
2026-10-14 23:34:39 -07:00
};
2026-10-16 22:49:06 -07:00
const go = ( tray ) => {
2026-10-16 22:18:00 -07:00
const options = multiple ? checkedAnswer ( item . id ) : [];
2026-10-14 23:54:13 -07:00
if ( options . length > 0 ) {
2026-10-16 22:49:06 -07:00
void submit ({ item_id : item . id , type : "option" , options , text : typed () ? textAnswer ( item . id ) : undefined }, tray );
2026-10-14 23:54:13 -07:00
return ;
}
2026-10-16 22:49:06 -07:00
if ( typed () || imageDrafts . has ( imageKey )) {
void submit ({ item_id : item . id , type : "other" , text : textAnswer ( item . id ) ?? "" }, tray );
2026-10-14 23:34:39 -07:00
}
};
2026-10-14 23:54:13 -07:00
const other = textEntry ( "Other…" , placeholder , { onInput : refresh , onSubmit : go });
2026-10-16 22:49:06 -07:00
send . onclick = () => go ( other . tray );
2026-10-14 23:34:39 -07:00
refresh ();
2026-10-16 22:49:06 -07:00
replace ( actions , other . el , h ( "div" , { class : "submit-row" }, delegate (), send ));
2026-10-14 23:34:39 -07:00
return ;
}
default : {
const dismiss = h ( "button" , { type : "button" }, "Dismiss" );
dismiss . onclick = () => void submit ({ item_id : item . id , type : "dismiss" });
2026-10-16 22:49:06 -07:00
replace ( actions , h ( "div" , { class : "reply-row" }, textEntry ( "Reply…" , "Reply to the agent" ). el , dismiss ));
2026-10-14 23:34:39 -07:00
}
}
2026-10-14 09:40:12 -07:00
};
switch ( item . kind ) {
case "question" :
2026-10-15 07:27:17 -07:00
replace ( body , markdown ( String ( p [ "context" ]), "md context" , { choice , taskId : item . task_id }), markdown ( String ( p [ "question" ]), "md question" , { taskId : item . task_id }), h ( "p" , { class : "why" }, h ( "strong" , {}, "Blocked on: " ), inlineMarkdown ( String ( p [ "why_blocked" ]), { taskId : item . task_id })), attachments ( p [ "attachments" ] ?? [], item . task_id ));
2026-10-14 09:40:12 -07:00
break ;
case "session_error" :
replace ( body , h ( "p" , { class : "error-text" }, String ( p [ "error" ])));
break ;
case "rate_limited" : {
const limit = ( p [ "rate_limit" ] ?? {});
const resets = typeof limit [ "resetsAt" ] === "number" ? new Date ( limit [ "resetsAt" ] * 1000 ). toLocaleString () : "unknown" ;
replace ( body , h ( "p" , { class : "error-text" }, String ( p [ "error" ])), h ( "p" , {}, ` ${ p [ "vendor" ] } ${ limit [ "rateLimitType" ] ?? "" } limit resets ${ resets } ` ));
break ;
}
}
2026-10-16 10:57:34 -07:00
el . addEventListener ( "keydown" , ( e ) => {
if ( e . key !== "ArrowDown" && e . key !== "ArrowUp" ) {
return ;
}
const stops = [... el . querySelectorAll ( ".pick, .option.choice-box input" )]. filter (( s ) => s . offsetParent !== null );
const at = stops . indexOf ( e . target );
if ( at < 0 ) {
return ;
}
e . preventDefault ();
stops [ Math . min ( Math . max ( at + ( e . key === "ArrowDown" ? 1 : - 1 ), 0 ), stops . length - 1 )]. focus ();
});
2026-10-14 09:40:12 -07:00
renderActions ();
return el ;
}
function thread ( item ) {
if ( item . responses . length === 0 ) {
return null ;
}
2026-10-16 22:49:06 -07:00
return h ( "ul" , { class : "thread" }, ... item . responses . map (( r ) => h ( "li" , {}, h ( "strong" , {}, r . user ), ` ${ r . type . replace ( "_" , " " ) } ` , r . options ? `: ${ r . options . join ( "; " ) } ` : "" , r . text ? ` — ${ r . text } ` : "" , r . images . length > 0 ? ` ( ${ r . images . length === 1 ? "1 image" : ` ${ r . images . length } images` } )` : "" )));
2026-10-14 09:40:12 -07:00
}
2026-10-15 07:27:17 -07:00
function attachments ( list , taskId ) {
2026-10-14 09:40:12 -07:00
if ( list . length === 0 ) {
return null ;
}
return h ( "ul" , { class : "attachments" }, ... list . map (( a ) => {
const caption = a . caption ? h ( "div" , { class : "caption" }, a . caption ) : null ;
switch ( a . kind ) {
case "file" : {
const lines = a . start_line ? `: ${ a . start_line }${ a . end_line ? `- ${ a . end_line } ` : "" } ` : "" ;
return h ( "li" , {}, h ( "code" , {}, ` ${ a . path ?? "" }${ lines } ` ), caption );
}
case "diff" :
return h ( "li" , {}, "diff " , h ( "code" , {}, a . ref ?? "" ), caption );
case "image" :
2026-10-15 07:27:17 -07:00
return h ( "li" , {}, a . image || a . path ? image ( a . image || a . path , a . caption ?? "" , taskId ) : null , caption );
2026-10-14 09:40:12 -07:00
case "diagram" :
2026-10-15 07:27:17 -07:00
return h ( "li" , {}, diagram ( a . mermaid ?? "" ), caption );
2026-10-14 09:40:12 -07:00
case "task" :
2026-10-15 13:30:09 -07:00
return h ( "li" , {}, h ( "a" , { href : `#/task/ ${ a . task_id } ` , class : "task-link" , style : taskStyle ( a . task_id ?? "" ) }, "task " , a . task_id ?? "" ), caption );
2026-10-14 09:40:12 -07:00
case "snapshot" :
return h ( "li" , {}, "snapshot " , h ( "code" , {}, a . snapshot_id ?? "" ), caption );
case "url" :
return h ( "li" , {}, safeLink ( a . url ?? "" ), caption );
}
}));
}
function safeLink ( url ) {
const ok = /^https?:\/\//i . test ( url );
return ok ? h ( "a" , { href : url , target : "_blank" , rel : "noopener noreferrer" }, url ) : h ( "code" , {}, url );
}
//# sourceMappingURL=inbox.js.map