diff --git a/packages/webui/src/public/bus.js b/packages/webui/src/public/bus.js index 876dbad8a..42643689a 100644 --- a/packages/webui/src/public/bus.js +++ b/packages/webui/src/public/bus.js @@ -558,10 +558,14 @@ // REQ-WEB-1: drafts survive polling/refusals; only an explicit submit writes. let settingsSnapshot=null, settingsInputs=null, settingsSaving=false, settingsReceipt=''; const inputKey=parts=>JSON.stringify(parts); + // Row 167 (#1664): one snapshot for the launch form, the assistant forms and the token limit + // reset. A read replaces it only while no form has a draft; every save and reset sets it from + // its answer, so the next submit sends the revision the last write returned. + const launchDrafts=()=>!!settingsInputs||Object.keys(assistantDrafts).length>0; + function takeSnapshot(value){if(!settingsSnapshot||!launchDrafts())settingsSnapshot=value;return settingsSnapshot;} function launchForm(value){ if(!value||value.refused)return `

Agent limits

${txt(value?.message||'Launch settings are not configured.')}

`; - if(!settingsInputs)settingsSnapshot=value; - const v=settingsSnapshot, modelTable=v.models||{}, limits=v.limits||{}, usage=v.usage||{}; + const v=takeSnapshot(value), modelTable=v.models||{}, limits=v.limits||{}, usage=v.usage||{}; const val=(parts,fallback)=>settingsInputs&&Object.hasOwn(settingsInputs,inputKey(parts))?settingsInputs[inputKey(parts)]:fallback; const disabled=!canSend||out()||settingsSaving?' disabled':''; const field=(parts,label,count,current)=>``; @@ -606,7 +610,7 @@ const path=e.target.dataset?.limitReset;if(path===undefined||!canSend||out()||settingsSaving)return; const revision=settingsSnapshot?.revision;if(!/^[a-f0-9]{64}$/.test(revision??''))return; settingsSaving=true;resetReceipt='';render(false);let reread=false; - try{const response=await fetch('/api/settings',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({revision,reset:[JSON.parse(path)]}),cache:'no-store',redirect:'manual'});if(response.type==='opaqueredirect'){resetReceipt='Sign in again.';signOut();}else{const result=await response.json();if(response.ok&&/^[a-f0-9]{64}$/.test(result?.revision??'')){if(!settingsInputs)settingsSnapshot=result;const prior=last.get('api:settings');if(prior)last.set('api:settings',{...prior,launch:result});reread=true;resetReceipt='Limit reset.';}else resetReceipt=result?.sent==='unknown'?'Outcome unknown. Read current settings before trying again.':`${result?.message||'Reset refused.'}`;}} + try{const response=await fetch('/api/settings',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({revision,reset:[JSON.parse(path)]}),cache:'no-store',redirect:'manual'});if(response.type==='opaqueredirect'){resetReceipt='Sign in again.';signOut();}else{const result=await response.json();if(response.ok&&/^[a-f0-9]{64}$/.test(result?.revision??'')){settingsSnapshot=result;const prior=last.get('api:settings');if(prior)last.set('api:settings',{...prior,launch:result});reread=true;resetReceipt='Limit reset.';}else resetReceipt=result?.sent==='unknown'?'Outcome unknown. Read current settings before trying again.':`${result?.message||'Reset refused.'}`;}} catch{resetReceipt='Outcome unknown. Read current settings before trying again.';}finally{settingsSaving=false;if(!out())render(reread);} }); function tokenUsage(value) { @@ -624,12 +628,12 @@ // Row 155 (#1649, Q169): each assistant instance's settings, its seat name and a stop. A save // sends only the fields that changed, with the read's revision. Drafts survive polling and // refusals as in the limits form, and nothing retries. - let assistantSnapshot=null, assistantDrafts=Object.create(null), nameDrafts=Object.create(null), assistantBusy=false, assistantReceipts=Object.create(null); + let assistantDrafts=Object.create(null), nameDrafts=Object.create(null), assistantBusy=false, assistantReceipts=Object.create(null); const asShown=(entry,field)=>field==='definition'?entry.definition??'':entry[field]==null?'':JSON.stringify(entry[field],null,2); function assistantSection(value,names){ - if(!value||value.refused||!value.assistants||!Object.keys(value.assistants).length)return ''; - if(!Object.keys(assistantDrafts).length)assistantSnapshot=value; - const v=assistantSnapshot, disabled=!canSend||out()||assistantBusy?' disabled':''; + if(!value||value.refused)return ''; + const v=takeSnapshot(value);if(!v.assistants||!Object.keys(v.assistants).length)return ''; + const disabled=!canSend||out()||assistantBusy?' disabled':''; const named=names?Object.fromEntries(names.map(r=>[r.instance,r.name])):null; const forms=Object.entries(v.assistants).map(([instance,entry])=>{ const d=assistantDrafts[instance]||{}, val=f=>Object.hasOwn(d,f)?d[f]:asShown(entry,f), k=esc(instance); @@ -639,7 +643,7 @@ return `

