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-14 10:37:04 -07:00
import { inlineMarkdown , markdown } from "./markdown.js" ;
2026-10-14 09:40:12 -07:00
import { findTask , onEach , state } from "./state.js" ;
const drafts = new Map ();
const modes = new Map ();
let scope = "all" ;
export function mountInbox ( root ) {
const tabs = h ( "nav" , { class : "scope-tabs" });
const list = h ( "div" , { class : "inbox-list" });
const submitBar = h ( "div" , { class : "submit-bar" });
const cards = new Map ();
const visible = () => {
const task = state . selectedTaskId ? findTask ( state . selectedTaskId ) : undefined ;
if ( scope === "task" && task ) {
return state . inbox . filter (( i ) => i . task_id === task . id );
}
if ( scope === "project" && task ) {
return state . inbox . filter (( i ) => i . project_id === task . project_id );
}
return state . inbox ;
};
const renderTabs = () => {
const hasTask = state . selectedTaskId !== undefined ;
const tab = ( s , label ) => {
const b = h ( "button" , { class : s === scope ? "active" : "" , disabled : s !== "all" && ! hasTask }, label );
b . onclick = () => {
scope = s ;
render ();
};
return b ;
};
replace ( tabs , tab ( "all" , "All" ), tab ( "project" , "This project" ), tab ( "task" , "This task" ));
};
const renderSubmit = () => {
const ready = [... drafts . values ()]. filter (( d ) => state . inbox . some (( i ) => i . id === d . item_id ));
if ( ready . length === 0 ) {
replace ( submitBar );
return ;
}
const button = h ( "button" , { class : "primary" }, `Submit ${ ready . length } ${ ready . length === 1 ? "response" : "responses" } ` );
button . onclick = async () => {
button . disabled = true ;
try {
await post ( "/api/inbox/responses" , { body : { responses : ready } });
for ( const d of ready ) {
drafts . delete ( d . item_id );
modes . delete ( d . item_id );
}
}
catch ( err ) {
2026-10-14 22:42:46 -07:00
void showAlert ( errorText ( err ));
2026-10-14 09:40:12 -07:00
}
button . disabled = false ;
render ();
};
replace ( submitBar , button );
};
const render = () => {
if ( scope !== "all" && ! state . selectedTaskId ) {
scope = "all" ;
}
renderTabs ();
const items = visible ();
const keep = new Set ( items . map (( i ) => i . id ));
for ( const id of cards . keys ()) {
if ( ! keep . has ( id )) {
cards . delete ( id );
}
}
for ( const id of drafts . keys ()) {
if ( ! state . inbox . some (( i ) => i . id === id )) {
drafts . delete ( id );
modes . 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 ;
}
const el = card ( item , () => {
renderSubmit ();
});
cards . set ( item . id , { sig , el });
return el ;
2026-10-14 10:37:04 -07:00
};
const els = groupByTask ( items ). map (( g ) => {
const task = findTask ( g . taskId );
const project = state . projects . find (( p ) => p . id === g . projectId );
return h ( "section" , { class : `task-group ${ g . blocked ? "blocked" : "" } ` }, h ( "h3" , {}, h ( "span" , { class : "project-name" }, project ? . name ?? g . projectId , " › " ), h ( "a" , { href : `#/task/ ${ g . taskId } ` }, task ? . title ?? g . taskId )), ... g . items . map ( cardFor ));
2026-10-14 09:40:12 -07:00
});
replace ( list , ...( els . length > 0 ? els : [ h ( "div" , { class : "empty" }, "Nothing waiting on you." )]));
renderSubmit ();
};
const off = onEach ([ "inbox" , "selection" , "tasks" ], render );
2026-10-14 19:35:34 -07:00
const changes = h ( "section" , { class : "inbox-changes" });
replace ( root , h ( "div" , { class : "inbox-view" }, changes , tabs , submitBar , list ));
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 10:37:04 -07:00
function groupByTask ( items ) {
const groups = new Map ();
for ( const item of [... items ]. sort (( a , b ) => a . created_at . localeCompare ( b . created_at ))) {
let g = groups . get ( item . task_id );
if ( ! g ) {
g = { taskId : item . task_id , projectId : item . project_id , blocked : false , items : [] };
groups . set ( item . task_id , g );
}
g . items . push ( item );
g . blocked ||= item . priority === "blocked" ;
}
const projectName = ( id ) => state . projects . find (( p ) => p . id === id ) ? . name ?? id ;
return [... groups . values ()]. sort (( a , b ) => Number ( b . blocked ) - Number ( a . blocked ) ||
projectName ( a . projectId ). localeCompare ( projectName ( b . projectId )) ||
a . items [ 0 ]. created_at . localeCompare ( b . items [ 0 ]. created_at ));
}
2026-10-14 09:40:12 -07:00
function card ( item , changed ) {
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 );
const allowed = item . kind === "question" ? [ "pick" , "snooze" ] : [ "dismiss" , "reply" , "snooze" ];
2026-10-14 09:40:12 -07:00
const labels = {
2026-10-14 10:37:04 -07:00
pick : "Pick" ,
reply : "Reply" ,
2026-10-14 09:40:12 -07:00
snooze : "Snooze" ,
dismiss : "Dismiss" ,
};
const renderActions = () => {
2026-10-14 10:37:04 -07:00
if ( item . kind === "report" ) {
const ok = h ( "button" , { type : "button" , class : "primary" }, "OK" );
ok . onclick = async () => {
ok . disabled = true ;
try {
await post ( "/api/inbox/responses" , { body : { responses : [{ item_id : item . id , type : "dismiss" }] } });
}
catch ( err ) {
2026-10-14 22:42:46 -07:00
void showAlert ( errorText ( err ));
2026-10-14 10:37:04 -07:00
ok . disabled = false ;
}
};
replace ( actions , h ( "div" , { class : "ack" }, ok ));
return ;
}
2026-10-14 09:40:12 -07:00
const mode = modes . get ( item . id ) ?? allowed [ 0 ];
const tabs = h ( "div" , { class : "modes" }, ... allowed . map (( m ) => {
const b = h ( "button" , { type : "button" , class : m === mode ? "active" : "" }, labels [ m ]);
b . onclick = () => {
modes . set ( item . id , m );
drafts . delete ( item . id );
if ( m === "dismiss" ) {
drafts . set ( item . id , { item_id : item . id , type : "dismiss" });
}
renderActions ();
changed ();
};
return b ;
}));
replace ( actions , tabs , modeInput ( item , mode , changed ));
};
switch ( item . kind ) {
case "question" :
2026-10-14 10:37:04 -07:00
replace ( body , p [ "context" ] ? markdown ( String ( p [ "context" ]), "md context" ) : null , markdown ( String ( p [ "question" ]), "md question" ), h ( "p" , { class : "why" }, h ( "strong" , {}, "Blocked on: " ), inlineMarkdown ( String ( p [ "why_blocked" ]))), attachments ( p [ "attachments" ] ?? []));
2026-10-14 09:40:12 -07:00
break ;
case "report" :
2026-10-14 19:35:34 -07:00
replace ( body , markdown ( String ( p [ "text" ] ?? "" ), "md summary" ));
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 modeInput ( item , mode , changed ) {
const draft = drafts . get ( item . id );
const set = ( d ) => {
if ( d ) {
drafts . set ( item . id , d );
}
else {
drafts . delete ( item . id );
}
changed ();
};
switch ( mode ) {
2026-10-14 10:37:04 -07:00
case "pick" : {
2026-10-14 09:40:12 -07:00
const p = item . payload ;
const options = p [ "options" ] ?? [];
2026-10-14 10:37:04 -07:00
const text = h ( "textarea" , {
rows : 3 ,
placeholder : "Your answer, a question back, or new instructions" ,
value : draft ? . type === "other" ? ( draft . text ?? "" ) : "" ,
hidden : draft ? . type !== "other" ,
});
const setOther = () => set ( text . value . trim () ? { item_id : item . id , type : "other" , text : text . value } : undefined );
text . oninput = setOther ;
const other = h ( "input" , { type : "radio" , name : `opt- ${ item . id } ` , checked : draft ? . type === "other" });
other . onchange = () => {
text . hidden = false ;
text . focus ();
setOther ();
2026-10-14 09:40:12 -07:00
};
return h ( "div" , { class : "options" }, ... options . map (( option ) => {
2026-10-14 10:37:04 -07:00
const radio = h ( "input" , { type : "radio" , name : `opt- ${ item . id } ` , checked : draft ? . type === "option" && draft . option === option });
radio . onchange = () => {
text . hidden = true ;
set ({ item_id : item . id , type : "option" , option });
};
return h ( "label" , { class : "option" }, radio , " " , inlineMarkdown ( option ), option === p [ "recommended_option" ] && h ( "span" , { class : "recommended" }, "recommended" ));
}), h ( "label" , { class : "option" }, other , " Other" ), text );
2026-10-14 09:40:12 -07:00
}
2026-10-14 10:37:04 -07:00
case "reply" : {
const text = h ( "textarea" , { rows : 3 , placeholder : "Reply to the agent" , value : draft ? . type === "other" ? ( draft . text ?? "" ) : "" });
text . oninput = () => set ( text . value . trim () ? { item_id : item . id , type : "other" , text : text . value } : undefined );
2026-10-14 09:40:12 -07:00
return text ;
}
case "snooze" : {
const presets = [
[ "1 hour" , () => new Date ( Date . now () + 3600_000 )],
[ "4 hours" , () => new Date ( Date . now () + 4 * 3600_000 )],
[
"Tomorrow 9am" ,
() => {
const d = new Date ();
d . setDate ( d . getDate () + 1 );
d . setHours ( 9 , 0 , 0 , 0 );
return d ;
},
],
];
return h ( "div" , { class : "snooze" }, ... presets . map (([ label , when ]) => {
const radio = h ( "input" , { type : "radio" , name : `snooze- ${ item . id } ` });
radio . onchange = () => set ({ item_id : item . id , type : "snooze" , snooze_until : when (). toISOString () });
return h ( "label" , {}, radio , " " , label );
}));
}
case "dismiss" :
return h ( "p" , { class : "hint" }, "Will be dismissed when you submit." );
}
}
function thread ( item ) {
if ( item . responses . length === 0 ) {
return null ;
}
return h ( "ul" , { class : "thread" }, ... item . responses . map (( r ) => h ( "li" , {}, h ( "strong" , {}, r . user ), ` ${ r . type . replace ( "_" , " " ) } ` , r . option ? `: ${ r . option } ` : "" , r . text ? ` — ${ r . text } ` : "" )));
}
function attachments ( list ) {
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" :
return h ( "li" , {}, a . image ? . startsWith ( "data:image/" ) ? h ( "img" , { src : a . image , alt : a . caption ?? "" }) : null , caption );
case "diagram" :
return h ( "li" , {}, h ( "pre" , { class : "mermaid-source" }, a . mermaid ?? "" ), caption );
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