2026-09-13 10:46:28 +00:00
'use strict'
const $ = ( id ) => document . getElementById ( id )
const state = {
data : null ,
logSeeded : false ,
lastSeq : 0 ,
modalIteration : null ,
2026-09-13 16:29:03 +00:00
goals : [ ] ,
selectedFile : '' ,
forceLauncher : false ,
prevStatus : null ,
}
const ACTIVE _STATUSES = new Set ( [ 'starting' , 'running' , 'paused' , 'waiting-answer' ] )
// Top-bar activity indicator: error/warn/problem states tint the bar, active
// states animate it, terminal states settle to a static line.
const ERROR _STATUSES = new Set ( [ 'error' , 'done-with-errors' ] )
const WARN _STATUSES = new Set ( [ 'aborted' , 'stopped-budget' , 'stalled' , 'stopped-guard' ] )
const TOPBAR _CLASSES = [ 'topbar--running' , 'topbar--paused' , 'topbar--waiting' , 'topbar--done' , 'topbar--warn' , 'topbar--error' ]
function topbarState ( s ) {
if ( s . idle === true ) return ''
if ( s . stale || ERROR _STATUSES . has ( s . status ) ) return 'error'
if ( WARN _STATUSES . has ( s . status ) ) return 'warn'
if ( s . status === 'starting' || s . status === 'running' ) return 'running'
if ( s . status === 'waiting-answer' ) return 'waiting'
if ( s . status === 'paused' ) return 'paused'
if ( s . status === 'done' ) return 'done'
return ''
2026-09-13 10:46:28 +00:00
}
const fmt = ( n ) => Number ( n || 0 ) . toLocaleString ( 'en-US' )
const fmtCost = ( n ) => {
const v = Number ( n || 0 )
if ( ! v ) return '$0.0000'
return v < 0.01 ? ` $ ${ v . toFixed ( 5 ) } ` : ` $ ${ v . toFixed ( 4 ) } `
}
const fmtDur = ( ms ) => {
const s = Math . max ( 0 , Math . round ( ( ms || 0 ) / 1000 ) )
const m = Math . floor ( s / 60 )
const h = Math . floor ( m / 60 )
if ( h ) return ` ${ h } h ${ String ( m % 60 ) . padStart ( 2 , '0' ) } m `
return ` ${ String ( m ) . padStart ( 2 , '0' ) } : ${ String ( s % 60 ) . padStart ( 2 , '0' ) } `
}
const esc = ( s ) =>
String ( s ? ? '' ) . replace ( /[&<>"']/g , ( c ) => ( { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' } ) [ c ] )
// ---------- stats ----------
function renderStats ( s ) {
const totals = s . totals || { tokens : { } , cost : 0 , durationMs : 0 }
const done = s . iterations . length
const last = s . iterations [ s . iterations . length - 1 ]
const percent = last ? . percent ? ? 0
const context = last ? . contextSize ? ? 0
2026-09-13 16:29:03 +00:00
const verify = s . verify || { passed : 0 , failed : 0 }
2026-09-13 10:46:28 +00:00
const statuses = s . iterations . map ( ( i ) => i . status )
2026-09-13 16:29:03 +00:00
const statusText = s . stale ? ` ${ s . status } · stale ` : s . status
2026-09-13 10:46:28 +00:00
const cards = [
{
k : 'Progress' ,
v : ` ${ percent } <small>%</small> ` ,
bar : percent ,
} ,
{ k : 'Iteration' , v : ` ${ done } <small> / ${ s . configuredIterations ? ? s . iterations . length } </small> ` } ,
2026-09-13 16:29:03 +00:00
{ k : 'Status' , v : ` <span style="font-size:14px"> ${ esc ( statusText ) } </span> ` } ,
2026-09-13 10:46:28 +00:00
{ k : 'Tokens' , v : ` ${ fmt ( totals . tokens ? . total ) } <small> in ${ fmt ( totals . tokens ? . input ) } · out ${ fmt ( totals . tokens ? . output ) } </small> ` } ,
{ k : 'Cost' , v : fmtCost ( totals . cost ) } ,
{
k : 'Context' ,
v : ` ${ fmt ( context ) } <small> tokens</small> ` ,
} ,
2026-09-13 16:29:03 +00:00
{ k : 'Verify' , v : ` ${ verify . passed || 0 } <small> pass · ${ verify . failed || 0 } fail</small> ` } ,
2026-09-13 10:46:28 +00:00
]
$ ( 'stats' ) . innerHTML = cards
. map (
( c ) => ` <div class="stat"><div class="k"> ${ c . k } </div><div class="v"> ${ c . v } </div> ${
c . bar !== undefined ? ` <div class="bar"><i style="width: ${ Math . max ( 0 , Math . min ( 100 , c . bar ) ) } %"></i></div> ` : ''
} < / d i v > ` ,
)
. join ( '' )
void statuses
}
2026-09-13 16:29:03 +00:00
// ---------- stages (kanban strip) ----------
function renderStages ( s ) {
const box = $ ( 'stages' )
const stages = s . stages || [ ]
if ( ! stages . length ) {
box . innerHTML = '<div class="empty">No stages in the goal.</div>'
return
}
const last = s . iterations [ s . iterations . length - 1 ]
const currentId = last ? . stageId || null
box . innerHTML = stages
. map ( ( st , i ) => {
const isCurrent = st . id === currentId
const cls = st . done ? 'done' : isCurrent ? 'current' : 'pending'
const verify = st . verify ? ` <span class="stage-verify" title="verify: ${ esc ( st . verify ) } ">verify</span> ` : ''
return ` <div class="stage-chip ${ cls } " title=" ${ esc ( st . details || st . title ) } ">
< span class = "n" > $ { i + 1 } < / s p a n > < s p a n c l a s s = " t " > $ { e s c ( s t . t i t l e ) } < / s p a n > $ { v e r i f y }
< / d i v > `
} )
. join ( '' )
const v = s . verify || { }
const hint = $ ( 'stages-hint' )
if ( hint ) hint . textContent = ` verification gate · ${ v . passed || 0 } passed · ${ v . failed || 0 } failed `
}
2026-09-13 10:46:28 +00:00
// ---------- iterations ----------
function renderIterations ( s ) {
const box = $ ( 'iterations' )
if ( ! s . iterations . length ) {
box . innerHTML = '<div class="empty">No iterations yet.</div>'
return
}
box . innerHTML = s . iterations
. map ( ( it ) => {
const pct = it . percent ? ? 0
const status = it . status || 'pending'
const questions = ( it . questions || [ ] ) . length
? ` <ul class="qlist"> ${ it . questions . map ( ( q ) => ` <li> ${ esc ( q . text ) } </li> ` ) . join ( '' ) } </ul> `
: ''
const files = ( it . filesChanged || [ ] ) . length
? ` <div class="files"> ${ it . filesChanged . slice ( 0 , 8 ) . map ( ( f ) => ` <span class="chip"> ${ esc ( f ) } </span> ` ) . join ( '' ) } ${
it . filesChanged . length > 8 ? ` <span class="chip">+ ${ it . filesChanged . length - 8 } </span> ` : ''
} < / d i v > `
: ''
return ` <div class="iter ${ esc ( status ) } ">
< div class = "head" >
< span class = "num" > # $ { it . index } < / s p a n >
< span class = "topic" > $ { esc ( it . topic || 'running…' ) } < / s p a n >
2026-09-13 16:29:03 +00:00
< span class = "badge mode" > $ { s . sharedContext ? 'shared ctx' : 'fresh ctx' } < / s p a n >
2026-09-13 10:46:28 +00:00
< span class = "badge ${esc(status)}" > $ { esc ( status ) } < / s p a n >
< / d i v >
< div class = "meta" >
< span > $ { fmt ( it . tokens ? . total ) } tok < / s p a n >
< span > $ { fmtCost ( it . cost ) } < / s p a n >
< span > ctx $ { fmt ( it . contextSize ) } < / s p a n >
< span > $ { fmtDur ( it . durationMs ) } < / s p a n >
$ { it . toolCount ? ` <span> ${ it . toolCount } tools</span> ` : '' }
2026-09-13 16:29:03 +00:00
$ { it . verify ? ` <span class="vb ${ it . verify . ok ? 'ok' : 'bad' } ">verify ${ it . verify . ok ? 'pass' : it . verify . timedOut ? 'timeout' : 'FAIL' } </span> ` : '' }
$ { it . attempts > 1 ? ` <span class="vb warn" title=" ${ esc ( it . retry ? . reason || 'model connection error' ) } ">auto-retry × ${ it . attempts } </span> ` : '' }
$ { it . guardViolation ? ` <span class="vb bad">guard blocked</span> ` : '' }
$ { it . checkpointId ? ` <span title="checkpoint before this iteration">cp ${ esc ( it . checkpointId ) } </span> ` : '' }
$ { it . sessionID ? ` <span class="sess" title=" ${ esc ( it . sessionID ) } ">session ${ esc ( String ( it . sessionID ) . slice ( 0 , 12 ) ) } …</span> ` : '' }
2026-09-13 10:46:28 +00:00
< / d i v >
< div class = "stage" > < span class = "lbl" > Stage : < / s p a n > $ { e s c ( i t . s t a g e T i t l e | | ' — ' ) } < s p a n c l a s s = " l b l " > [ $ { e s c ( i t . s t a g e S t a t u s | | ' n / a ' ) } ] < / s p a n > < / d i v >
< div class = "stage" > < span class = "lbl" > Next : < / s p a n > $ { e s c ( i t . n e x t S t a g e | | ' — ' ) } < / d i v >
$ { it . summary ? ` <div class="summary"> ${ esc ( it . summary ) } </div> ` : '' }
$ { files }
$ { questions }
2026-09-13 16:29:03 +00:00
$ { it . checkpointId ? ` <div class="actions"><button class="revert" data-iter=" ${ it . index } " title="Restore the working tree to the checkpoint taken before this iteration">Revert before # ${ it . index } </button></div> ` : '' }
2026-09-13 10:46:28 +00:00
< div class = "bar" > < i style = "width:${Math.max(0, Math.min(100, pct))}%" > < / i > < / d i v >
< / d i v > `
} )
. join ( '' )
void box
}
// ---------- log ----------
function logNode ( entry ) {
const div = document . createElement ( 'div' )
div . className = ` line ${ entry . level || 'info' } `
const time = new Date ( entry . t ) . toLocaleTimeString ( 'en-US' , { hour12 : false } )
const prefix = entry . level === 'section' ? '' : ` <span class="t"> ${ time } </span> `
div . innerHTML = prefix + esc ( entry . text )
return div
}
function appendLog ( entry ) {
if ( ! entry || entry . seq <= state . lastSeq ) return
state . lastSeq = entry . seq
const log = $ ( 'log' )
const nearBottom = log . scrollHeight - log . scrollTop - log . clientHeight < 60
log . appendChild ( logNode ( entry ) )
while ( log . childElementCount > 2000 ) log . removeChild ( log . firstChild )
if ( nearBottom || entry . level === 'section' ) log . scrollTop = log . scrollHeight
}
function seedLog ( entries ) {
const log = $ ( 'log' )
log . innerHTML = ''
state . lastSeq = 0
for ( const e of entries || [ ] ) {
state . lastSeq = Math . max ( state . lastSeq , e . seq || 0 )
log . appendChild ( logNode ( e ) )
}
log . scrollTop = log . scrollHeight
}
// ---------- question modal ----------
function renderModal ( s ) {
const modal = $ ( 'modal' )
const pq = s . pendingQuestion
if ( ! pq ) {
modal . classList . add ( 'hidden' )
state . modalIteration = null
return
}
if ( state . modalIteration === pq . iteration ) return
state . modalIteration = pq . iteration
const questions = pq . questions . length ? pq . questions : [ { id : 'q1' , text : 'Any corrections before the next iteration?' , options : [ ] } ]
$ ( 'modal-sub' ) . textContent = ` Iteration ${ pq . iteration } finished. Answers are injected into the next session. `
$ ( 'modal-questions' ) . innerHTML = questions
. map (
( q , i ) => ` <div class="q" data-i=" ${ i } ">
< label > $ { esc ( q . text ) } < / l a b e l >
$ {
q . options ? . length
? ` <div class="opts"> ${ q . options
. map ( ( o ) => ` <button type="button" class="opt" data-val=" ${ esc ( o ) } "> ${ esc ( o ) } </button> ` )
. join ( '' ) } < / d i v > `
: ''
}
< input type = "text" placeholder = "Type your answer (Enter to submit)" / >
< / d i v > ` ,
)
. join ( '' )
modal . classList . remove ( 'hidden' )
const first = $ ( 'modal-questions' ) . querySelector ( 'input' )
if ( first ) setTimeout ( ( ) => first . focus ( ) , 30 )
$ ( 'modal-questions' )
. querySelectorAll ( '.opt' )
. forEach ( ( btn ) => {
btn . addEventListener ( 'click' , ( ) => {
const input = btn . closest ( '.q' ) . querySelector ( 'input' )
input . value = btn . dataset . val
input . focus ( )
} )
} )
}
async function submitModal ( skip ) {
const questions = [ ... $ ( 'modal-questions' ) . querySelectorAll ( '.q' ) ]
const answers = questions . map ( ( q ) => ( {
question : q . querySelector ( 'label' ) ? . textContent || '' ,
answer : skip ? '' : q . querySelector ( 'input' ) ? . value ? . trim ( ) || '' ,
} ) )
await fetch ( '/api/answer' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { answers } ) ,
} )
$ ( 'modal' ) . classList . add ( 'hidden' )
state . modalIteration = null
}
2026-09-13 16:29:03 +00:00
// ---------- launcher (plan picker) ----------
async function loadGoals ( ) {
try {
const res = await fetch ( '/api/goals' )
state . goals = await res . json ( )
} catch {
state . goals = [ ]
}
const sel = $ ( 'goal-select' )
if ( ! state . goals . length ) {
sel . innerHTML = '<option value="">No plans found</option>'
selectGoal ( '' )
return
}
sel . innerHTML = state . goals
. map ( ( g ) => ` <option value=" ${ esc ( g . file ) } "> ${ esc ( g . title ) } — ${ g . done } / ${ g . total } ( ${ g . percent } %)</option> ` )
. join ( '' )
const keep = state . goals . some ( ( g ) => g . file === state . selectedFile ) ? state . selectedFile : state . goals [ 0 ] . file
sel . value = keep
selectGoal ( keep )
}
function selectGoal ( file ) {
state . selectedFile = file
const g = state . goals . find ( ( x ) => x . file === file )
const meta = $ ( 'goal-meta' )
const list = $ ( 'goal-stages' )
for ( const id of [ 'btn-mark-all' , 'btn-clear-all' , 'btn-delete-goal' ] ) {
const btn = $ ( id )
if ( btn ) btn . disabled = ! g
}
if ( ! g ) {
meta . textContent = ''
list . innerHTML = ''
return
}
const last = g . lastRun
? ` last run ${ g . lastRun . status } · ${ g . lastRun . iterations } it · $ ${ ( g . lastRun . cost || 0 ) . toFixed ( 4 ) } `
: 'never run'
meta . textContent = ` ${ g . file } — ${ g . done } / ${ g . total } done ( ${ g . percent } %) · ${ g . type } · ${ last } `
$ ( 'global-verify' ) . value = g . globalVerify || ''
list . innerHTML =
g . stages
. map (
( st , i ) => ` <div class="goal-stage ${ st . done ? 'done' : '' } ">
< span class = "st-state" > $ { st . done ? 'done' : 'todo' } < / s p a n >
< span class = "st-title" title = "${esc(st.title)}" > $ { i + 1 } . $ { esc ( st . title ) } < / s p a n >
< input type = "text" data - stage = "${i}" placeholder = "verify command" value = "${esc(st.verify || '')}" / >
< / d i v > ` ,
)
. join ( '' ) || '<div class="empty">This plan has no stages.</div>'
}
async function saveStageVerify ( stageIndex , verify ) {
if ( ! state . selectedFile ) return
try {
const res = await fetch ( '/api/goals/verify' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { file : state . selectedFile , stageIndex , verify } ) ,
} )
const data = await res . json ( )
if ( ! data . ok ) {
$ ( 'launcher-error' ) . textContent = data . error || 'save failed'
return
}
const i = state . goals . findIndex ( ( x ) => x . file === state . selectedFile )
if ( i >= 0 ) state . goals [ i ] = data . goal
selectGoal ( state . selectedFile )
} catch ( err ) {
$ ( 'launcher-error' ) . textContent = err . message
}
}
async function saveGlobalVerify ( ) {
try {
const res = await fetch ( '/api/goals/verify-global' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { verify : $ ( 'global-verify' ) . value } ) ,
} )
const data = await res . json ( )
if ( ! data . ok ) {
$ ( 'launcher-error' ) . textContent = data . error || 'save failed'
return
}
await loadGoals ( )
} catch ( err ) {
$ ( 'launcher-error' ) . textContent = err . message
}
}
// POST a plan action scoped to the currently selected file and surface errors.
async function postGoalAction ( path , body , label ) {
if ( ! state . selectedFile ) return null
try {
const res = await fetch ( path , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { file : state . selectedFile , ... body } ) ,
} )
const data = await res . json ( ) . catch ( ( ) => ( { } ) )
if ( ! res . ok || data . ok === false ) {
$ ( 'launcher-error' ) . textContent = data . error || ` ${ label } failed ( ${ res . status } ) `
return null
}
return data
} catch ( err ) {
$ ( 'launcher-error' ) . textContent = err . message
return null
}
}
async function setGoalDone ( done ) {
$ ( 'launcher-error' ) . textContent = ''
const data = await postGoalAction ( '/api/goals/done' , { done } , done ? 'mark' : 'clear' )
if ( data ) await loadGoals ( )
}
async function deleteGoal ( ) {
const g = state . goals . find ( ( x ) => x . file === state . selectedFile )
const label = g ? . title || state . selectedFile
const ok = confirm (
` Delete the plan " ${ label } "? \n \n The file ${ state . selectedFile } is moved to .kilocode-loop/trash/ so it can be restored. This is not a commit. ` ,
)
if ( ! ok ) return
$ ( 'launcher-error' ) . textContent = ''
const data = await postGoalAction ( '/api/goals/delete' , { } , 'delete' )
if ( data ) {
state . selectedFile = ''
await loadGoals ( )
}
}
async function startRun ( ) {
if ( ! state . selectedFile ) {
$ ( 'launcher-error' ) . textContent = 'Pick a plan first.'
return
}
$ ( 'launcher-error' ) . textContent = ''
const body = {
file : state . selectedFile ,
iterations : Number ( $ ( 'opt-iterations' ) . value ) || 5 ,
agent : $ ( 'opt-agent' ) . value || undefined ,
hitl : $ ( 'opt-hitl' ) . value ,
dryRun : $ ( 'opt-dryrun' ) . checked ,
promptExtra : $ ( 'opt-prompt-extra' ) . value . trim ( ) || undefined ,
}
try {
const res = await fetch ( '/api/start' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( body ) ,
} )
const data = await res . json ( ) . catch ( ( ) => ( { } ) )
if ( ! res . ok || ! data . ok ) {
$ ( 'launcher-error' ) . textContent = data . error || ` start failed ( ${ res . status } ) `
return
}
state . forceLauncher = false
} catch ( err ) {
$ ( 'launcher-error' ) . textContent = err . message
}
}
$ ( 'goal-select' ) . addEventListener ( 'change' , ( e ) => selectGoal ( e . target . value ) )
$ ( 'goal-stages' ) . addEventListener ( 'change' , ( e ) => {
const input = e . target . closest ( 'input[data-stage]' )
if ( input ) saveStageVerify ( Number ( input . dataset . stage ) , input . value )
} )
$ ( 'btn-save-global-verify' ) . addEventListener ( 'click' , saveGlobalVerify )
$ ( 'btn-goals-refresh' ) . addEventListener ( 'click' , loadGoals )
$ ( 'btn-mark-all' ) . addEventListener ( 'click' , ( ) => setGoalDone ( true ) )
$ ( 'btn-clear-all' ) . addEventListener ( 'click' , ( ) => setGoalDone ( false ) )
$ ( 'btn-delete-goal' ) . addEventListener ( 'click' , deleteGoal )
$ ( 'btn-start' ) . addEventListener ( 'click' , startRun )
$ ( 'btn-new-run' ) . addEventListener ( 'click' , ( ) => {
state . forceLauncher = true
loadGoals ( )
render ( state . data || { status : 'idle' , idle : true } )
} )
2026-09-13 10:46:28 +00:00
// ---------- controls ----------
2026-09-13 16:29:03 +00:00
async function control ( action , extra ) {
2026-09-13 10:46:28 +00:00
try {
await fetch ( '/api/control' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-09-13 16:29:03 +00:00
body : JSON . stringify ( { action , ... ( extra || { } ) } ) ,
2026-09-13 10:46:28 +00:00
} )
} catch {
/* ignore */
}
}
// ---------- state ----------
function render ( s ) {
state . data = s
2026-09-13 16:29:03 +00:00
// Top-bar activity indicator (CSS animation drives it; JS only sets state).
const bar = $ ( 'topbar' )
if ( bar ) {
bar . classList . remove ( ... TOPBAR _CLASSES )
const barState = topbarState ( s )
if ( barState ) bar . classList . add ( ` topbar-- ${ barState } ` )
}
const idle = s . idle === true
const stopped = [ 'done' , 'done-with-errors' , 'aborted' , 'error' , 'stopped-budget' , 'stalled' , 'stopped-guard' ] . includes ( s . status )
const showLauncher = idle || state . forceLauncher
$ ( 'launcher' ) . classList . toggle ( 'hidden' , ! showLauncher )
$ ( 'run-view' ) . classList . toggle ( 'hidden' , showLauncher )
$ ( 'toggle-shared' ) . classList . toggle ( 'hidden' , idle )
$ ( 'btn-new-run' ) . classList . toggle ( 'hidden' , idle || ! stopped )
// When a run finishes, refresh the plan list so the launcher shows new state.
if ( state . prevStatus && ACTIVE _STATUSES . has ( state . prevStatus ) && ! ACTIVE _STATUSES . has ( s . status ) ) loadGoals ( )
state . prevStatus = s . status
if ( idle ) {
$ ( 'run-meta' ) . textContent = 'no active run — pick a plan'
const pillIdle = $ ( 'status-pill' )
pillIdle . textContent = 'idle'
pillIdle . className = 'status'
return
}
2026-09-13 10:46:28 +00:00
const pauseBtn = $ ( 'btn-pause' )
const paused = s . status === 'paused' || s . pauseRequested
pauseBtn . textContent = paused ? 'Resume' : 'Pause'
2026-09-13 16:29:03 +00:00
pauseBtn . disabled = stopped
2026-09-13 10:46:28 +00:00
$ ( 'btn-abort' ) . textContent = s . abortRequested ? 'Abort armed ✓' : 'Abort after session'
$ ( 'btn-abort' ) . classList . toggle ( 'primary' , Boolean ( s . abortRequested ) )
const pill = $ ( 'status-pill' )
pill . textContent = s . status
pill . className = ` status ${ s . status } `
const goal = s . goal ? . title || '—'
2026-09-13 16:29:03 +00:00
const mode = s . sharedContext ? 'shared context' : 'fresh sessions'
const custom = s . customInstruction ? ' · custom instruction' : ''
$ ( 'run-meta' ) . textContent = ` ${ s . runId } · ${ goal } · agent ${ s . agent } · ${ mode } ${ custom } ${ s . dryRun ? ' · DRY-RUN' : '' } `
const sharedChk = $ ( 'chk-shared' )
if ( document . activeElement !== sharedChk ) sharedChk . checked = Boolean ( s . sharedContext )
sharedChk . disabled = stopped
$ ( 'toggle-shared' ) . classList . toggle ( 'on' , Boolean ( s . sharedContext ) )
const exportLink = $ ( 'btn-export' )
if ( exportLink ) exportLink . href = s . runId ? ` /api/runs/ ${ encodeURIComponent ( s . runId ) } /export ` : '#'
2026-09-13 10:46:28 +00:00
renderStats ( s )
2026-09-13 16:29:03 +00:00
renderStages ( s )
2026-09-13 10:46:28 +00:00
renderIterations ( s )
renderModal ( s )
}
function connect ( ) {
const es = new EventSource ( '/api/events' )
es . onmessage = ( ev ) => {
let msg
try {
msg = JSON . parse ( ev . data )
} catch {
return
}
if ( msg . type === 'state' ) {
if ( ! state . logSeeded ) {
state . logSeeded = true
seedLog ( msg . state . logs )
}
render ( msg . state )
} else if ( msg . type === 'log' ) {
appendLog ( msg . entry )
}
}
es . onerror = ( ) => {
// EventSource reconnects automatically; surface it in the status pill.
const pill = $ ( 'status-pill' )
if ( pill ) pill . textContent = 'reconnecting…'
}
}
$ ( 'btn-pause' ) . addEventListener ( 'click' , ( ) => {
const paused = state . data ? . status === 'paused' || state . data ? . pauseRequested
control ( paused ? 'resume' : 'pause' )
} )
$ ( 'btn-abort' ) . addEventListener ( 'click' , ( ) => control ( 'abort-after-current' ) )
$ ( 'btn-stop' ) . addEventListener ( 'click' , ( ) => {
if ( confirm ( 'Stop the loop now and kill the current session?' ) ) control ( 'stop' )
} )
2026-09-13 16:29:03 +00:00
// Revert the working tree to the checkpoint taken before an iteration.
$ ( 'iterations' ) . addEventListener ( 'click' , async ( e ) => {
const btn = e . target . closest ( '.revert' )
if ( ! btn ) return
const iteration = Number ( btn . dataset . iter )
if ( ! confirm ( ` Revert the working tree to the checkpoint taken before iteration ${ iteration } ? \n \n This discards tracked and untracked changes made since then. It is not a commit and cannot be undone from here. ` ) ) return
try {
const res = await fetch ( '/api/revert' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { iteration } ) ,
} )
const data = await res . json ( )
if ( ! data . ok ) alert ( data . error || 'revert failed' )
} catch ( err ) {
alert ( ` revert failed: ${ err . message } ` )
}
} )
$ ( 'chk-shared' ) . addEventListener ( 'change' , ( e ) => control ( 'set-shared-context' , { value : e . target . checked } ) )
2026-09-13 10:46:28 +00:00
$ ( 'btn-submit' ) . addEventListener ( 'click' , ( ) => submitModal ( false ) )
$ ( 'btn-skip' ) . addEventListener ( 'click' , ( ) => submitModal ( true ) )
// Enter submits inside the modal.
$ ( 'modal' ) . addEventListener ( 'keydown' , ( e ) => {
if ( e . key === 'Enter' ) {
e . preventDefault ( )
submitModal ( false )
}
} )
// Terminal-style shortcuts mirror the CLI controls.
document . addEventListener ( 'keydown' , ( e ) => {
if ( ! $ ( 'modal' ) . classList . contains ( 'hidden' ) ) return
if ( e . target . tagName === 'INPUT' || e . target . tagName === 'TEXTAREA' ) return
const k = e . key . toLowerCase ( )
if ( k === 'x' ) control ( 'abort-after-current' )
else if ( k === 'p' ) {
const paused = state . data ? . status === 'paused' || state . data ? . pauseRequested
control ( paused ? 'resume' : 'pause' )
} else if ( k === 'q' && e . shiftKey ) control ( 'stop' )
} )
2026-09-13 16:29:03 +00:00
loadGoals ( )
2026-09-13 10:46:28 +00:00
connect ( )