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" ;
import { diagram } from "./mermaid.js" ;
2026-10-15 09:40:30 -07:00
import { onEach , state } from "./state.js" ;
2026-10-14 23:34:39 -07:00
const checked = new Map ();
const texts = new Map ();
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-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 23:34:39 -07:00
for ( const draft of [ checked , texts ]) {
for ( const id of draft . keys ()) {
if ( ! state . inbox . some (( i ) => i . id === id )) {
draft . delete ( id );
}
2026-10-14 09:40:12 -07:00
}
}
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 09:40:30 -07:00
reconcile ( list , items . length > 0 ? items . map ( cardFor ) : [ nothing ]);
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 ) => {
if ( ! e . target . closest ( "a" )) {
go ();
}
};
el . onkeydown = ( e ) => {
if ( e . key === "Enter" || e . key === " " ) {
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-14 10:37:04 -07:00
const el = h ( "article" , { class : `card ${ item . kind } ${ item . priority } ` }, h ( "header" , {}, h ( "span" , { class : `priority ${ item . priority } ` }, item . priority ), 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-14 23:34:39 -07:00
const submit = async ( response ) => {
if ( busy ()) {
2026-10-14 10:37:04 -07:00
return ;
}
2026-10-14 23:34:39 -07:00
lock ( true );
try {
await post ( "/api/inbox/responses" , { body : { responses : [ response ] } });
checked . delete ( item . id );
texts . delete ( item . id );
}
catch ( err ) {
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 );
const tag = recommended . includes ( text ) && h ( "span" , { class : "recommended" }, "recommended" );
if ( ! multiple ) {
return clickable ( h ( "div" , { class : "pick" , role : "button" , tabIndex : 0 }, inlineMarkdown ( text ), tag ), () => {
void submit ({ item_id : item . id , type : "option" , options : [ text ] });
});
}
const input = h ( "input" , { type : "checkbox" , checked : ( checked . get ( item . id ) ?? []). includes ( text ) });
input . onchange = () => {
const now = ( checked . get ( item . id ) ?? []). filter (( o ) => o !== text );
if ( input . checked ) {
now . push ( text );
}
checked . set ( item . id , options . filter (( o ) => now . includes ( o )));
renderActions ();
};
return h ( "label" , { class : "option" }, input , " " , inlineMarkdown ( text ), tag );
};
const textEntry = ( label , placeholder , shared ) => {
const open = texts . has ( item . id );
const text = h ( "textarea" , { rows : 3 , placeholder , value : texts . get ( item . id ) ?? "" });
const send = h ( "button" , { type : "button" , class : "primary" , disabled : ! text . value . trim () }, "Submit" );
const editor = h ( "div" , { class : "text-editor" , hidden : ! open }, text , ! shared && h ( "div" , { class : "submit-row" }, send ));
const block = clickable ( h ( "div" , { class : "pick other" , role : "button" , tabIndex : 0 , hidden : open }, label ), () => {
texts . set ( item . id , "" );
block . hidden = true ;
editor . hidden = false ;
text . focus ();
});
const go = () => {
2026-10-14 23:54:13 -07:00
if ( shared ) {
shared . onSubmit ();
return ;
}
2026-10-14 23:34:39 -07:00
if ( text . value . trim ()) {
void submit ({ item_id : item . id , type : "other" , text : text . value });
}
};
send . onclick = go ;
text . oninput = () => {
texts . set ( item . id , text . value );
send . disabled = ! text . value . trim ();
shared ? . onInput ();
};
text . onblur = () => {
if ( ! text . value . trim () && ! busy ()) {
texts . delete ( item . id );
editor . hidden = true ;
block . hidden = false ;
shared ? . onInput ();
}
};
text . onkeydown = ( e ) => {
if ( e . key === "Enter" && ! e . shiftKey && ! e . isComposing ) {
e . preventDefault ();
go ();
}
};
return h ( "div" , { class : "text-entry" }, block , editor );
};
const renderActions = () => {
switch ( item . kind ) {
case "report" : {
const ok = h ( "button" , { type : "button" , class : "primary" }, "OK" );
ok . onclick = () => void submit ({ item_id : item . id , type : "dismiss" });
replace ( actions , h ( "div" , { class : "ack" }, ok ));
return ;
}
case "question" : {
const placeholder = "Your answer, a question back, or new instructions" ;
if ( ! multiple ) {
replace ( actions , textEntry ( "Other…" , placeholder ));
return ;
2026-10-14 09:40:12 -07:00
}
2026-10-14 23:34:39 -07:00
const send = h ( "button" , { type : "button" , class : "primary" }, "Submit" );
const typed = () => ( texts . get ( item . id ) ?? "" ). trim ();
const refresh = () => {
send . disabled = ! typed () && ( checked . get ( item . id ) ?? []). length === 0 ;
};
2026-10-14 23:54:13 -07:00
const go = () => {
const options = checked . get ( item . id ) ?? [];
if ( options . length > 0 ) {
void submit ({ item_id : item . id , type : "option" , options , text : typed () ? texts . get ( item . id ) : undefined });
return ;
}
2026-10-14 23:34:39 -07:00
if ( typed ()) {
void submit ({ item_id : item . id , type : "other" , text : texts . get ( item . id ) });
}
};
2026-10-14 23:54:13 -07:00
const other = textEntry ( "Other…" , placeholder , { onInput : refresh , onSubmit : go });
send . onclick = go ;
2026-10-14 23:34:39 -07:00
refresh ();
replace ( actions , other , h ( "div" , { class : "submit-row" }, send ));
return ;
}
default : {
const dismiss = h ( "button" , { type : "button" }, "Dismiss" );
dismiss . onclick = () => void submit ({ item_id : item . id , type : "dismiss" });
replace ( actions , h ( "div" , { class : "reply-row" }, textEntry ( "Reply…" , "Reply to the agent" ), dismiss ));
}
}
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 "report" :
2026-10-15 07:27:17 -07:00
replace ( body , markdown ( String ( p [ "text" ] ?? "" ), "md summary" , { taskId : 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 ;
}
}
renderActions ();
return el ;
}
function thread ( item ) {
if ( item . responses . length === 0 ) {
return null ;
}
2026-10-14 23:34:39 -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 } ` : "" )));
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" :
return h ( "li" , {}, h ( "a" , { href : `#/task/ ${ a . task_id } ` }, "task " , a . task_id ?? "" ), caption );
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