2026-10-15 16:33:17 -07:00
import { get , post , type AgentSession , type ByreEvent , type Participant , type Schemas , type Snapshot } from "./api.js" ;
2026-10-15 16:52:20 -07:00
import { ago , copyable , errorText , h , replace , showAlert , showConfirm } from "./dom.js" ;
2026-10-14 19:35:34 -07:00
import { markdown } from "./markdown.js" ;
2026-10-15 13:15:49 -07:00
import { previewURL } from "./preview.js" ;
2026-10-16 10:44:27 -07:00
import { findingCount , reviewCounts , severityCounts , unignoredFindings } from "./reviews.js" ;
2026-10-16 08:42:48 -07:00
import { composerText , endSend , flushDrafts , forgetLostImages , lostImageCount , setComposerImages , setComposerText , startSend } from "./drafts.js" ;
2026-10-16 11:08:57 -07:00
import { currentSession , findTask , focusTab , focusTask , markSummaryRead , on , onEach , pickReview , pinSnapshot , setFocus , showReview , state , transcriptLoading , type Topic } from "./state.js" ;
2026-10-15 13:30:09 -07:00
import { taskStyle } from "./taskcolor.js" ;
2026-10-15 20:20:02 -07:00
import { fillTodoLinks } from "./todolink.js" ;
2026-10-15 09:40:30 -07:00
export type Usage = { used : number ; window : number ; model : string };
2026-10-15 16:33:17 -07:00
type Review = Schemas [ "Review" ];
2026-10-15 09:40:30 -07:00
export type ChatSource = {
key : () => string | undefined ;
events : () => ByreEvent [];
2026-10-16 10:45:38 -07:00
loading : () => boolean ;
2026-10-15 09:40:30 -07:00
session : () => AgentSession | undefined ;
root : () => string ;
taskId : () => string | undefined ;
resetTopics : Topic [];
refreshTopics : Topic [];
transcriptTopic : Topic ;
streamTopic : Topic ;
2026-10-16 10:45:38 -07:00
readyTopic : Topic ;
2026-10-15 09:40:30 -07:00
empty : string ;
canSend : ( sessionState : string ) => boolean ;
2026-10-15 21:56:59 -07:00
placeholder : string ;
2026-10-15 09:40:30 -07:00
send : ( m : { key : string ; sessionId : string | undefined ; text : string ; images : string [] | undefined ; model : string | undefined ; firstModel : string }) => Promise < void >;
};
export type Head = {
render : ( usage : ( sessionId : string ) => Usage | undefined ) => ( Node | false | null )[];
onEvents ?: ( events : ByreEvent []) => void ;
reset ?: () => void ;
dispose ?: () => void ;
};
2026-10-14 09:40:12 -07:00
2026-10-15 14:55:58 -07:00
export const models = [
"claude-opus-5-5" ,
"claude-sonnet-5-5" ,
"claude-haiku-5-5" ,
"claude-fable-5-1" ,
"gpt-6.1-sol" ,
"gpt-6-astra" ,
"gpt-6-sol" ,
"gpt-6-luna" ,
"gpt-5.6-sol" ,
"gpt-5.6-terra" ,
"gpt-5.6-luna" ,
];
2026-10-14 09:40:12 -07:00
2026-10-15 09:59:13 -07:00
const quietReply = "NO_REPLY" ;
2026-10-14 16:03:46 -07:00
type Call = { name : string ; target : string };
type Run = {
first : HTMLElement ;
group : { el : HTMLDetailsElement ; summary : HTMLElement ; body : HTMLElement } | undefined ;
calls : Call [];
2026-10-15 11:08:53 -07:00
parent : string ;
};
type Subagent = {
type : string ;
description : string ;
status : HTMLElement ;
2026-10-15 00:08:32 -07:00
};
2026-10-14 16:03:46 -07:00
type Ctx = {
tools : Map < string , HTMLElement >;
2026-10-15 11:08:53 -07:00
subagents : Map < string , Subagent >;
2026-10-15 16:33:17 -07:00
reviewers : Set < string >;
2026-10-15 18:34:12 -07:00
reviewButtons : Map < number , HTMLElement >;
2026-10-14 16:03:46 -07:00
run : Run | undefined ;
2026-10-15 12:05:29 -07:00
root : () => string ;
2026-10-15 09:40:30 -07:00
taskId : string | undefined ;
2026-10-15 13:56:37 -07:00
held : HTMLElement | undefined ;
2026-10-15 20:41:42 -07:00
open : boolean ;
2026-10-15 20:20:51 -07:00
drawn : HTMLElement [] | undefined ;
2026-10-15 09:59:13 -07:00
quiet : boolean ;
2026-10-15 10:30:24 -07:00
handoff : { summary : HTMLElement ; body : HTMLElement } | undefined ;
2026-10-14 16:03:46 -07:00
};
2026-10-15 09:40:30 -07:00
const taskSource : ChatSource = {
key : () => state . selectedTaskId ,
events : () => state . transcript ,
2026-10-16 10:45:38 -07:00
loading : transcriptLoading ,
2026-10-15 09:40:30 -07:00
session : () => ( state . selectedTaskId ? currentSession ( state . selectedTaskId ) : undefined ),
root : () => ( state . selectedTaskId ? ( findTask ( state . selectedTaskId ) ? . worktree_path ?? "" ) : "" ),
taskId : () => state . selectedTaskId ,
resetTopics : [ "selection" ],
refreshTopics : [ "tasks" , "sessions" ],
transcriptTopic : "transcript" ,
streamTopic : "stream" ,
2026-10-16 10:45:38 -07:00
readyTopic : "ready" ,
2026-10-15 09:40:30 -07:00
empty : "No task selected. Ask your lead, or pick one with ⌘K." ,
2026-10-15 18:33:18 -07:00
canSend : () => true ,
2026-10-15 21:56:59 -07:00
placeholder : "Message the agent. Paste or drop images to attach them." ,
2026-10-15 09:40:30 -07:00
send : async ({ key , sessionId , text , images , model , firstModel }) => {
if ( sessionId ) {
await post ( "/api/sessions/{sessionId}/messages" , { path : { sessionId }, body : { text , images , model } });
return ;
}
await post ( "/api/tasks/{taskId}/sessions" , { path : { taskId : key }, body : { prompt : text , model : firstModel , images } });
},
};
2026-10-14 09:40:12 -07:00
export function mountTranscript ( root : HTMLElement ) : () => void {
2026-10-15 09:40:30 -07:00
return mountChat ( root , taskSource , taskHead );
}
function taskHead ( rerender : () => void ) : Head {
let unmerged : { taskId : string ; has : boolean } | undefined ;
let timer : number | undefined ;
const load = async () => {
const taskId = state . selectedTaskId ;
if ( ! taskId ) {
return ;
}
try {
const u = await get ( "/api/tasks/{taskId}/unmerged" , { path : { taskId } });
if ( state . selectedTaskId !== taskId ) {
return ;
}
const has = u . commits > 0 || u . files > 0 ;
if ( unmerged ? . taskId === taskId && unmerged . has === has ) {
return ;
}
unmerged = { taskId , has };
rerender ();
} catch ( err ) {
console . error ( "load unmerged changes" , err );
}
};
return {
reset : () => {
clearTimeout ( timer );
void load ();
},
dispose : () => clearTimeout ( timer ),
onEvents : ( events ) => {
if ( events . some (( ev ) => ev . kind === "agent.tool_result" || ev . kind === "turn.ended" )) {
clearTimeout ( timer );
timer = setTimeout (() => void load (), 1500 );
}
},
render : ( usage ) => {
const task = state . selectedTaskId ? findTask ( state . selectedTaskId ) : undefined ;
if ( ! task ) {
return [];
}
const sess = currentSession ( task . id );
if ( task . status !== "active" ) {
return [ h ( "div" , { class : "head-actions" }, task . status )];
}
const target = state . projects . find (( p ) => p . id === task . project_id ) ? . default_branch ?? "the default branch" ;
2026-10-15 14:55:58 -07:00
const busy = sess ? . state === "running" ;
2026-10-15 12:11:35 -07:00
const held = task . merge_queued_at !== undefined ;
2026-10-16 10:36:38 -07:00
const intoParent = task . merging_into_parent ;
2026-10-15 09:40:30 -07:00
const merge = h (
"button" ,
2026-10-15 12:11:35 -07:00
{
2026-10-15 14:00:18 -07:00
class : "merge-button" ,
2026-10-16 10:36:38 -07:00
disabled : busy || held || intoParent || unmerged ? . taskId !== task . id || ! unmerged . has ,
title : intoParent
? "Waiting for the parent task's agent to merge this subtask"
: held
? "Waiting for the merge in progress in this project to finish"
: task . parent_task_id !== undefined
? `Merge this subtask into its parent task's branch, or into ${ target } (⌘S)`
: `The agent merges ${ target } in, then byre squashes into one commit on ${ target } and pushes it to origin; held while another task in the project is merging (⌘S)` ,
2026-10-15 12:11:35 -07:00
},
2026-10-16 10:36:38 -07:00
intoParent ? "Merging into parent" : held ? "Merge held" : "Merge" ,
2026-10-15 09:40:30 -07:00
);
2026-10-15 12:30:10 -07:00
merge . onclick = () => confirmMerge ( task . id );
2026-10-15 09:40:30 -07:00
const mergeIn = h ( "button" , { disabled : busy , title : `Ask the agent to bring ${ target } into this branch and resolve any conflicts` }, `Sync from ${ target } ` );
mergeIn . onclick = async () => {
mergeIn . disabled = true ;
try {
await post ( "/api/tasks/{taskId}/merge-in" , { path : { taskId : task.id } });
} catch ( err ) {
void showAlert ( errorText ( err ));
mergeIn . disabled = false ;
}
};
2026-10-16 10:08:08 -07:00
const discard = h ( "button" , { class : "danger" , disabled : busy , title : "The agent writes the task summary, then byre deletes the worktree and branch without merging" }, "Discard" );
2026-10-15 09:40:30 -07:00
discard . onclick = () =>
2026-10-15 14:00:18 -07:00
confirmThen ( `Discard " ${ task . title } "?` , "Discard" , true , discard , async () => {
2026-10-15 09:40:30 -07:00
await post ( "/api/tasks/{taskId}/discard" , { path : { taskId : task.id } });
await setFocus ({ task_id : null });
});
return [
h ( "div" , { class : "head-actions session-actions" }, compactButton ( sess ? . id , sess ? . state , sess && usage ( sess . id )), mergeIn ),
h ( "div" , { class : "head-actions" }, merge , discard ),
];
},
};
}
export function mountChat ( root : HTMLElement , src : ChatSource , makeHead : ( rerender : () => void ) => Head ) : () => void {
2026-10-14 09:40:12 -07:00
const head = h ( "header" , { class : "task-header" });
const transcript = h ( "div" , { class : "transcript" });
const composer = h ( "div" , { class : "composer" });
2026-10-14 16:03:46 -07:00
2026-10-15 13:49:10 -07:00
let stuck = true ;
const pin = () => {
if ( stuck ) {
transcript . scrollTop = transcript . scrollHeight ;
}
};
transcript . addEventListener ( "scroll" , () => {
stuck = transcript . scrollHeight - transcript . scrollTop - transcript . clientHeight < 80 ;
});
transcript . addEventListener ( "load" , pin , true );
const resizes = new ResizeObserver ( pin );
resizes . observe ( transcript );
const mutations = new MutationObserver ( pin );
mutations . observe ( transcript , { childList : true , subtree : true , characterData : true });
2026-10-16 11:08:57 -07:00
const shown = new Set < HTMLElement >();
const markShown = () => {
const taskId = ctx . taskId ;
if ( ! taskId || document . visibilityState !== "visible" ) {
return ;
}
for ( const el of shown ) {
markSummaryRead ( taskId , Number ( el . dataset [ "seq" ]));
}
};
const sightings = new IntersectionObserver (
( entries ) => {
for ( const e of entries ) {
if ( e . isIntersecting ) {
shown . add ( e . target as HTMLElement );
} else {
shown . delete ( e . target as HTMLElement );
}
}
markShown ();
},
{ root : transcript },
);
document . addEventListener ( "visibilitychange" , markShown );
const jump = () => {
const target = state . jump ;
if ( ! target || target . taskId !== ctx . taskId ) {
return ;
}
const end = transcript . querySelector < HTMLElement >( `.turn-end[data-seq=" ${ target . seq } "]` );
if ( ! end ) {
return ;
}
state . jump = undefined ;
const msg = end . previousElementSibling ;
const box = transcript . getBoundingClientRect ();
if ( msg ? . classList . contains ( "agent" )) {
transcript . scrollTop += msg . getBoundingClientRect (). top - box . top - 8 ;
} else {
transcript . scrollTop += end . getBoundingClientRect (). bottom - box . bottom + 8 ;
}
stuck = transcript . scrollHeight - transcript . scrollTop - transcript . clientHeight < 80 ;
};
2026-10-15 09:40:30 -07:00
const newCtx = () : Ctx => ({
tools : new Map (),
2026-10-15 11:08:53 -07:00
subagents : new Map (),
2026-10-15 16:33:17 -07:00
reviewers : new Set (),
2026-10-15 18:34:12 -07:00
reviewButtons : new Map (),
2026-10-15 09:40:30 -07:00
run : undefined ,
2026-10-15 13:56:37 -07:00
held : undefined ,
2026-10-15 20:41:42 -07:00
open : false ,
2026-10-15 20:20:51 -07:00
drawn : undefined ,
2026-10-15 09:59:13 -07:00
quiet : false ,
2026-10-15 10:30:24 -07:00
handoff : undefined ,
2026-10-15 12:05:29 -07:00
root : src.root ,
2026-10-15 09:40:30 -07:00
taskId : src.taskId (),
});
2026-10-14 16:03:46 -07:00
let ctx = newCtx ();
2026-10-15 09:40:30 -07:00
let context = new Map < string , Usage >();
const headImpl = makeHead (() => renderHead ());
2026-10-14 16:03:46 -07:00
2026-10-16 07:13:13 -07:00
const scanner = h ( "div" , { class : "scanner" , hidden : true });
2026-10-16 10:45:38 -07:00
const loading = h ( "div" , { class : "empty" }, "loading…" );
2026-10-14 16:03:46 -07:00
const setRunning = ( running : boolean ) => {
transcript . classList . toggle ( "running" , running );
2026-10-16 07:13:13 -07:00
scanner . hidden = ! running ;
2026-10-14 16:03:46 -07:00
};
2026-10-14 23:53:17 -07:00
const putBefore = ( el : HTMLElement , anchor : HTMLElement | undefined ) => {
if ( anchor ? . parentNode === transcript ) {
transcript . insertBefore ( el , anchor );
2026-10-14 16:03:46 -07:00
} else {
2026-10-16 08:34:06 -07:00
transcript . insertBefore ( el , scanner );
2026-10-14 16:03:46 -07:00
}
};
2026-10-15 20:41:42 -07:00
const syncRunning = () => setRunning ( src . session () ? . state === "running" && ctx . open );
2026-10-15 13:56:37 -07:00
const release = () => {
2026-10-16 07:13:13 -07:00
if ( ! ctx . held ) {
2026-10-15 13:56:37 -07:00
return ;
}
ctx . held = undefined ;
2026-10-15 20:41:42 -07:00
ctx . open = true ;
2026-10-15 13:56:37 -07:00
syncRunning ();
};
const put = ( el : HTMLElement , keepHeld = false ) => {
2026-10-15 10:30:24 -07:00
if ( ctx . handoff ) {
ctx . handoff . body . append ( el );
return ;
}
2026-10-15 13:56:37 -07:00
if ( ! keepHeld ) {
release ();
}
2026-10-15 20:20:51 -07:00
ctx . drawn ? . push ( el );
2026-10-15 10:30:24 -07:00
putBefore ( el , pending [ 0 ] ? . el );
};
2026-10-15 20:20:51 -07:00
const fold = () => {
for ( const el of ctx . drawn ?? []) {
el . remove ();
}
ctx . drawn = undefined ;
ctx . held = undefined ;
2026-10-15 20:41:42 -07:00
ctx . open = false ;
2026-10-15 20:20:51 -07:00
ctx . quiet = false ;
syncRunning ();
};
2026-10-14 09:40:12 -07:00
const renderHead = () => {
2026-10-15 13:56:37 -07:00
syncRunning ();
2026-10-15 09:40:30 -07:00
if ( ! src . key ()) {
2026-10-14 09:40:12 -07:00
replace ( head );
return ;
}
2026-10-15 09:40:30 -07:00
replace ( head , ... headImpl . render (( id ) => context . get ( id )));
2026-10-14 09:40:12 -07:00
};
2026-10-14 19:35:34 -07:00
let composerModel = "" ;
2026-10-14 22:42:46 -07:00
let focusStart = false ;
2026-10-14 09:40:12 -07:00
const renderComposer = () => {
2026-10-15 09:40:30 -07:00
const key = src . key ();
2026-10-16 08:42:48 -07:00
const old = composer . querySelector ( "textarea" );
if ( old ) {
releaseInput ( old );
}
2026-10-15 09:40:30 -07:00
if ( ! key ) {
2026-10-14 09:40:12 -07:00
replace ( composer );
return ;
}
2026-10-15 09:40:30 -07:00
const sess = src . session ();
2026-10-14 22:50:13 -07:00
const live = sess && sess . state !== "completed" ? sess : undefined ;
composerModel = live ? live . model || context . get ( live . id ) ? . model || "" : "" ;
2026-10-15 14:55:58 -07:00
const form = composeForm ( src , key , live ? . id , live ? . state ?? "idle" , composerModel , live ? . vendor , showPending );
2026-10-14 23:34:39 -07:00
const caret = old !== null && document . activeElement === old ? [ old . selectionStart , old . selectionEnd ] : undefined ;
2026-10-14 22:50:13 -07:00
replace ( composer , form );
2026-10-14 23:34:39 -07:00
const text = form . querySelector ( "textarea" ) ! ;
if ( caret ) {
text . focus ();
text . setSelectionRange ( caret [ 0 ] ! , caret [ 1 ] ! );
} else if ( focusStart && ! sess ) {
text . focus ();
2026-10-14 09:40:12 -07:00
}
2026-10-14 22:42:46 -07:00
focusStart = false ;
2026-10-14 22:50:13 -07:00
};
let pending : { el : HTMLElement ; text : string ; imgs : PendingImage [] }[] = [];
const showPending = ( text : string , imgs : PendingImage []) => {
const el = h (
"div" ,
{ class : "msg user pending" },
2026-10-15 18:33:18 -07:00
h ( "div" , { class : "who" }, src . session () ? . state === "running" ? "queued…" : "sending…" ),
2026-10-14 22:50:13 -07:00
imgs . length > 0 ? h ( "div" , { class : "msg-images" }, ... imgs . map (( img ) => h ( "img" , { src : img.url , alt : "attached image" }))) : null ,
text . trim () ? markdown ( text , "text md" ) : null ,
);
const entry = { el , text , imgs };
pending . push ( entry );
ctx . run = undefined ;
2026-10-16 07:13:13 -07:00
putBefore ( el , undefined );
2026-10-15 13:49:10 -07:00
stuck = true ;
pin ();
2026-10-14 22:50:13 -07:00
return () => {
el . remove ();
pending = pending . filter (( p ) => p !== entry );
};
};
const confirmPending = ( text : string ) => {
const entry = pending . find (( p ) => p . text === text );
if ( ! entry ) {
return ;
}
entry . el . remove ();
releaseImages ( entry . imgs );
pending = pending . filter (( p ) => p !== entry );
2026-10-14 16:03:46 -07:00
};
2026-10-15 11:36:51 -07:00
const runHead = ( run : Run ) => [ subagentTag ( run . parent , ctx ), runSummary ( run . calls )];
2026-10-15 11:08:53 -07:00
const addToRun = ( el : HTMLElement , call : Call | undefined , parent : string ) => {
2026-10-14 16:03:46 -07:00
const run = ctx . run ;
2026-10-15 11:08:53 -07:00
if ( ! run || run . parent !== parent ) {
2026-10-15 11:36:51 -07:00
ctx . run = { first : el , group : undefined , calls : call ? [ call ] : [], parent };
2026-10-14 16:03:46 -07:00
put ( el );
return ;
}
if ( call ) {
run . calls . push ( call );
}
if ( ! run . group ) {
const summary = h ( "summary" );
const body = h ( "div" , { class : "tool-group-body" });
const group = h ( "details" , { class : "tool-group" }, summary , body );
run . first . replaceWith ( group );
2026-10-15 20:20:51 -07:00
ctx . drawn ? . push ( group );
2026-10-14 16:03:46 -07:00
body . append ( run . first );
run . group = { el : group , summary , body };
}
run . group . body . append ( el );
2026-10-15 11:08:53 -07:00
replace ( run . group . summary , ... runHead ( run ));
2026-10-15 00:08:32 -07:00
};
2026-10-14 22:28:25 -07:00
let stream : { el : HTMLElement ; text : string ; timer : number } | undefined ;
2026-10-14 19:35:34 -07:00
const dropStream = () => {
if ( ! stream ) {
return ;
}
2026-10-14 22:28:25 -07:00
clearTimeout ( stream . timer );
2026-10-14 19:35:34 -07:00
stream . el . remove ();
stream = undefined ;
};
const appendDelta = ( ev : ByreEvent ) => {
2026-10-15 16:33:17 -07:00
if ( ctx . handoff || ( ev . agent_session_id && ctx . reviewers . has ( ev . agent_session_id ))) {
2026-10-15 10:30:24 -07:00
return ;
}
2026-10-14 19:35:34 -07:00
if ( ! stream ) {
2026-10-15 20:41:42 -07:00
stream = { el : h ( "div" , { class : "msg agent" , hidden : true }), text : "" , timer : 0 };
2026-10-14 19:35:34 -07:00
ctx . run = undefined ;
2026-10-15 13:56:37 -07:00
put ( stream . el , true );
2026-10-14 19:35:34 -07:00
}
stream . text += String ( ev . payload [ "text" ] ?? "" );
2026-10-14 22:28:25 -07:00
if ( stream . timer ) {
2026-10-14 19:35:34 -07:00
return ;
}
2026-10-14 22:28:25 -07:00
stream . timer = setTimeout (() => {
2026-10-14 19:35:34 -07:00
if ( ! stream ) {
return ;
}
2026-10-14 22:28:25 -07:00
stream . timer = 0 ;
2026-10-15 13:56:37 -07:00
stream . el . hidden = ctx . held !== undefined || quietReply . startsWith ( stream . text . trim ());
2026-10-15 09:40:30 -07:00
replace ( stream . el , markdown ( stream . text , "text md" , { taskId : ctx.taskId }));
2026-10-15 20:20:02 -07:00
fillTodoLinks ( stream . el , models [ 0 ] ! );
2026-10-15 13:49:10 -07:00
pin ();
2026-10-14 22:28:25 -07:00
}, 100 );
2026-10-14 19:35:34 -07:00
};
2026-10-15 10:30:24 -07:00
const openHandoff = () => {
const summary = h ( "summary" , {}, "writing a handoff…" );
2026-10-14 19:35:34 -07:00
const body = h ( "div" , { class : "folded-turn" });
2026-10-15 10:30:24 -07:00
put ( h ( "details" , { class : "note" }, summary , body ));
ctx . handoff = { summary , body };
};
const closeHandoff = ( end : HTMLElement | undefined ) : HTMLElement | undefined => {
const handoff = ctx . handoff ;
if ( ! handoff ) {
return undefined ;
}
ctx . handoff = undefined ;
if ( end ) {
replace ( handoff . summary , ... end . childNodes );
} else {
replace ( handoff . summary , "handoff not written" );
2026-10-14 19:35:34 -07:00
}
2026-10-15 10:30:24 -07:00
return handoff . summary ;
2026-10-14 19:35:34 -07:00
};
2026-10-14 09:40:12 -07:00
const appendEvents = ( events : ByreEvent []) => {
2026-10-14 16:03:46 -07:00
let contextChanged = false ;
2026-10-15 09:40:30 -07:00
headImpl . onEvents ? .( events );
2026-10-14 09:40:12 -07:00
for ( const ev of events ) {
2026-10-14 16:03:46 -07:00
const p = ev . payload as Record < string , unknown >;
2026-10-15 11:08:53 -07:00
const parent = parentOf ( p );
2026-10-15 16:33:17 -07:00
if ( ev . kind === "session.created" && p [ "role" ] === "reviewer" && ev . agent_session_id ) {
ctx . reviewers . add ( ev . agent_session_id );
continue ;
}
if ( ev . agent_session_id && ctx . reviewers . has ( ev . agent_session_id )) {
continue ;
}
2026-10-15 11:08:53 -07:00
if ( ev . kind === "agent.vendor" ) {
claudeTaskStatus ( p , ctx );
continue ;
}
if (( ev . kind === "agent.message" && ! parent ) || ev . kind === "turn.ended" || ev . kind === "user.message" ) {
2026-10-14 19:35:34 -07:00
dropStream ();
}
2026-10-14 22:50:13 -07:00
if ( ev . kind === "user.message" && ev . actor !== "byre" ) {
confirmPending ( String ( p [ "text" ] ?? "" ));
}
2026-10-14 16:03:46 -07:00
if ( ev . kind === "session.context" && ev . agent_session_id ) {
const prev = context . get ( ev . agent_session_id );
const window = Number ( p [ "window" ]) || prev ? . window || 0 ;
context . set ( ev . agent_session_id , { used : Number ( p [ "used" ]), window , model : String ( p [ "model" ] ?? "" ) });
contextChanged = true ;
continue ;
}
2026-10-15 00:08:32 -07:00
if ( ev . kind === "snapshot.created" ) {
2026-10-15 11:36:51 -07:00
ctx . run = undefined ;
2026-10-15 18:34:12 -07:00
put ( snapRow ( p as unknown as Snapshot , ctx ));
2026-10-15 00:08:32 -07:00
continue ;
}
2026-10-16 10:44:27 -07:00
if ( ev . kind === "review.started" || ev . kind === "review.finished" || ev . kind === "review.finding" ) {
2026-10-15 18:34:12 -07:00
const r = p as unknown as Review ;
const button = ctx . reviewButtons . get ( r . snapshot_n );
if ( button ) {
2026-10-15 20:07:36 -07:00
markReview ( button , r );
2026-10-15 18:34:12 -07:00
}
}
2026-10-15 11:08:53 -07:00
if ( ev . kind === "agent.tool_call" && isSubagentCall ( p )) {
ctx . run = undefined ;
2026-10-15 11:36:51 -07:00
put ( renderSubagent ( p , ctx ));
2026-10-15 11:08:53 -07:00
continue ;
}
2026-10-14 16:03:46 -07:00
if ( ev . kind === "agent.tool_call" ) {
2026-10-15 20:29:44 -07:00
addToRun ( renderToolCall ( p , ctx ), { name : toolLabel ( String ( p [ "name" ])), target : toolTarget ( p [ "input" ], ctx . root ()) }, parent );
2026-10-14 16:03:46 -07:00
continue ;
2026-10-14 09:40:12 -07:00
}
2026-10-14 16:03:46 -07:00
const el = renderEvent ( ev , ctx );
if ( ! el ) {
continue ;
}
if ( ev . kind === "agent.thinking" ) {
2026-10-15 11:08:53 -07:00
addToRun ( el , undefined , parent );
2026-10-14 16:03:46 -07:00
continue ;
}
ctx . run = undefined ;
2026-10-15 20:41:42 -07:00
if ( ev . kind === "turn.ended" ) {
ctx . open = false ;
}
2026-10-15 10:30:24 -07:00
if ( ev . kind === "turn.ended" && p [ "handoff" ] === true && ctx . handoff ) {
2026-10-15 11:36:51 -07:00
closeHandoff ( el );
2026-10-14 19:35:34 -07:00
continue ;
}
2026-10-15 10:30:24 -07:00
if ( ev . kind === "turn.ended" || ( ev . kind === "user.message" && p [ "mid_turn" ] !== true )) {
closeHandoff ( undefined );
}
if ( ev . kind === "user.message" && p [ "handoff" ] === true ) {
openHandoff ();
}
2026-10-15 20:20:51 -07:00
if ( ev . kind === "turn.ended" && ctx . quiet && ctx . drawn ) {
fold ();
2026-10-15 09:59:13 -07:00
continue ;
}
2026-10-15 20:20:51 -07:00
if ( ev . kind === "turn.ended" || ev . kind === "user.message" ) {
ctx . drawn = undefined ;
}
2026-10-15 13:56:37 -07:00
if ( ev . kind === "user.message" && ev . actor === "byre" && ev . owner && p [ "mid_turn" ] !== true && p [ "handoff" ] !== true ) {
ctx . held = el ;
2026-10-15 20:41:42 -07:00
ctx . open = false ;
2026-10-15 20:20:51 -07:00
ctx . drawn = [];
2026-10-15 13:56:37 -07:00
continue ;
2026-10-14 19:35:34 -07:00
}
2026-10-15 20:41:42 -07:00
if ( ev . kind === "user.message" && p [ "mid_turn" ] !== true ) {
ctx . open = true ;
}
2026-10-16 11:08:57 -07:00
if ( ev . kind === "turn.ended" && p [ "summary" ] === true && ctx . taskId ) {
el . dataset [ "seq" ] = String ( ev . seq );
sightings . observe ( el );
}
2026-10-15 13:56:37 -07:00
put ( el );
2026-10-14 16:03:46 -07:00
}
2026-10-15 20:41:42 -07:00
syncRunning ();
2026-10-14 16:03:46 -07:00
if ( contextChanged ) {
renderHead ();
2026-10-15 09:40:30 -07:00
const sess = src . session ();
2026-10-14 19:35:34 -07:00
if ( sess && ! sess . model && ( context . get ( sess . id ) ? . model ?? "" ) !== composerModel ) {
renderComposer ();
}
2026-10-14 09:40:12 -07:00
}
2026-10-15 20:20:02 -07:00
fillTodoLinks ( transcript , models [ 0 ] ! );
2026-10-15 13:49:10 -07:00
pin ();
2026-10-16 11:08:57 -07:00
jump ();
2026-10-14 09:40:12 -07:00
};
const renderAll = () => {
2026-10-15 13:49:10 -07:00
stuck = true ;
2026-10-14 19:35:34 -07:00
dropStream ();
2026-10-14 22:50:13 -07:00
pending = [];
2026-10-16 11:08:57 -07:00
sightings . disconnect ();
shown . clear ();
2026-10-14 16:03:46 -07:00
ctx = newCtx ();
context = new Map ();
2026-10-14 22:42:46 -07:00
focusStart = true ;
2026-10-15 09:40:30 -07:00
headImpl . reset ? .();
2026-10-14 09:40:12 -07:00
renderHead ();
2026-10-15 09:40:30 -07:00
if ( ! src . key ()) {
2026-10-14 19:35:34 -07:00
renderComposer ();
2026-10-16 08:34:06 -07:00
replace ( transcript , h ( "div" , { class : "empty" }, src . empty ), scanner );
2026-10-14 09:40:12 -07:00
return ;
}
2026-10-16 08:34:06 -07:00
replace ( transcript , scanner );
2026-10-16 10:45:38 -07:00
if ( src . loading ()) {
transcript . prepend ( loading );
}
2026-10-15 09:40:30 -07:00
appendEvents ( src . events ());
2026-10-14 19:35:34 -07:00
renderComposer ();
2026-10-14 09:40:12 -07:00
};
const offs = [
2026-10-15 09:40:30 -07:00
onEach ( src . resetTopics , renderAll ),
onEach ( src . refreshTopics , () => {
2026-10-14 09:40:12 -07:00
renderHead ();
renderComposer ();
}),
2026-10-15 09:40:30 -07:00
onEach ([ src . transcriptTopic ], ( fresh ) => appendEvents ( fresh as ByreEvent [])),
onEach ([ src . streamTopic ], ( ev ) => appendDelta ( ev as ByreEvent )),
2026-10-16 10:45:38 -07:00
onEach ([ src . readyTopic ], () => loading . remove ()),
2026-10-15 20:20:02 -07:00
onEach ([ "tasks" , "sessions" ], () => fillTodoLinks ( transcript , models [ 0 ] ! )),
2026-10-16 11:08:57 -07:00
on ( "jump" , jump ),
2026-10-14 09:40:12 -07:00
];
2026-10-16 08:34:06 -07:00
replace ( root , h ( "div" , { class : "transcript-view" }, head , transcript , composer ));
2026-10-14 09:40:12 -07:00
renderAll ();
return () => {
for ( const off of offs ) {
off ();
}
2026-10-15 09:40:30 -07:00
headImpl . dispose ? .();
2026-10-15 13:49:10 -07:00
resizes . disconnect ();
mutations . disconnect ();
2026-10-16 11:08:57 -07:00
sightings . disconnect ();
document . removeEventListener ( "visibilitychange" , markShown );
2026-10-14 19:35:34 -07:00
dropStream ();
2026-10-16 08:42:48 -07:00
const input = composer . querySelector ( "textarea" );
if ( input ) {
releaseInput ( input );
}
flushDrafts ();
2026-10-14 09:40:12 -07:00
};
}
2026-10-16 08:42:48 -07:00
const composerInputs = new Map < string , HTMLTextAreaElement >();
2026-10-14 16:03:46 -07:00
2026-10-16 08:42:48 -07:00
function releaseInput ( input : HTMLTextAreaElement ) : void {
for ( const [ key , el ] of composerInputs ) {
if ( el === input ) {
composerInputs . delete ( key );
2026-10-14 16:03:46 -07:00
}
2026-10-16 08:42:48 -07:00
}
}
function keepDraft ( input : HTMLTextAreaElement , key : string ) : void {
composerInputs . set ( key , input );
input . value = composerText ( key );
input . addEventListener ( "input" , () => setComposerText ( key , input . value ));
input . addEventListener ( "blur" , flushDrafts );
}
function lostImagesNote ( key : string ) : HTMLElement | null {
const n = lostImageCount ( key );
if ( n === 0 ) {
return null ;
}
const dismiss = h ( "button" , { type : "button" , class : "remove" , title : "Dismiss" }, "× " );
const note = h ( "div" , { class : "compose-note" }, ` ${ n === 1 ? "1 image" : ` ${ n } images` } from before the reload need re-attaching.` , dismiss );
dismiss . onclick = () => {
forgetLostImages ( key );
note . remove ();
};
return note ;
2026-10-14 16:03:46 -07:00
}
2026-10-14 20:07:26 -07:00
type PendingImage = { file : Blob ; url : string };
const imageDrafts = new Map < string , PendingImage [] >();
const imageTypes = [ "image/png" , "image/jpeg" , "image/gif" , "image/webp" ];
const maxImageBytes = 5 << 20 ;
function imageTray ( form : HTMLFormElement , input : HTMLTextAreaElement , key : string ) : { el : HTMLElement ; take : () => PendingImage []; restore : ( imgs : PendingImage []) => void } {
const el = h ( "div" , { class : "compose-images" });
const render = () => {
const imgs = imageDrafts . get ( key ) ?? [];
2026-10-16 08:42:48 -07:00
setComposerImages ( key , imgs . length );
2026-10-14 20:07:26 -07:00
el . hidden = imgs . length === 0 ;
replace (
el ,
... imgs . map (( img ) => {
const remove = h ( "button" , { type : "button" , class : "remove" , title : "Remove image" }, "× " );
remove . onclick = () => {
URL . revokeObjectURL ( img . url );
const rest = ( imageDrafts . get ( key ) ?? []). filter (( i ) => i !== img );
if ( rest . length > 0 ) {
imageDrafts . set ( key , rest );
} else {
imageDrafts . delete ( key );
}
render ();
};
return h ( "div" , { class : "thumb" }, h ( "img" , { src : img.url , alt : "attached image" }), remove );
}),
);
};
const add = ( files : Iterable < File >) => {
const added : PendingImage [] = [];
for ( const f of files ) {
if ( ! imageTypes . includes ( f . type )) {
2026-10-14 22:42:46 -07:00
void showAlert ( ` ${ f . name || "That file" } isn't a PNG, JPEG, GIF or WebP image.` );
2026-10-14 20:07:26 -07:00
continue ;
}
if ( f . size > maxImageBytes ) {
2026-10-14 22:42:46 -07:00
void showAlert ( ` ${ f . name || "That image" } is over ${ maxImageBytes >> 20 } MiB.` );
2026-10-14 20:07:26 -07:00
continue ;
}
added . push ({ file : f , url : URL.createObjectURL ( f ) });
}
if ( added . length === 0 ) {
return false ;
}
imageDrafts . set ( key , [...( imageDrafts . get ( key ) ?? []), ... added ]);
render ();
return true ;
};
input . addEventListener ( "paste" , ( e ) => {
const files = [...( e . clipboardData ? . files ?? [])]. filter (( f ) => f . type . startsWith ( "image/" ));
if ( files . length > 0 ) {
e . preventDefault ();
add ( files );
}
});
form . addEventListener ( "dragover" , ( e ) => {
if ( e . dataTransfer ? . types . includes ( "Files" )) {
e . preventDefault ();
form . classList . add ( "dropping" );
}
});
form . addEventListener ( "dragleave" , ( e ) => {
if ( ! form . contains ( e . relatedTarget as Node | null )) {
form . classList . remove ( "dropping" );
}
});
form . addEventListener ( "drop" , ( e ) => {
form . classList . remove ( "dropping" );
const files = [...( e . dataTransfer ? . files ?? [])];
if ( files . length > 0 ) {
e . preventDefault ();
add ( files );
}
});
render ();
return {
el ,
take : () => {
const imgs = imageDrafts . get ( key ) ?? [];
imageDrafts . delete ( key );
render ();
return imgs ;
},
restore : ( imgs ) => {
if ( imgs . length > 0 ) {
imageDrafts . set ( key , [... imgs , ...( imageDrafts . get ( key ) ?? [])]);
}
render ();
},
};
}
async function uploadImages ( imgs : PendingImage []) : Promise < string [] | undefined > {
if ( imgs . length === 0 ) {
return undefined ;
}
const ids : string [] = [];
for ( const img of imgs ) {
const blob = await post ( "/api/blobs" , { body : img.file });
ids . push ( blob . id );
}
return ids ;
}
function releaseImages ( imgs : PendingImage []) : void {
for ( const img of imgs ) {
URL . revokeObjectURL ( img . url );
}
}
2026-10-14 19:35:34 -07:00
const modelChoices = new Map < string , string >();
2026-10-15 14:55:58 -07:00
function vendorOf ( model : string ) : string {
return model . startsWith ( "gpt-" ) ? "codex" : "claude" ;
}
function modelSelect ( key : string , current : string , vendor : string | undefined ) : HTMLSelectElement {
const offered = vendor ? models . filter (( m ) => vendorOf ( m ) === vendor ) : models ;
const options = current && ! offered . includes ( current ) ? [ current , ... offered ] : offered ;
2026-10-14 19:35:34 -07:00
const select = h ( "select" , { title : "Model for the next turn" }, ... options . map (( m ) => h ( "option" , { value : m }, m )));
2026-10-15 14:55:58 -07:00
select . value = modelChoices . get ( key ) ?? ( current || options [ 0 ] ! );
2026-10-14 19:35:34 -07:00
select . onchange = () => {
modelChoices . set ( key , select . value );
};
return select ;
}
2026-10-14 22:50:13 -07:00
function composeForm (
2026-10-15 09:40:30 -07:00
src : ChatSource ,
key : string ,
2026-10-14 22:50:13 -07:00
sessionId : string | undefined ,
sessionState : string ,
currentModel : string ,
2026-10-15 14:55:58 -07:00
vendor : string | undefined ,
2026-10-14 22:50:13 -07:00
showPending : ( text : string , imgs : PendingImage []) => () => void ,
) : HTMLElement {
2026-10-15 21:56:59 -07:00
const text = h ( "textarea" , { placeholder : src.placeholder , rows : 3 });
2026-10-16 08:42:48 -07:00
const draftKey = `message: ${ key } ` ;
keepDraft ( text , draftKey );
2026-10-15 09:40:30 -07:00
const send = h ( "button" , { type : "submit" , disabled : ! src . canSend ( sessionState ) }, "Send" );
2026-10-15 14:55:58 -07:00
const interrupt = h ( "button" , { type : "button" , class : "danger" , disabled : sessionState !== "running" }, "Interrupt" );
2026-10-14 09:40:12 -07:00
interrupt . onclick = async () => {
2026-10-14 22:50:13 -07:00
if ( ! sessionId ) {
return ;
}
2026-10-14 09:40:12 -07:00
try {
await post ( "/api/sessions/{sessionId}/interrupt" , { path : { sessionId } });
} catch ( err ) {
2026-10-14 22:42:46 -07:00
void showAlert ( errorText ( err ));
2026-10-14 09:40:12 -07:00
}
};
2026-10-15 09:40:30 -07:00
const modelKey = `message: ${ sessionId ?? key } ` ;
2026-10-15 14:55:58 -07:00
const model = modelSelect ( modelKey , currentModel , vendor );
2026-10-14 20:07:26 -07:00
const form = h ( "form" , { class : "compose" });
2026-10-16 08:42:48 -07:00
const tray = imageTray ( form , text , draftKey );
form . append ( lostImagesNote ( draftKey ) ?? "" , tray . el , text , h ( "div" , { class : "compose-actions" }, model , interrupt , send ));
2026-10-14 09:40:12 -07:00
form . onsubmit = async ( e ) => {
e . preventDefault ();
2026-10-14 19:35:34 -07:00
const value = text . value ;
2026-10-16 08:42:48 -07:00
if ( ! value . trim () && ! imageDrafts . has ( draftKey )) {
2026-10-14 09:40:12 -07:00
return ;
}
2026-10-14 20:07:26 -07:00
const imgs = tray . take ();
2026-10-14 19:35:34 -07:00
text . value = "" ;
2026-10-16 08:42:48 -07:00
const sent = startSend ( draftKey , value , imgs . length );
2026-10-14 22:50:13 -07:00
const cancel = showPending ( value , imgs );
2026-10-14 09:40:12 -07:00
try {
2026-10-14 20:07:26 -07:00
const images = await uploadImages ( imgs );
2026-10-15 09:40:30 -07:00
await src . send ({ key , sessionId , text : value , images , model : model.value !== currentModel ? model.value : undefined , firstModel : model.value });
2026-10-14 22:50:13 -07:00
modelChoices . delete ( modelKey );
2026-10-16 08:42:48 -07:00
endSend ( sent , true );
form . querySelector ( ".compose-note" ) ? . remove ();
2026-10-14 09:40:12 -07:00
} catch ( err ) {
2026-10-14 22:50:13 -07:00
cancel ();
2026-10-16 08:42:48 -07:00
endSend ( sent , false );
const live = composerInputs . get ( draftKey ) ?? text ;
live . value = composerText ( draftKey );
2026-10-14 20:07:26 -07:00
tray . restore ( imgs );
2026-10-14 22:42:46 -07:00
void showAlert ( errorText ( err ));
2026-10-14 09:40:12 -07:00
}
};
2026-10-14 16:03:46 -07:00
submitOnEnter ( text , form );
2026-10-14 09:40:12 -07:00
return form ;
}
2026-10-14 16:03:46 -07:00
function submitOnEnter ( input : HTMLTextAreaElement , form : HTMLFormElement ) : void {
2026-10-14 09:40:12 -07:00
input . onkeydown = ( e ) => {
2026-10-14 16:03:46 -07:00
if ( e . key === "Enter" && ! e . shiftKey && ! e . isComposing ) {
2026-10-14 09:40:12 -07:00
e . preventDefault ();
form . requestSubmit ();
}
};
}
2026-10-15 09:40:30 -07:00
export function compactButton ( sessionId : string | undefined , sessionState : string | undefined , c : { used : number ; window : number ; model : string } | undefined ) : HTMLElement {
2026-10-14 23:38:17 -07:00
const pct = c && c . window > 0 ? Math . min ( 100 , ( c . used / c . window ) * 100 ) : 0 ;
const level = pct >= 90 ? "danger" : pct >= 70 ? "warn" : "ok" ;
const usage = c && c . window > 0 ? ` \ ncontext: ${ c . used . toLocaleString () } of ${ c . window . toLocaleString () } tokens ${ c . model ? ` ( ${ c . model } )` : "" } ` : "" ;
2026-10-14 19:35:34 -07:00
const b = h (
"button" ,
2026-10-14 23:38:17 -07:00
{
type : "button" ,
class : `compact ${ level } ` ,
style : `--fill: ${ pct } %` ,
disabled : ! sessionId || sessionState !== "idle" ,
title : `Ask the agent for a handoff, then continue in a new session with a fresh context ${ usage } ` ,
},
2026-10-14 19:35:34 -07:00
"Compact" ,
);
b . onclick = async () => {
2026-10-14 23:38:17 -07:00
if ( ! sessionId ) {
return ;
}
2026-10-14 19:35:34 -07:00
b . disabled = true ;
try {
await post ( "/api/sessions/{sessionId}/compact" , { path : { sessionId } });
} catch ( err ) {
2026-10-14 22:42:46 -07:00
void showAlert ( errorText ( err ));
2026-10-14 19:35:34 -07:00
b . disabled = false ;
}
};
return b ;
}
2026-10-15 11:08:53 -07:00
function parentOf ( p : Record < string , unknown >) : string {
return typeof p [ "parent_tool_use_id" ] === "string" ? p [ "parent_tool_use_id" ] : "" ;
}
function subagentTag ( parent : string , ctx : Ctx ) : HTMLElement | null {
if ( ! parent ) {
return null ;
}
const sub = ctx . subagents . get ( parent );
return h ( "span" , { class : "subagent-tag" , title : sub?.description ?? "" }, `↳ ${ sub ? . type ?? "subagent" } ` );
}
2026-10-14 16:03:46 -07:00
function renderToolCall ( p : Record < string , unknown >, ctx : Ctx ) : HTMLElement {
const result = h ( "div" , { class : "tool-result pending" }, "running…" );
ctx . tools . set ( String ( p [ "id" ]), result );
return h (
"details" ,
{ class : "tool" },
2026-10-15 20:29:44 -07:00
h ( "summary" , {}, subagentTag ( parentOf ( p ), ctx ), h ( "span" , { class : "tool-name" }, toolLabel ( String ( p [ "name" ]))), " " , h ( "span" , { class : "tool-brief" }, toolBrief ( p [ "input" ], ctx . root ()))),
2026-10-15 16:52:20 -07:00
copyable ( h ( "pre" , { class : "tool-input" }, JSON . stringify ( p [ "input" ], null , 2 ))),
2026-10-14 16:03:46 -07:00
result ,
);
}
2026-10-15 20:29:44 -07:00
function toolLabel ( name : string ) : string {
const prefix = "mcp__byre__" ;
if ( ! name . startsWith ( prefix )) {
return name ;
}
return name
. slice ( prefix . length )
. split ( "_" )
. map (( w ) => w . charAt ( 0 ). toUpperCase () + w . slice ( 1 ))
. join ( " " );
}
2026-10-15 11:08:53 -07:00
function isSubagentCall ( p : Record < string , unknown >) : boolean {
return p [ "name" ] === "Agent" || p [ "name" ] === "Task" ;
}
function renderSubagent ( p : Record < string , unknown >, ctx : Ctx ) : HTMLElement {
const input = ( typeof p [ "input" ] === "object" && p [ "input" ] !== null ? p [ "input" ] : {}) as Record < string , unknown >;
const id = String ( p [ "id" ]);
const type = String ( input [ "subagent_type" ] ?? "subagent" );
const description = String ( input [ "description" ] ?? "" );
const status = h ( "span" , { class : "subagent-status" }, "starting…" );
const result = h ( "div" , { class : "tool-result pending" }, "running…" );
const tag = subagentTag ( parentOf ( p ), ctx );
ctx . tools . set ( id , result );
ctx . subagents . set ( id , { type , description , status });
return h (
"details" ,
{ class : "tool subagent" },
h ( "summary" , {}, tag , h ( "span" , { class : "tool-name" }, ` ${ p [ "name" ] } ${ type } ` ), " " , h ( "span" , { class : "tool-brief" }, description ), " " , status ),
markdown ( String ( input [ "prompt" ] ?? "" ), "text md subagent-prompt" , { taskId : ctx.taskId }),
result ,
);
}
function claudeTaskStatus ( p : Record < string , unknown >, ctx : Ctx ) : void {
if ( p [ "type" ] !== "system" ) {
return ;
}
const sub = ctx . subagents . get ( String ( p [ "tool_use_id" ]));
if ( ! sub ) {
return ;
}
switch ( p [ "subtype" ]) {
case "task_started" :
replace ( sub . status , p [ "is_backgrounded" ] === true ? "running in the background" : "running" );
return ;
case "task_progress" :
replace ( sub . status , String ( p [ "description" ] ?? "running" ));
return ;
case "task_notification" :
sub . status . className = `subagent-status ${ p [ "status" ] === "completed" ? "done" : "error" } ` ;
replace ( sub . status , String ( p [ "status" ]));
return ;
}
}
2026-10-15 20:07:36 -07:00
function markReview ( button : HTMLElement , r : Review ) : void {
const n = findingCount ( r );
2026-10-15 18:34:12 -07:00
button . classList . toggle ( "attention" , n > 0 );
2026-10-15 20:07:36 -07:00
if ( r . state === "running" ) {
replace ( button , "review" , h ( "span" , { class : "review-mark spinner" , title : "review in progress" }));
return ;
}
if ( r . state === "failed" ) {
replace ( button , "review" );
return ;
}
if ( r . state === "skipped" ) {
replace ( button , "review" , h ( "span" , { class : "review-mark skipped" , title : "skipped: no changes" }, "●" ));
return ;
}
if ( n === 0 ) {
2026-10-16 10:44:27 -07:00
replace ( button , "review" , h ( "span" , { class : "review-mark clean" , title : Object.keys ( r . finding_states ). length > 0 ? "every finding addressed" : "no findings" }, "✓" ));
2026-10-15 20:07:36 -07:00
return ;
}
2026-10-16 10:44:27 -07:00
replace ( button , "review" , h ( "span" , { class : "badge" , title : ` ${ n } open findings` }, n ));
2026-10-15 18:34:12 -07:00
}
function snapRow ( snap : Snapshot , ctx : Ctx ) : HTMLElement {
const taskId = ctx . taskId ;
2026-10-15 11:36:51 -07:00
const changes = h ( "button" , { type : "button" , title : `Show snapshot ${ snap . n } in the Changes tab` }, "changes" );
changes . onclick = () => {
pinSnapshot ( snap . n );
2026-10-15 09:40:30 -07:00
focusTab ( "changes" );
2026-10-15 00:08:32 -07:00
};
2026-10-15 11:36:51 -07:00
const fork = h ( "button" , { type : "button" , title : "Start a new task from this snapshot, its agent continuing the conversation as it stood here" }, "fork" );
fork . onclick = async () => {
if ( ! taskId ) {
return ;
}
fork . disabled = true ;
try {
const task = await post ( "/api/tasks/{taskId}/snapshots/{n}/fork" , { path : { taskId , n : snap.n } });
focusTask ( task . id );
} catch ( err ) {
fork . disabled = false ;
void showAlert ( errorText ( err ));
}
};
2026-10-15 17:46:41 -07:00
const review = h ( "button" , { type : "button" , title : `Show the review of snapshot ${ snap . n } in the Reviews tab` }, "review" );
review . onclick = () => showReview ( snap . n );
2026-10-15 18:34:12 -07:00
ctx . reviewButtons . set ( snap . n , review );
2026-10-15 11:36:51 -07:00
const time = new Date ( snap . created_at ). toLocaleTimeString ([], { hour12 : false });
2026-10-15 17:46:41 -07:00
return h ( "div" , { class : "snap-row" , title : ` ${ snap . trigger . replace ( "_" , " " ) } · ${ time } ` }, `snapshot # ${ snap . n } ` , changes , fork , review );
2026-10-15 00:08:32 -07:00
}
2026-10-14 16:03:46 -07:00
function runSummary ( calls : Call []) : HTMLElement {
const counts = new Map < string , { call : Call ; n : number }>();
for ( const c of calls ) {
const key = ` ${ c . name } \ 0 ${ c . target } ` ;
const entry = counts . get ( key );
if ( entry ) {
entry . n ++ ;
} else {
counts . set ( key , { call : c , n : 1 });
}
}
if ( counts . size === 0 ) {
return h ( "span" , { class : "tool-brief" }, "thinking" );
}
const parts : ( HTMLElement | string )[] = [];
for ( const { call , n } of counts . values ()) {
if ( parts . length > 0 ) {
parts . push ( ", " );
}
parts . push (
h ( "span" , {}, h ( "span" , { class : "tool-name" }, call . name ), call . target ? h ( "span" , { class : "tool-brief" }, " " , call . target ) : null , n > 1 ? h ( "span" , { class : "tool-count" }, ` × ${ n } ` ) : null ),
);
}
return h ( "span" , {}, ... parts );
}
function renderEvent ( ev : ByreEvent , ctx : Ctx ) : HTMLElement | null {
2026-10-14 09:40:12 -07:00
const p = ev . payload as Record < string , unknown >;
2026-10-15 07:27:17 -07:00
const md = { taskId : ev.task_id };
2026-10-14 09:40:12 -07:00
const when = h ( "time" , { dateTime : ev.created_at , title : ev.created_at }, ago ( ev . created_at ));
switch ( ev . kind ) {
case "user.message" :
2026-10-15 09:59:13 -07:00
ctx . quiet = false ;
2026-10-14 16:03:46 -07:00
if ( ev . actor === "byre" ) {
2026-10-14 19:35:34 -07:00
const text = String ( p [ "text" ]);
2026-10-14 23:40:32 -07:00
const first = text . split ( "\n" )[ 0 ] ?? "" ;
const brief = first . length > 120 ? ` ${ first . slice ( 0 , 120 ) } …` : first ;
return h (
"details" ,
2026-10-15 13:28:40 -07:00
{ class : "tool byre-prompt" , hidden : p [ "continuation" ] === true },
2026-10-14 23:40:32 -07:00
h ( "summary" , {}, h ( "span" , { class : "tool-name" }, "byre" ), " " , h ( "span" , { class : "tool-brief" }, brief ), " " , when ),
2026-10-15 07:27:17 -07:00
markdown ( text , "text md" , md ),
2026-10-14 23:40:32 -07:00
);
2026-10-14 16:03:46 -07:00
}
2026-10-14 20:07:26 -07:00
return h (
"div" ,
{ class : "msg user" },
h ( "div" , { class : "who" }, ev . actor , " " , when ),
messageImages ( p [ "images" ]),
2026-10-15 07:27:17 -07:00
String ( p [ "text" ] ?? "" ). trim () ? markdown ( String ( p [ "text" ]), "text md" , md ) : null ,
2026-10-14 20:07:26 -07:00
);
2026-10-14 09:40:12 -07:00
case "agent.message" :
2026-10-15 11:08:53 -07:00
if ( parentOf ( p )) {
return h ( "div" , { class : "msg agent subagent" }, h ( "div" , { class : "who" }, subagentTag ( parentOf ( p ), ctx ), " " , when ), markdown ( String ( p [ "text" ]), "text md" , md ));
}
2026-10-15 13:28:40 -07:00
if ( String ( p [ "text" ]). trim (). split ( "\n" ). pop () ? . trim () === quietReply ) {
2026-10-15 09:59:13 -07:00
ctx . quiet = true ;
return null ;
}
ctx . quiet = false ;
2026-10-15 20:01:53 -07:00
return h ( "div" , { class : "msg agent" }, markdown ( String ( p [ "text" ]), "text md" , md ));
2026-10-14 09:40:12 -07:00
case "agent.thinking" :
2026-10-15 11:08:53 -07:00
return h ( "details" , { class : "thinking" }, h ( "summary" , {}, subagentTag ( parentOf ( p ), ctx ), "thinking" ), markdown ( String ( p [ "text" ]), "text md" , md ));
2026-10-14 09:40:12 -07:00
case "agent.tool_result" : {
2026-10-14 16:03:46 -07:00
const target = ctx . tools . get ( String ( p [ "tool_use_id" ]));
2026-10-15 16:52:20 -07:00
const body = copyable ( h ( "pre" , {}, resultText ( p [ "content" ])));
2026-10-14 09:40:12 -07:00
if ( ! target ) {
return h ( "div" , { class : "tool-result orphan" }, body );
}
2026-10-14 21:16:05 -07:00
target . className = "tool-result" ;
2026-10-14 09:40:12 -07:00
replace ( target , body );
return null ;
}
case "turn.ended" :
2026-10-14 19:35:34 -07:00
if ( p [ "handoff" ] === true ) {
return h ( "div" , { class : "note" }, "handoff written; continuing in a new session with a fresh context " , when );
}
2026-10-14 09:40:12 -07:00
return h ( "div" , { class : `turn-end ${ p [ "is_error" ] ? "error" : "" } ` }, p [ "is_error" ] ? `turn ended: ${ String ( p [ "subtype" ]). replaceAll ( "_" , " " ) } ` : "" );
case "session.created" :
return h ( "div" , { class : "note" }, ` ${ ev . actor } started a ${ p [ "vendor" ] } session` , p [ "model" ] ? ` ( ${ p [ "model" ] } )` : "" , " " , when );
2026-10-14 19:35:34 -07:00
case "session.model" :
return h ( "div" , { class : "note" }, ` ${ ev . actor } switched the model to ${ p [ "model" ] } ` , when );
2026-10-14 09:40:12 -07:00
case "session.state" :
return p [ "error" ] ? h ( "div" , { class : "note error" }, `session ${ p [ "state" ] } : ${ p [ "error" ] } ` ) : null ;
case "inbox.created" : {
const kind = p [ "kind" ];
if ( kind === "question" ) {
const q = p [ "payload" ] as Record < string , unknown >;
2026-10-16 08:35:49 -07:00
return h ( "div" , { class : "note ask" }, "asked: " , String ( q [ "question" ]));
2026-10-14 09:40:12 -07:00
}
if ( kind === "session_error" || kind === "rate_limited" ) {
const q = p [ "payload" ] as Record < string , unknown >;
return h ( "div" , { class : "note error" }, String ( q [ "error" ]));
}
return null ;
}
2026-10-15 11:36:51 -07:00
case "task.created" : {
2026-10-16 10:36:38 -07:00
if ( typeof p [ "forked_from" ] === "string" ) {
return h (
"div" ,
{ class : "note" },
` ${ ev . actor } forked this task from ` ,
taskLink ( p [ "forked_from" ]),
` at snapshot # ${ String ( p [ "forked_at" ]) } ; its agent continues that task's conversation as it stood there ` ,
when ,
);
2026-10-15 11:36:51 -07:00
}
2026-10-16 10:36:38 -07:00
if ( typeof p [ "parent_task_id" ] === "string" ) {
return h ( "div" , { class : "note" }, "subtask of " , taskLink ( p [ "parent_task_id" ]), ", created by its agent " , when );
}
return null ;
2026-10-15 11:36:51 -07:00
}
2026-10-16 10:36:38 -07:00
case "task.parent_merge_started" :
return h ( "div" , { class : "note" }, ` ${ ev . actor } asked ` , taskLink ( String ( p [ "parent_task_id" ])), "'s agent to merge this subtask into its branch " , when );
case "task.parent_merge_stopped" :
return h ( "div" , { class : "note error" }, "the merge into the parent task didn't start " , when );
2026-10-14 21:21:54 -07:00
case "task.merge_title" :
return h ( "div" , { class : "note" }, ` ${ ev . actor } set the merge commit title to " ${ String ( p [ "merge_title" ]) } " ` , when );
2026-10-15 16:33:17 -07:00
case "review.finished" : {
const r = p as unknown as Review ;
2026-10-15 20:07:36 -07:00
const outcome = r . state === "failed" ? `failed: ${ r . error } ` : r . state === "skipped" ? "skipped: no changes" : reviewCounts ( r );
2026-10-15 17:46:41 -07:00
return h ( "div" , { class : `note ${ r . state === "failed" ? " error" : "" } ` }, `review of snapshot # ${ r . snapshot_n } : ${ outcome } ` , when );
2026-10-15 16:33:17 -07:00
}
2026-10-15 12:11:35 -07:00
case "task.merge_queued" :
return h ( "div" , { class : "note" }, ` ${ ev . actor } asked to merge; held until the merge in progress in this project is done ` , when );
2026-10-15 19:36:42 -07:00
case "task.merge_stopped" :
return h ( "div" , { class : "note" }, ` ${ ev . actor } stopped the merge for review: ${ String ( p [ "reason" ]) } ` , when );
2026-10-15 13:15:49 -07:00
case "preview.exposed" : {
const url = previewURL ( Number ( p [ "port" ]));
return h ( "div" , { class : "note" }, ` ${ ev . actor } exposed " ${ String ( p [ "name" ]) } " at ` , h ( "a" , { href : url , target : "_blank" , rel : "noopener" }, url ), " " , when );
2026-10-15 11:52:13 -07:00
}
2026-10-14 19:35:34 -07:00
case "task.merged" :
2026-10-16 10:36:38 -07:00
if ( typeof p [ "parent_task_id" ] === "string" ) {
return h ( "div" , { class : "note" }, ` ${ ev . actor } merged the task into ` , taskLink ( p [ "parent_task_id" ]), `, now at ${ String ( p [ "commit" ]). slice ( 0 , 7 ) } ` , when );
}
2026-10-14 22:45:02 -07:00
return h (
"div" ,
{ class : "note" },
` ${ ev . actor } merged the task ${ p [ "commit" ] ? ` as ${ String ( p [ "commit" ]). slice ( 0 , 7 ) } ` : "" }${ p [ "pushed" ] ? ` and pushed it to ${ String ( p [ "pushed" ]) } ` : "" } ` ,
when ,
);
2026-10-14 19:35:34 -07:00
2026-10-16 10:08:08 -07:00
case "task.summary" :
return h ( "div" , { class : "note" }, ` ${ ev . actor } set the task summary ` , when );
case "task.discard_started" :
return h ( "div" , { class : "note" }, ` ${ ev . actor } asked to discard the task; the agent writes the task summary first ` , when );
2026-10-14 19:35:34 -07:00
case "task.discarded" :
return h ( "div" , { class : "note" }, ` ${ ev . actor } discarded the task ` , when );
2026-10-14 09:40:12 -07:00
}
return null ;
}
2026-10-16 10:36:38 -07:00
function taskLink ( id : string ) : Node {
const task = findTask ( id );
if ( task ? . status !== "active" ) {
return document . createTextNode ( task ? . title ?? id );
}
return h (
"a" ,
{
href : `#/task/ ${ task . id } ` ,
class : "task-link" ,
style : taskStyle ( task . id ),
onclick : ( e : MouseEvent ) => {
e . preventDefault ();
focusTask ( task . id );
},
},
task . title ,
);
}
2026-10-14 20:07:26 -07:00
function messageImages ( ids : unknown ) : HTMLElement | null {
if ( ! Array . isArray ( ids ) || ids . length === 0 ) {
return null ;
}
return h (
"div" ,
{ class : "msg-images" },
... ids . map (( id ) => {
const src = `/api/blobs/ ${ encodeURIComponent ( String ( id )) } ` ;
return h ( "a" , { href : src , target : "_blank" , rel : "noopener" }, h ( "img" , { src , alt : "attached image" , loading : "lazy" }));
}),
);
}
2026-10-15 12:30:10 -07:00
export async function confirmMerge ( taskId : string ) : Promise < void > {
const task = findTask ( taskId );
const sessionState = currentSession ( taskId ) ? . state ;
2026-10-16 08:29:51 -07:00
if ( ! task || task . status !== "active" ) {
return ;
}
if ( sessionState === "running" ) {
void showAlert ( "Agent is mid-turn; merge when it's idle" );
return ;
}
if ( task . merge_queued_at !== undefined ) {
void showAlert ( "Merge already held behind another merge in this project" );
2026-10-15 12:30:10 -07:00
return ;
}
2026-10-16 10:36:38 -07:00
if ( task . merging_into_parent ) {
void showAlert ( "Already waiting for the parent task's agent to merge this subtask" );
return ;
}
2026-10-15 12:30:10 -07:00
const target = state . projects . find (( p ) => p . id === task . project_id ) ? . default_branch ?? "the default branch" ;
2026-10-16 10:36:38 -07:00
const parent = task . parent_task_id !== undefined ? findTask ( task . parent_task_id ) : undefined ;
2026-10-15 12:30:10 -07:00
let coAuthors ;
try {
const [ unmerged , people ] = await Promise . all ([
get ( "/api/tasks/{taskId}/unmerged" , { path : { taskId } }),
get ( "/api/tasks/{taskId}/co-authors" , { path : { taskId } }),
]);
if ( unmerged . commits === 0 && unmerged . files === 0 ) {
2026-10-16 08:29:51 -07:00
void showAlert ( `Nothing to merge: no commits or uncommitted files differ from ${ target } ` );
2026-10-15 12:30:10 -07:00
return ;
}
coAuthors = people ;
} catch ( err ) {
void showAlert ( errorText ( err ));
return ;
}
2026-10-16 09:49:58 -07:00
const title = mergeDrafts . get ( taskId ) ?? task . merge_title ;
mergeDrafts . delete ( taskId );
let input : HTMLInputElement | undefined ;
const edit = h ( "button" , { type : "button" , class : "merge-title-edit" , title : "Edit the commit title" , ariaLabel : "Edit the commit title" }, "✎" );
const row = h ( "div" , { class : "merge-title" }, h ( "span" , { class : "merge-title-text" }, title ), edit );
edit . onclick = () => {
const field = h ( "input" , { type : "text" , value : title , required : true , maxLength : 72 , pattern : ".*\\S.*" , ariaLabel : "Commit title" });
field . onkeydown = ( e ) => {
if ( e . key === "Enter" ) {
e . preventDefault ();
row . closest ( "form" ) ? . querySelector < HTMLButtonElement >( ".modal-actions button:last-child" ) ? . click ();
}
};
input = field ;
replace ( row , field );
field . focus ();
field . select ();
};
const warnings = h ( "ul" , { class : "merge-warnings" });
2026-10-16 10:36:38 -07:00
const into = h ( "p" , { class : "merge-into" }, `Merge into ${ target } as:` );
2026-10-16 09:49:58 -07:00
const co = coAuthorsSummary ( coAuthors );
2026-10-16 10:36:38 -07:00
const toMain : Node [] = co ? [ into , row , co ] : [ into , row ];
let toParent = false ;
const question : Node [] = [];
if ( parent ? . status === "active" ) {
toParent = true ;
const option = ( value : boolean , label : string , hint : string ) => {
const radio = h ( "input" , { type : "radio" , name : "merge-target" , checked : value === toParent });
radio . onchange = () => {
toParent = value ;
for ( const n of toMain ) {
( n as HTMLElement ). hidden = toParent ;
}
};
return h ( "label" , { class : "merge-target" }, radio , h ( "span" , {}, label , h ( "span" , { class : "merge-target-hint" }, hint )));
};
question . push (
h (
"div" ,
{ class : "merge-targets" },
option ( true , `Into ${ parent . title } ` , "Its agent merges this subtask's branch into the parent's branch; the subtask closes once it has." ),
option ( false , `Into ${ target } ` , "Squashed into one commit on the default branch and pushed, like any task." ),
),
);
for ( const n of toMain ) {
( n as HTMLElement ). hidden = true ;
}
2026-10-15 18:55:20 -07:00
}
2026-10-16 10:36:38 -07:00
question . push (... toMain . slice ( 0 , 2 ), warnings , ... toMain . slice ( 2 ));
2026-10-16 09:49:58 -07:00
const leave = ( go : () => void ) => {
mergeDrafts . set ( taskId , input ? . value ?? title );
warnings . closest ( "dialog" ) ? . close ( "cancel" );
go ();
};
const watch = watchMergeWarnings ( taskId , warnings , leave );
await watch . ready ;
try {
await confirmThen ( question , "Merge" , false , document . querySelector ( ".merge-button" ), async () => {
2026-10-16 10:36:38 -07:00
const body = toParent ? { target : "parent" as const } : { merge_title : input?.value ?? title };
await post ( "/api/tasks/{taskId}/merge" , { path : { taskId }, body });
2026-10-16 09:49:58 -07:00
});
} finally {
watch . dispose ();
}
}
const mergeDrafts = new Map < string , string >();
function watchMergeWarnings ( taskId : string , list : HTMLElement , leave : ( go : () => void ) => void ) : { ready : Promise < void >; dispose : () => void } {
let reviewItems : HTMLElement [] = [];
let target = 0 ;
const render = () => {
const questions = state . inbox . filter (( i ) => i . task_id === taskId && i . kind === "question" ). length ;
const items = [... reviewItems ];
if ( questions > 0 ) {
const text = questions === 1 ? "The agent's open question will be dismissed" : `The agent's ${ questions } open questions will be dismissed` ;
items . push ( mergeWarning ( text , () => leave (() => focusTask ( taskId , "inbox" ))));
}
replace ( list , ... items );
list . hidden = items . length === 0 ;
};
const load = async () => {
const started = ++ target ;
let items : HTMLElement [];
try {
items = await reviewWarnings ( taskId , leave );
} catch ( err ) {
items = [ mergeWarning ( "Review status unavailable" , undefined , errorText ( err ))];
}
if ( started === target ) {
reviewItems = items ;
render ();
}
};
const offs = [
on ( "event" , ( detail ) => {
const ev = detail as ByreEvent ;
if ( ev . task_id === taskId && ( ev . kind === "snapshot.created" || ev . kind . startsWith ( "review." ))) {
void load ();
}
}),
on ( "inbox" , render ),
];
return {
ready : load (),
dispose : () => {
target ++ ;
for ( const off of offs ) {
off ();
}
},
};
}
async function reviewWarnings ( taskId : string , leave : ( go : () => void ) => void ) : Promise < HTMLElement [] > {
const [ snaps , reviews ] = await Promise . all ([ get ( "/api/tasks/{taskId}/snapshots" , { path : { taskId } }), get ( "/api/tasks/{taskId}/reviews" , { path : { taskId } })]);
const latest = snaps . at ( - 1 );
if ( ! latest ) {
return [ mergeWarning ( "These changes haven't been reviewed" )];
}
const items : HTMLElement [] = [];
const open = () =>
leave (() => {
focusTask ( taskId , "reviews" );
pickReview ( latest . n );
});
const r = reviews . find (( r ) => r . snapshot_n === latest . n );
if ( ! r ) {
items . push ( mergeWarning ( "The latest snapshot hasn't been reviewed" , open ));
} else if ( r . state === "running" ) {
items . push ( mergeWarning ( "Review of the latest snapshot is still running" , open ));
} else if ( r . state === "failed" ) {
items . push ( mergeWarning ( "Review of the latest snapshot failed" , open ));
2026-10-16 10:44:27 -07:00
} else if ( unignoredFindings ( r ). length > 0 ) {
items . push ( mergeWarning ( `Review of the latest snapshot found ${ severityCounts ( unignoredFindings ( r )) } ` , open ));
2026-10-16 09:49:58 -07:00
}
const changes = await get ( "/api/tasks/{taskId}/changes" , { path : { taskId }, query : { since : latest.commit_sha } });
if ( changes . files . length > 0 ) {
items . push ( mergeWarning ( "Changes since the last snapshot haven't been reviewed" ));
}
return items ;
}
function mergeWarning ( text : string , open ?: () => void , detail? : string ) : HTMLElement {
if ( ! open ) {
return h ( "li" , { class : "merge-warning" , title : detail ?? "" }, `⚠ ${ text } ` );
}
const link = h ( "button" , { type : "button" , class : "merge-warning-link" }, text );
link . onclick = open ;
return h ( "li" , { class : "merge-warning" }, "⚠ " , link );
}
function coAuthorsSummary ( coAuthors : Participant []) : HTMLElement | null {
if ( coAuthors . length === 0 ) {
return null ;
}
const missing = coAuthors . filter (( p ) => ! p . git_name || ! p . git_email ). length ;
const label = ` ${ coAuthors . length } co-author ${ coAuthors . length === 1 ? "" : "s" }${ missing > 0 ? ` ( ${ missing } without git identity)` : "" } ` ;
return h ( "details" , { class : "merge-coauthors" }, h ( "summary" , {}, label ), h ( "p" , { class : "modal-text" }, mergeTrailers ( coAuthors )));
2026-10-15 12:30:10 -07:00
}
2026-10-15 18:55:20 -07:00
function mergeTrailers ( coAuthors : Participant []) : string {
const lines : string [] = [];
2026-10-15 07:15:51 -07:00
const credited = coAuthors . filter (( p ) => p . git_name && p . git_email );
const missing = coAuthors . filter (( p ) => ! p . git_name || ! p . git_email );
if ( credited . length > 0 ) {
2026-10-15 18:55:20 -07:00
lines . push ( "Co-authored-by:" , ... credited . map (( p ) => ` ${ p . git_name } < ${ p . git_email } >` ));
2026-10-15 07:15:51 -07:00
}
if ( missing . length > 0 ) {
2026-10-15 18:55:20 -07:00
if ( lines . length > 0 ) {
lines . push ( "" );
}
lines . push ( "No git identity, so no trailer:" , ... missing . map (( p ) => ` ${ p . display_name } ( ${ p . login } )` ));
2026-10-15 07:15:51 -07:00
}
return lines . join ( "\n" );
}
2026-10-15 18:55:20 -07:00
async function confirmThen ( question : string | Node [], action : string , danger : boolean , anchor : Element | null , act : () => Promise < void >) {
2026-10-15 14:00:18 -07:00
if ( ! ( await showConfirm ( question , action , danger , anchor ))) {
2026-10-14 22:42:46 -07:00
return ;
}
2026-10-14 19:35:34 -07:00
try {
2026-10-14 21:01:38 -07:00
await act ();
2026-10-14 19:35:34 -07:00
} catch ( err ) {
2026-10-14 22:42:46 -07:00
await showAlert ( errorText ( err ));
2026-10-14 19:35:34 -07:00
}
}
2026-10-14 16:03:46 -07:00
function toolBrief ( input : unknown , root : string ) : string {
2026-10-14 09:40:12 -07:00
if ( typeof input !== "object" || input === null ) {
return "" ;
}
const i = input as Record < string , unknown >;
const brief = i [ "command" ] ?? i [ "file_path" ] ?? i [ "pattern" ] ?? i [ "url" ] ?? i [ "question" ] ?? i [ "description" ] ?? "" ;
2026-10-14 16:03:46 -07:00
const text = relative ( String ( brief ), root );
2026-10-14 09:40:12 -07:00
return text . length > 120 ? ` ${ text . slice ( 0 , 120 ) } …` : text ;
}
2026-10-14 16:03:46 -07:00
function toolTarget ( input : unknown , root : string ) : string {
if ( typeof input !== "object" || input === null ) {
return "" ;
}
const path = ( input as Record < string , unknown >)[ "file_path" ];
return typeof path === "string" ? relative ( path , root ) : "" ;
}
function relative ( path : string , root : string ) : string {
return root && path . startsWith ( ` ${ root } /` ) ? path . slice ( root . length + 1 ) : path ;
}
2026-10-14 09:40:12 -07:00
function resultText ( content : unknown ) : string {
if ( typeof content === "string" ) {
return content ;
}
if ( Array . isArray ( content )) {
return content . map (( c ) => ( typeof c === "object" && c !== null && "text" in c ? String ( c . text ) : JSON . stringify ( c ))). join ( "\n" );
}
return JSON . stringify ( content , null , 2 );
}