diff --git a/packages/webui/src/public/bus.js b/packages/webui/src/public/bus.js
index 64a7d6674..1e0be849d 100644
--- a/packages/webui/src/public/bus.js
+++ b/packages/webui/src/public/bus.js
@@ -621,16 +621,67 @@
}
return `Token usage
Best reading of session-editable transcripts, not proof. Unknown turns may have used more tokens. Account is unassigned until account tracking is configured.
| Subject | Input | Output | Cache write | Cache read | Unknown turns |
${rows.join('')}
`;
}
+ // 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);
+ 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':'';
+ 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);
+ const name=Object.hasOwn(nameDrafts,instance)?nameDrafts[instance]:named?.[instance]??'';
+ return `
`;
+ }).join('');
+ 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={};
+ 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;
+ if(f==='definition'){patch.definition=text.trim();continue;}
+ if(f==='tracker'&&text.trim()===''){patch.tracker=null;continue;}
+ let parsed;try{parsed=JSON.parse(text);}catch{throw Error(`${what} must be JSON.`);}
+ if(!parsed||typeof parsed!=='object'||Array.isArray(parsed))throw Error(`${what} must be a JSON object.`);
+ patch[f]=parsed;
+ }
+ if(!Object.keys(patch).length)throw Error('Nothing changed.');
+ return {revision:assistantSnapshot.revision,assistants:{[instance]:patch}};
+ }
+ const unknownAssistant='Outcome unknown. Read current settings before trying again. Draft kept.';
+ async function assistantPost(url,payload,instance,ok){
+ assistantBusy=true;assistantReceipts[instance]='';render(false);
+ try{const response=await fetch(url,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload),cache:'no-store',redirect:'manual'});if(response.type==='opaqueredirect'){assistantReceipts[instance]='Sign in again. Draft kept.';signOut();}else{const result=await response.json();assistantReceipts[instance]=response.ok?ok(result):result?.sent==='unknown'?unknownAssistant:`${result?.message||'Refused.'} Draft kept.`;}}
+ catch{assistantReceipts[instance]=unknownAssistant;}finally{assistantBusy=false;if(!out())render(false);}
+ }
+ const assistantForm=el=>el?.closest?.('.assistant-form');
+ for(const kind of ['input','change'])document.addEventListener(kind,e=>{const form=assistantForm(e.target);if(!form)return;const instance=form.dataset.instance,field=e.target.dataset.assistantField;if(field==='name')nameDrafts[instance]=e.target.value;else if(field)assistantDrafts[instance]={...Object.fromEntries(['definition','vars','tracker'].map(f=>[f,form.querySelector(`[data-assistant-field="${f}"]`).value]))};});
+ 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.';});
+ });
+ document.addEventListener('click',async e=>{
+ const form=assistantForm(e.target);if(!form||!canSend||out()||assistantBusy)return;const instance=form.dataset.instance;
+ if(e.target.dataset.assistantName!==undefined){const name=form.querySelector('[data-assistant-field="name"]').value.trim();
+ await assistantPost('/api/name',{instance,name},instance,result=>{if(typeof result?.name!=='string')return unknownAssistant;delete nameDrafts[instance];const prior=last.get('names');if(prior)last.set('names',{...prior,rows:[...prior.rows.filter(r=>r.instance!==result.instance),{instance:result.instance,name:result.name,at:result.at}]});return `Seat name set to ${result.name}.`;});}
+ else if(e.target.dataset.assistantStop!==undefined)await assistantPost('/api/assistant-stop',{instance},instance,result=>Array.isArray(result?.stopped)?`Stopped ${plural(result.stopped.length,'run')}.`:unknownAssistant);
+ });
async function settingsView(get) {
const s = await get.api('settings'), n = s.notifier || {};
- let usage;
+ 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 */ }
let limitRows = [], limitsRefused = false;
try { limitRows = await get('limits'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; limitsRefused = true; }
const notify = typeof n.binding === 'string'
? `- Binding
${txt(n.binding)}
The binding name only. Its channels, server and user ids stay in the binding file.
`
: empty('No notifier binding to show.', `${n.refused ? `${txt(n.refused)}: ` : ''}${txt(n.message || 'the notifier settings could not be read')}`);
- return `${head('Settings', `${readLine()} Agent limits and appearance.`)}${appearance()}${launchForm(s.launch)}${tokenLimits(limitRows, limitsRefused)}${tokenUsage(usage)}
+ return `${head('Settings', `${readLine()} Agent limits and appearance.`)}${appearance()}${launchForm(s.launch)}${assistantSection(s.launch, names)}${tokenLimits(limitRows, limitsRefused)}${tokenUsage(usage)}
About
- Address
${txt(s.address) || 'unknown'}- Release
- ${s.release ? `
${txt(s.release)}` : 'unknown: no RELEASE file was read'} - Business
- ${txt(s.business) || none}
- Board
${txt(s.board) || 'unknown'}
`;
}