Assistant

The definition stays an assistant one. Holder and credentials stay hand edits. Stop ends the runs of your threads; the assistant starts again when you next write to it.

${forms}

${canSend?'Changes are checked and logged with your identity.':'Changes require signed-in send mode.'}

`; } function assistantPayload(instance,form){ - const entry=assistantSnapshot.assistants[instance], patch={}; + const entry=settingsSnapshot.assistants[instance], patch={}; for(const f of ['definition','vars','tracker']){ const text=form.querySelector(`[data-assistant-field="${f}"]`).value, what=f==='vars'?'Vars':'The tracker'; if(text===asShown(entry,f))continue; @@ -650,7 +654,7 @@ patch[f]=parsed; } if(!Object.keys(patch).length)throw Error('Nothing changed.'); - return {revision:assistantSnapshot.revision,assistants:{[instance]:patch}}; + return {revision:settingsSnapshot.revision,assistants:{[instance]:patch}}; } const unknownAssistant='Outcome unknown. Read current settings before trying again. Draft kept.'; async function assistantPost(url,payload,instance,ok){ @@ -663,7 +667,7 @@ document.addEventListener('submit',async e=>{ const form=assistantForm(e.target);if(!form||e.target!==form)return;e.preventDefault();if(!canSend||out()||assistantBusy)return;const instance=form.dataset.instance;let payload; try{payload=assistantPayload(instance,form);}catch(err){assistantReceipts[instance]=err.message;render(false);return;} - await assistantPost('/api/settings',payload,instance,result=>{if(!/^[a-f0-9]{64}$/.test(result?.revision??''))return unknownAssistant;delete assistantDrafts[instance];assistantSnapshot=result;const prior=last.get('api:settings');if(prior)last.set('api:settings',{...prior,launch:result});return 'Assistant settings saved.';}); + await assistantPost('/api/settings',payload,instance,result=>{if(!/^[a-f0-9]{64}$/.test(result?.revision??''))return unknownAssistant;delete assistantDrafts[instance];settingsSnapshot=result;const prior=last.get('api:settings');if(prior)last.set('api:settings',{...prior,launch:result});return 'Assistant settings saved.';}); }); document.addEventListener('click',async e=>{ const form=assistantForm(e.target);if(!form||!canSend||out()||assistantBusy)return;const instance=form.dataset.instance; @@ -726,7 +730,7 @@ const s = await get.api('settings'), n = s.notifier || {}; let usage, names = null; try { [usage] = await get('usage'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; } - try { names = await get('names'); } catch { /* the section says the name could not be read */ } + try { names = await get('names'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; /* the section says the name could not be read */ } let instances = null; try { [instances] = await get('instances'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; } let limitRows = [], limitsRefused = false;