const $ = s => document.querySelector(s); let config={providers:[]},memories=[],conversations=[]; const state={messages:[],chatId:null,attachments:[],selectedProviderId:'',selectedModel:'',modelsByProvider:{}}; let editingProviderId=''; const codePackages=new Map();let packageSeq=0;let sourceMessageCount=0;const modelRefreshInFlight=new Set(); async function api(path,opt={}){const r=await fetch(path,opt);const t=await r.text();let j;try{j=JSON.parse(t)}catch{j={error:t}}if(!r.ok)throw new Error(j.error||t);return j} function normalizeBaseURL(v){let x=String(v||'').trim().replace(/\/+$/,'');return x.replace(/\/(chat\/completions|completions|models)$/i,'')} function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))} function editableProviders(){return (config.providers||[]).filter(p=>p&&String(p.id||'').trim()&&String(p.name||'').trim())} function validProviders(){return editableProviders().filter(p=>String(p.baseUrl||'').trim())} function editingProvider(){const id=editingProviderId;return editableProviders().find(p=>p.id===id)||current()} function current(){const id=state.selectedProviderId||$('#provider')?.value||editingProviderId;return validProviders().find(p=>p.id===id)||validProviders()[0]||null} function currentModel(){return state.selectedModel||$('#modelSelect')?.value||current()?.model||''} function renderProviderSelects(selectedId){ const ps=validProviders(); const ep=editableProviders(); const wanted=selectedId||state.selectedProviderId||ps[0]?.id||''; const opts=ps.map(p=>``).join('')||''; const eopts=ep.map(p=>``).join('')||''; $('#provider').innerHTML=opts; $('#settingsProviderSelect').innerHTML=eopts; if(wanted&&ps.some(p=>p.id===wanted)){state.selectedProviderId=wanted;$('#provider').value=wanted} const editWanted=editingProviderId||wanted||ep[0]?.id||''; if(editWanted&&ep.some(p=>p.id===editWanted)){editingProviderId=editWanted;$('#settingsProviderSelect').value=editWanted;loadProviderForm(ep.find(p=>p.id===editWanted))} const p=current();if(p){if(!state.selectedProviderId)state.selectedProviderId=p.id;renderModelSelect(p);updateTopMeta(p)} } function updateTopMeta(){/* provider/model are shown only in the composer now */} function providerDraft(){const p=editingProvider()||{};return{id:editingProviderId||p.id||'',name:$('#pName').value.trim(),baseUrl:normalizeBaseURL($('#pBase').value),model:$('#pModel').value.trim(),apiKey:$('#pKey').value.trim(),maxNewTok:Number($('#pMax').value)||4096,models:Array.isArray(p.models)?p.models:[]}} function loadProviderForm(p){if(!p)return;$('#pName').value=p.name||'';$('#pBase').value=p.baseUrl||'';$('#pModel').value=p.model||'';$('#pMax').value=p.maxNewTok||4096;$('#pKey').value='';$('#pKey').placeholder=p.hasApiKey?'Saved locally — leave blank to keep it':'Enter API key';$('#keyStatus').textContent=p.hasApiKey?'API key saved locally. Leave the field blank to keep it.':'No API key saved yet.';$('#modelList').value=(p.models||[]).join(', ');} function renderModelSelect(p){const models=[...(p?.models||[])];if(p?.model&&!models.includes(p.model))models.unshift(p.model);const select=$('#modelSelect');if(!select)return;select.innerHTML=models.map(m=>``).join('')||'';const target=state.selectedModel&&models.includes(state.selectedModel)?state.selectedModel:(p?.model&&models.includes(p.model)?p.model:models[0]||'');if(target){state.selectedModel=target;select.value=target}else{state.selectedModel='';select.value=''}updateTopMeta(p)} function setProvider(id,opts={}){if(!id)return;const p=editableProviders().find(x=>x.id===id);if(!p)return;state.selectedProviderId=id;state.selectedModel=opts.model||p.model||'';editingProviderId=id;$('#provider').value=id;$('#settingsProviderSelect').value=id;loadProviderForm(p);renderModelSelect(p);updateTopMeta(p);if(opts.refresh!==false)refreshProviderModels(id,true)} function jsonModelIds(body){let data=body?.data??body?.models??body; if(!Array.isArray(data))return[];return data.map(x=>typeof x==='string'?x:x?.id).filter(Boolean).map(String)} async function saveConfigList(providers){await api('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({providers})});config=await api('/api/config')} async function refreshProviderModels(id,silent=false){const p=editableProviders().find(x=>x.id===id);if(!p||!String(p.baseUrl||'').trim()||modelRefreshInFlight.has(id))return;modelRefreshInFlight.add(id);if(!silent)$('#testResult').textContent='Discovering allowed models…';try{const draft={id:p.id,name:p.name,baseUrl:p.baseUrl,model:p.model,apiKey:p.hasApiKey?'__KEEP_SAVED__':'',maxNewTok:p.maxNewTok};const r=await api('/api/models-draft',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(draft)});const ids=[...new Set(jsonModelIds(r).filter(Boolean))];if(ids.length){p.models=ids;let desired=p.model;if(!desired||!ids.includes(desired))desired=ids[0];if(desired!==p.model){p.model=desired;await saveConfigList(config.providers)}else{await saveConfigList(config.providers)}config=await api('/api/config');if(state.selectedProviderId===id){state.selectedModel=desired;renderProviderSelects(id);renderModelSelect(editableProviders().find(x=>x.id===id)||p)}$('#modelList').value=ids.join(', ');if(!silent)$('#testResult').textContent=`${ids.length} allowed model${ids.length===1?'':'s'} discovered.`}else if(!silent)$('#testResult').textContent='Provider returned no allowed model IDs.'}catch(e){if(!silent)$('#testResult').textContent='Model discovery failed: '+e.message}finally{modelRefreshInFlight.delete(id)}} function languageFilename(lang){return({python:'main.py',py:'main.py',javascript:'main.js',js:'main.js',typescript:'main.ts',ts:'main.ts',tsx:'main.tsx',jsx:'main.jsx',go:'main.go',golang:'main.go',rust:'main.rs',java:'Main.java',c:'main.c',cpp:'main.cpp',html:'index.html',css:'styles.css',json:'data.json',sql:'query.sql',bash:'script.sh',shell:'script.sh',sh:'script.sh'})[String(lang||'').toLowerCase()]||'code.txt'} function inferLanguageFromCode(code,hint=''){const h=String(hint||'').toLowerCase();if(h&&!['text','txt','plain','plaintext','code'].includes(h))return h;const c=String(code||'');if(/(^|\n)\s*(from\s+\S+\s+import|import\s+\S+|def\s+\w+\s*\(|class\s+\w+\s*[:(]|if\s+__name__\s*==|print\s*\()/m.test(c))return'python';if(/(^|\n)\s*(const|let|var)\s+\w+\s*=|=>|console\.log\s*\(|require\s*\(/m.test(c))return'javascript';if(/(^|\n)\s*package\s+main\b|func\s+\w+\s*\(|fmt\.Print/m.test(c))return'go';if(/(^|\n)\s*<(!doctype|html|head|body)\b/i.test(c))return'html';if(/(^|\n)\s*(SELECT|INSERT\s+INTO|CREATE\s+TABLE)\b/i.test(c))return'sql';if(/(^|\n)\s*#!/.test(c)&&/python/.test(c.slice(0,120).toLowerCase()))return'python';return h||'text'} function parseFenceInfo(info,lang,code=''){const raw=String(info||'');const fm=raw.match(/(?:filename|file)\s*=\s*([^\s]+)/i);if(fm)return fm[1].replace(/^['"]|['"]$/g,'');return languageFilename(inferLanguageFromCode(code,lang))} function uniqueArtifactName(base,index,lang){let n=base||languageFilename(lang);if(index>0&&/^main\.(py|js|ts|go|rs|java|c|cpp|sh|html|css)$/.test(n)){const ext=n.slice(n.lastIndexOf('.'));n=`module-${index+1}${ext}`}return n} function extractCodeFiles(markdown){const re=/```([^\n`]*)\n?([\s\S]*?)```/g;const files=[];let m,i=0;while((m=re.exec(markdown||''))){const info=(m[1]||'').trim(),rawLang=(info.split(/\s+/)[0]||'text').toLowerCase(),code=m[2]||'',lang=inferLanguageFromCode(code,rawLang),name=uniqueArtifactName(parseFenceInfo(info,rawLang,code),i,lang);files.push({name,content:code.replace(/^\n/,'').replace(/\n$/,''),language:lang});i++}return files} function md(s){let x=esc(s);const stash=[];x=x.replace(/```([^\n`]*)\n?([\s\S]*?)```/g,(_,info,code)=>{const rawLang=(String(info||'').trim().split(/\s+/)[0]||'text').toLowerCase(),lang=inferLanguageFromCode(code,rawLang),filename=parseFenceInfo(info,rawLang,code),id=stash.length;stash.push({lang,code,filename});return`@@CODE${id}@@`});x=x.replace(/^### (.*)$/gm,'

$1

').replace(/^## (.*)$/gm,'

$1

').replace(/^# (.*)$/gm,'

$1

').replace(/\*\*(.*?)\*\*/g,'$1').replace(/`([^`]+)`/g,'$1').replace(/^[-*] (.*)$/gm,'
  • $1
  • ').replace(/(
  • .*<\/li>\n?)+/gs,m=>``).replace(/\n\n+/g,'

    ').replace(/\n/g,'
    ');x='

    '+x+'

    ';x=x.replace(/

    (@@CODE\d+@@)<\/p>/g,'$1');stash.forEach((c,i)=>{const code64=encodeURIComponent(c.code),name64=encodeURIComponent(c.filename);x=x.replace(`@@CODE${i}@@`,`

    ${esc(c.filename)} · ${esc(c.lang)}
    ${esc(c.code)}
    `)});return x} function bindCodeActions(root){root?.querySelectorAll('[data-copy]').forEach(b=>b.onclick=async()=>{try{await navigator.clipboard.writeText(decodeURIComponent(b.dataset.copy));const old=b.textContent;b.textContent='Copied';setTimeout(()=>b.textContent=old,900)}catch{}});root?.querySelectorAll('[data-run]').forEach(b=>b.onclick=()=>openRun(decodeURIComponent(b.dataset.run),b.dataset.lang));root?.querySelectorAll('[data-save-code]').forEach(b=>b.onclick=()=>downloadCodeFiles([{name:decodeURIComponent(b.dataset.saveName),content:decodeURIComponent(b.dataset.saveCode)}]));root?.querySelectorAll('[data-package]').forEach(b=>b.onclick=()=>downloadCodePackage(b.dataset.package))} async function downloadCodeFiles(files){if(!files.length)return;try{const r=await api('/api/artifacts',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({files})});const resp=await fetch(r.url);if(!resp.ok)throw new Error(await resp.text());const blob=await resp.blob(),u=URL.createObjectURL(blob),a=document.createElement('a');a.href=u;a.download=r.downloadName||((r.files&&r.files.length>1)?'AgentDesk-code.zip':(r.files?.[0]||'code.txt'));document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(u),15000)}catch(e){alert('Download failed: '+e.message)}} async function downloadCodePackage(id){const files=codePackages.get(id);if(files)await downloadCodeFiles(files)} function attachPackageButton(root,markdown){root?.querySelectorAll(':scope > .codePackageBar').forEach(x=>x.remove());const files=extractCodeFiles(markdown);if(!files.length)return;const id='pkg-'+(++packageSeq);codePackages.set(id,files);const bar=document.createElement('div');bar.className='codePackageBar';bar.innerHTML=``;root.appendChild(bar);bindCodeActions(bar)} function renderMarketCard(data){ if(!data||!data.symbol)return; const wrap=document.createElement('div');wrap.className='marketCard'; const price=Number(data.price)||0, change=Number(data.change)||0, pct=Number(data.changePercent)||0; const positive=change>=0; const pts=Array.isArray(data.points)?data.points.filter(x=>Number.isFinite(Number(x.p))):[]; wrap.innerHTML=`
    ${esc(data.name||data.symbol)} (${esc(data.symbol)})
    ${esc(data.exchange||'')} · ${esc(data.currency||'USD')}
    Yahoo Finance
    $${price.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}
    ${positive?'+':''}$${Math.abs(change).toFixed(2)} (${positive?'+':''}${pct.toFixed(2)}%)
    ${esc((data.marketState||'').replace(/_/g,' '))}
    1D5D1M6MYTD1Y5YMAX
    Open${pts.length?('$'+Number(pts[0].p).toFixed(2)):'—'}
    Day Low${Number(data.dayLow)?('$'+Number(data.dayLow).toFixed(2)):'—'}
    Day High${Number(data.dayHigh)?('$'+Number(data.dayHigh).toFixed(2)):'—'}
    Volume${Number(data.volume)?Number(data.volume).toLocaleString():'—'}
    52-wk Low${Number(data.weekLow)?('$'+Number(data.weekLow).toFixed(2)):'—'}
    52-wk High${Number(data.weekHigh)?('$'+Number(data.weekHigh).toFixed(2)):'—'}
    `; $('#messages').appendChild(wrap);const canvas=wrap.querySelector('canvas'),ctx=canvas.getContext('2d'); const w=canvas.width,h=canvas.height;ctx.clearRect(0,0,w,h);ctx.strokeStyle='#2a2e34';ctx.lineWidth=1; for(let i=1;i<5;i++){const y=(h*i)/5;ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(w,y);ctx.stroke()} if(pts.length>1){let minP=Math.min(...pts.map(x=>Number(x.p))),maxP=Math.max(...pts.map(x=>Number(x.p)));if(maxP===minP){maxP+=1;minP-=1} const padX=8,padY=12;const grad=ctx.createLinearGradient(0,0,0,h);grad.addColorStop(0,positive?'rgba(36,208,123,.28)':'rgba(255,66,88,.28)');grad.addColorStop(1,'rgba(255,66,88,0.02)'); const xy=pt=>({x:padX+(pts.indexOf(pt)/(pts.length-1))*(w-padX*2),y:padY+(1-(Number(pt.p)-minP)/(maxP-minP))*(h-padY*2)}); ctx.beginPath();pts.forEach((pt,i)=>{const x=padX+(i/(pts.length-1))*(w-padX*2),y=padY+(1-(Number(pt.p)-minP)/(maxP-minP))*(h-padY*2);i?ctx.lineTo(x,y):ctx.moveTo(x,y)});ctx.lineTo(w-padX,h-padY);ctx.lineTo(padX,h-padY);ctx.closePath();ctx.fillStyle=grad;ctx.fill(); ctx.beginPath();pts.forEach((pt,i)=>{const x=padX+(i/(pts.length-1))*(w-padX*2),y=padY+(1-(Number(pt.p)-minP)/(maxP-minP))*(h-padY*2);i?ctx.lineTo(x,y):ctx.moveTo(x,y)});ctx.strokeStyle=positive?'#2fd77b':'#ff465c';ctx.lineWidth=2.2;ctx.stroke(); ctx.fillStyle='#8e959d';ctx.font='10px ui-sans-serif';ctx.fillText('$'+maxP.toFixed(2),w-48,12);ctx.fillText('$'+minP.toFixed(2),w-48,h-4); } $('#messages').scrollTop=$('#messages').scrollHeight; } function addThinkingMessage(){const m=$('#messages');$('.welcome')?.remove();const d=document.createElement('div');d.className='msg thinkingMsg';d.innerHTML='
    AgentDesk
    Thinking
    ';m.appendChild(d);m.scrollTop=m.scrollHeight;return d}function hideThinking(el){if(el&&el.isConnected)el.remove()} function renderUserAttachments(list=[]){return (list||[]).map(a=>{const kind=a.kind||'file',name=esc(a.name||'attachment'),size=a.size?formatBytes(a.size):'',type=esc(String(a.mime||a.extension||kind)),thumb=kind==='image'&&a.dataURL?'Attached image':'';return '
    '+thumb+'
    '+name+''+type+(size?' · '+size:'')+''+(a.path?''+esc(a.path)+'':'')+'
    '}).join('')} function formatBytes(n){n=Number(n)||0;if(n<1024)return n+' B';if(n<1048576)return (n/1024).toFixed(1)+' KB';return (n/1048576).toFixed(1)+' MB'} function addMsg(role,text,cls='',attachments=[]){const m=$('#messages');$('.welcome')?.remove();const d=document.createElement('div');d.className='msg '+cls+(role==='You'?' userMessage':'');const attachHtml=role==='You'&&attachments.length?'
    '+renderUserAttachments(attachments)+'
    ':'';d.innerHTML=`
    ${role==='You'?'●':'✦'}${esc(role)}
    ${attachHtml}
    ${role==='AgentDesk'?md(text):esc(text).replace(/\n/g,'
    ')}
    `;m.appendChild(d);m.scrollTop=m.scrollHeight;if(role==='AgentDesk'){bindCodeActions(d);attachPackageButton(d,typeof text==='string'?text:'')}if(!$('#chatSources').classList.contains('hidden')){sourceMessageCount++;if(sourceMessageCount>=5)clearChatSources()}return d.querySelector('.bubble')} function beginActivity(){const e=$('#toolActivity');e.classList.remove('hidden');e.innerHTML='
    Working…
    ';return e} function addTool(text){const e=$('#toolActivity');if(e.classList.contains('hidden'))beginActivity();const d=document.createElement('div');d.className='activityItem';d.textContent='• '+text;e.appendChild(d);$('#messages').scrollTop=$('#messages').scrollHeight} function hideActivity(){const e=$('#toolActivity');if(e){e.classList.add('hidden');e.innerHTML=''}} function clearChatSources(){const r=$('#chatSources');r.classList.add('hidden');$('#chatSourceCount').textContent='0';$('#chatSourceList').innerHTML='';sourceMessageCount=0} function addChatSources(list){const usable=(list||[]).filter(s=>String(s?.url||'').trim());if(!usable.length)return;const r=$('#chatSources');const wasHidden=r.classList.contains('hidden');if(wasHidden){sourceMessageCount=0;r.classList.remove('hidden')}const existing=new Set([...r.querySelectorAll('[data-source-url]')].map(x=>x.dataset.sourceUrl));for(const s of usable){const u=String(s.url).trim();if(existing.has(u))continue;existing.add(u);const d=document.createElement('div');d.className='railSource';d.dataset.sourceUrl=u;d.innerHTML=`${esc(s.title||u)}
    ${esc(u)}
    `;$('#chatSourceList').appendChild(d)}$('#chatSourceCount').textContent=String(existing.size);sourceMessageCount=0} function autoGrowPrompt(){const p=$('#prompt');if(!p)return;p.style.height='auto';const h=Math.min(Math.max(p.scrollHeight,54),220);p.style.height=h+'px';p.style.overflowY=p.scrollHeight>220?'auto':'hidden'} function resetChat(){state.messages=[];state.chatId=null;state.attachments=[];renderAttachments();clearChatSources();hideActivity();$('#messages').innerHTML='
    ✦

    What are we building?

    Ask questions, write code, search the web, or use your saved memory.

    ';bindChips();renderChats();$('#prompt').value='';autoGrowPrompt()} function renderAttachments(){const el=$('#attachmentTray');el.innerHTML=state.attachments.map((a,i)=>{const v=a.kind==='image'&&a.dataURL?`image preview`:'';return`
    ${v}
    ${a.kind==='image'?'🖼️':'📎'} ${esc(a.name)}${a.kind==='image'?'
    image bytes ready for vision input
    ':''}
    `}).join('');el.querySelectorAll('[data-del]').forEach(b=>b.onclick=()=>{state.attachments.splice(Number(b.dataset.del),1);renderAttachments()})} async function ensureChat(title){if(state.chatId)return;state.chatId=String(Date.now());const c={id:state.chatId,title:(title||'New Chat').slice(0,55),folder:'General',updated:new Date().toISOString(),messages:[]};conversations.unshift(c);await saveConversation(c);renderChats()} async function saveConversation(c){await api('/api/conversations',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(c)})} async function getBrowserLocation(){if(!navigator.geolocation)return null;return await new Promise(resolve=>navigator.geolocation.getCurrentPosition(pos=>resolve({latitude:pos.coords.latitude,longitude:pos.coords.longitude}),()=>resolve(null),{enableHighAccuracy:false,timeout:5000,maximumAge:10*60*1000}))} function webish(t){const x=String(t||'').toLowerCase();return ['search the web','search online','browse the web','look this up','look it up','research ','latest news','current news','current price','according to','what models does','cheapest platform','to this date','as of today','right now'].some(v=>x.includes(v))} function weatherish(t){return /\b(weather|forecast|temperature|rain|snow|humidity|wind)\b/i.test(t)} async function startResearchJob(query,p,geoLocation){const start=await api('/api/research/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({provider:p.id,model:currentModel(),query,maxNewTok:Number(p.maxNewTok)||8192,location:geoLocation||null})});return pollResearchJob(start.id)} async function pollResearchJob(id,onProgress){let seen=0;for(;;){const j=await api('/api/research/status?id='+encodeURIComponent(id));const items=Array.isArray(j.progress)?j.progress:[];for(let i=seen;isetTimeout(r,700))}} async function send(){ const promptEl=$('#prompt'); const text=promptEl.value.trim(); if(!text&&!state.attachments.length)return; const p=current(); if(!p){alert('Add a provider first.');return} const attachments=state.attachments.slice(); const content=[]; if(text)content.push({type:'text',text}); for(const a of attachments){ const meta=`[Attached file: ${a.name}]\nPath: ${a.path||'(local upload path unavailable)'}\nMIME: ${a.mime||'unknown'}\nSize: ${formatBytes(a.size||0)}`; if(a.kind==='image'){ content.push({type:'text',text:meta+'\nAnalyze actual image bytes when vision is supported; never infer contents from the filename.'}); if(a.uploadId)content.push({type:'image_ref',image_ref:{id:a.uploadId,mime:a.mime,name:a.name,path:a.path||'',size:a.size||0}}); }else{ content.push({type:'text',text:meta+'\n'+(a.text||'(No text was extracted automatically. Use file_read or Terminal on the exact path.)')}); } content.push({type:'file_ref',file_ref:{id:a.uploadId||'',name:a.name,mime:a.mime||'',path:a.path||'',size:a.size||0,kind:a.kind||'file'}}); } promptEl.value=''; autoGrowPrompt(); state.attachments=[]; renderAttachments(); clearChatSources(); beginActivity(); await ensureChat(text||attachments.map(a=>a.name).join(', ')||'New Chat'); addMsg('You',text||attachments.map(a=>a.name).join(', '),'',attachments); state.messages.push({role:'user',content}); try{ if($('#terminalMode').checked&&!$('#terminalMode').dataset.confirmed){ const ok=confirm('Allow AgentDesk to run shell commands on this computer for this chat?'); if(!ok)$('#terminalMode').checked=false;else $('#terminalMode').dataset.confirmed='1'; } const geo=weatherish(text)?await getBrowserLocation():null; const research=$('#researchMode').checked; const agent=$('#agentMode').checked||research||webish(text)||weatherish(text); const r=await fetch('/api/chat/stream',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({provider:p.id,model:currentModel(),messages:state.messages,maxNewTok:Number(p.maxNewTok)||4096,agent,research,allowTerminal:$('#terminalMode').checked,location:geo}) }); if(!r.ok)throw new Error(await r.text()); const reader=r.body.getReader(); const dec=new TextDecoder(); let buf='',out='',bubble=null; const thinking=addThinkingMessage(); for(;;){ const {value,done}=await reader.read(); if(done)break; buf+=dec.decode(value,{stream:true}); const chunks=buf.split('\n\n'); buf=chunks.pop()||''; for(const part of chunks){ const line=part.split('\n').find(x=>x.startsWith('data: ')); if(!line)continue; const ev=JSON.parse(line.slice(6)); if(ev.type==='tool')addTool(ev.text); else if(ev.type==='market')renderMarketCard(ev.data); else if(ev.type==='sources')addChatSources(ev.sources||[]); else if(ev.type==='notice')addTool(ev.text||'Notice'); else if(ev.type==='chunk'){ hideThinking(thinking); out+=ev.text; if(!bubble)bubble=addMsg('AgentDesk',''); bubble.innerHTML=md(out); bindCodeActions(bubble.parentElement); attachPackageButton(bubble.parentElement,out); $('#messages').scrollTop=$('#messages').scrollHeight; }else if(ev.type==='error'){ hideThinking(thinking); throw new Error(ev.error); } } } if(!out&&!bubble){hideThinking(thinking);throw new Error('Model returned no text');} hideThinking(thinking); hideActivity(); state.messages.push({role:'assistant',content:out}); await persistChat(); renderChats(); }catch(e){ document.querySelector('.thinkingMsg')?.remove(); hideActivity(); addMsg('AgentDesk','Error: '+e.message); state.messages.push({role:'assistant',content:'Error: '+e.message}); await persistChat(); } } async function persistChat(){const c=conversations.find(x=>x.id===state.chatId);if(!c)return;c.messages=state.messages;c.updated=new Date().toISOString();await saveConversation(c)} function attachmentRefs(c){if(!Array.isArray(c))return[];return c.filter(x=>x?.type==='file_ref'||x?.type==='image_ref').map(x=>x.type==='image_ref'?{kind:'image',name:x.image_ref?.name,mime:x.image_ref?.mime,path:x.image_ref?.path,size:x.image_ref?.size||0}:{kind:x.file_ref?.kind||'file',name:x.file_ref?.name,mime:x.file_ref?.mime,path:x.file_ref?.path,size:x.file_ref?.size||0})} function displayMessageContent(c){if(typeof c==='string')return c;if(Array.isArray(c)){const txt=c.filter(x=>x?.type==='text').map(x=>x.text||'').join(' ').replace(/\[Attached file:[\s\S]*?\nSize:[^\n]*\n/gi,'').replace(/\[Image attached:[^\n]*\][^\n]*\n?/gi,'').trim();return txt||'[Attachment]'}return String(c??'')} async function loadChat(id){ if(!id)return; try{ const c=await api('/api/conversations?id='+encodeURIComponent(id)); state.chatId=c.id; state.messages=Array.isArray(c.messages)?c.messages:[]; state.attachments=[]; renderAttachments(); clearChatSources(); hideActivity(); const el=$('#messages'); el.innerHTML=''; if(!state.messages.length){ el.innerHTML='
    ✦

    Empty chat

    This chat has no saved messages yet.

    '; }else{ for(const m of state.messages){ const role=m?.role==='user'?'You':'AgentDesk'; const refs=role==='You'?attachmentRefs(m.content):[]; const d=document.createElement('div'); d.className='msg '+(role==='You'?'userMessage':''); d.innerHTML=`
    ${role==='You'?'●':'✦'}${role}
    ${refs.length?'
    '+renderUserAttachments(refs)+'
    ':''}
    ${role==='AgentDesk'?md(displayMessageContent(m.content)):esc(displayMessageContent(m.content)).replace(/\n/g,'
    ')}
    `; el.appendChild(d); if(role==='AgentDesk'){bindCodeActions(d);attachPackageButton(d,displayMessageContent(m.content))} } el.scrollTop=el.scrollHeight; } renderChats(); }catch(e){addMsg('AgentDesk','Could not load that chat: '+e.message)} } function renderChats(){const el=$('#chatList');el.innerHTML=conversations.slice(0,80).map(c=>`
    `).join('');el.querySelectorAll('[data-open]').forEach(b=>b.onclick=e=>{e.stopPropagation();loadChat(b.dataset.open)});el.querySelectorAll('[data-rename]').forEach(b=>b.onclick=async e=>{e.stopPropagation();const c=conversations.find(x=>x.id===b.dataset.rename);if(!c)return;const t=prompt('Rename chat:',c.title||'New Chat');if(t===null)return;const title=t.trim();if(!title)return;c.title=title;c.updated=new Date().toISOString();try{await saveConversation(c);renderChats()}catch(err){alert('Rename failed: '+err.message)}});el.querySelectorAll('[data-delete]').forEach(b=>b.onclick=async e=>{e.stopPropagation();if(!confirm('Delete this chat?'))return;try{await api('/api/conversations?id='+encodeURIComponent(b.dataset.delete),{method:'DELETE'});conversations=conversations.filter(c=>c.id!==b.dataset.delete);if(state.chatId===b.dataset.delete)resetChat();renderChats()}catch(err){alert('Delete failed: '+err.message)}})} async function search(){const q=$('#searchInput').value.trim();if(!q)return;$('#searchResults').innerHTML='

    Searching and reading top results…

    ';try{const r=await api('/api/search?q='+encodeURIComponent(q));$('#searchResults').innerHTML=(r.results||[]).map((x,i)=>`
    ${esc(x.title||x.url)}${x.content?``:''}

    ${esc(x.url)}

    ${esc(x.snippet||'')}
    ${x.content?`
    Page content retrieved by AgentDesk
    ${esc(x.content)}
    ${Array.isArray(x.links)&&x.links.length?``:''}
    `:''}
    `).join('')||'

    No results found.

    '}catch(e){$('#searchResults').textContent='Search error: '+e.message}} async function deepResearch(){const q=$('#researchInput').value.trim();if(!q)return;const p=current();if(!p){$('#researchStatus').textContent='Add a provider first.';return}$('#researchStatus').textContent='Starting…';$('#researchOutput').innerHTML='';$('#researchSources').innerHTML='';$('#sourceCount').textContent='0';try{const geo=await getBrowserLocation();const r=await startResearchJob(q,p,geo);formatResearchOutput(r.content||'');renderResearchSources(r.sources||[]);$('#researchStatus').textContent='Research complete.'}catch(e){$('#researchStatus').textContent='Error: '+e.message}} function formatResearchOutput(s){const el=$('#researchOutput');el.innerHTML=md(s);bindCodeActions(el);attachPackageButton(el,s)} function renderResearchSources(list){$('#sourceCount').textContent=String(list.length);$('#researchSources').innerHTML=(list||[]).map(x=>``).join('')} async function uploadFiles(files){ for(const file of Array.from(files||[])){ const fd=new FormData(); fd.append('file',file,file.name||('pasted-image-'+Date.now()+'.png')); try{ const r=await api('/api/files',{method:'POST',body:fd}); state.attachments.push({ id:r.id||'',uploadId:r.id||'',name:r.name||file.name,text:r.text||'', kind:r.kind||'file',mime:r.mime||file.type||'',dataURL:r.dataURL||'', path:r.path||'',size:Number(r.size)||file.size||0,extension:r.extension||'' }); renderAttachments(); addTool('Attached: '+(r.name||file.name)+' · '+formatBytes(r.size||file.size||0)+(r.text?' · contents extracted':' · metadata only')); if(r.kind==='image')addTool('Image attached. Vision support will be checked when the message is sent.'); }catch(e){addTool('File error: '+e.message)} } } function show(v){const target=$('#'+v+'View');if(!target)return;document.querySelectorAll('.view').forEach(x=>x.classList.add('hidden'));target.classList.remove('hidden');document.querySelectorAll('.nav').forEach(n=>n.classList.toggle('active',n.dataset.view===v));$('#viewTitle').textContent=v==='settings'?'Providers':v==='research'?'Deep Research':v[0].toUpperCase()+v.slice(1);if(v==='settings'&¤t()){loadProviderForm(current());renderProviderCards()}if(v==='terminal')loadTerminalInfo()} function bindChips(){document.querySelectorAll('[data-fill]').forEach(b=>b.onclick=()=>{$('#prompt').value=b.dataset.fill;autoGrowPrompt();$('#prompt').focus()})} function renderMem(){const list=$('#memList');list.innerHTML=memories.map(m=>`
    ${esc(m.text)}
    ${esc(m.kind)} · ${new Date(m.created).toLocaleString()}
    `).join('');list.querySelectorAll('[data-id]').forEach(b=>b.onclick=async()=>{await api('/api/memory?id='+encodeURIComponent(b.dataset.id),{method:'DELETE'});memories=await api('/api/memory');renderMem()})} const MEMORY_EXPORT_PROMPT=`Create a portable memory export about me from your saved memory/context.\n\nRules:\n- Do not include API keys, passwords, private credentials, security codes, or other secrets.\n- Include useful long-term preferences, projects, facts, recurring instructions, and context that would help another assistant respond consistently.\n- Do not include hidden chain-of-thought or private internal reasoning.\n- Return ONLY valid JSON in this exact shape:\n{"memories":[{"kind":"preference","text":"..."},{"kind":"project","text":"..."},{"kind":"fact","text":"..."}]}\n- Keep each memory concise and factual.` async function importMemory(){const t=$('#memoryImportInput').value.trim();if(!t)return;try{const r=await api('/api/memory/import',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:t})});$('#memoryImportStatus').textContent=`Imported ${r.added} memory item${r.added===1?'':'s'} successfully.`;$('#memoryImportInput').value='';memories=await api('/api/memory');renderMem()}catch(e){$('#memoryImportStatus').textContent='Import failed: '+e.message}} async function saveProvider(){ const id=editingProviderId||current()?.id||('provider-'+Date.now()); const existing=config.providers.find(x=>x.id===id); const d=providerDraft(); d.id=id; d.models=Array.isArray(existing?.models)?existing.models:(Array.isArray(d.models)?d.models:[]); if(!d.name||!d.baseUrl){$('#testResult').textContent='Name and Base URL are required.';return} if(existing?.hasApiKey && !d.apiKey)d.apiKey='__KEEP_SAVED__'; const list=config.providers.some(x=>x.id===id)?config.providers.map(x=>x.id===id?{...x,...d}:x):[...config.providers,d]; $('#providerSaveState').textContent='Saving…'; try{ await saveConfigList(list); editingProviderId=id;state.selectedProviderId=id;state.selectedModel=d.model||''; renderProviderSelects(id); const saved=editableProviders().find(x=>x.id===id); if(saved)loadProviderForm(saved); $('#providerSaveState').textContent='Saved.'; $('#testResult').textContent=d.model?'Provider saved. Discovering allowed models in the background…':'Provider saved. Discovering models in the background…'; await refreshProviderModels(id,false); }catch(e){$('#providerSaveState').textContent='';$('#testResult').textContent='Save failed: '+e.message} } function newProvider(){ const id='provider-'+Date.now(); const p={id,name:'New Provider',baseUrl:'',model:'',maxNewTok:4096,models:[],hasApiKey:false}; config.providers=[...(config.providers||[]),p]; editingProviderId=id;state.selectedProviderId='';state.selectedModel=''; renderProviderSelects(); $('#settingsProviderSelect').value=id;loadProviderForm(p);show('settings');$('#pName').focus();$('#testResult').textContent='Fill in the provider name, Base URL, and API key if required, then Save Provider.'; } async function deleteProvider(id){if(validProviders().length<=1){alert('AgentDesk needs at least one usable provider.');return}if(!confirm('Delete this provider?'))return;try{await saveConfigList(config.providers.filter(p=>p.id!==id));const next=validProviders()[0];if(next)setProvider(next.id,{refresh:true});renderProviderCards();$('#testResult').textContent='Provider deleted.'}catch(e){$('#testResult').textContent='Delete failed: '+e.message}} async function testProvider(){const d=providerDraft();if(!d.apiKey&&d.id&&config.providers.some(x=>x.id===d.id&&x.hasApiKey))d.apiKey='__KEEP_SAVED__';if(!d.name||!d.baseUrl||!d.model){$('#testResult').textContent='Name, Base URL, and Model are required to test.';return;}$('#testResult').textContent='Testing…';try{const r=await api('/api/test-provider',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({...d,model:currentModel()||d.model})});$('#testResult').textContent=(r.content||'Connected.')+' — connection successful.';refreshProviderModels(d.id,true)}catch(e){$('#testResult').textContent='Failed: '+e.message}} async function discoverModels(){const id=editingProviderId||current()?.id;if(id)await refreshProviderModels(id,false)} function renderProviderCards(){const el=$('#providerCards');el.innerHTML=validProviders().map(p=>`
    ${esc(p.name)}
    ${esc(p.baseUrl||'No Base URL')}
    ${esc((p.models||[]).length)} discovered model${(p.models||[]).length===1?'':'s'} · ${p.hasApiKey?'API key saved':'No API key'}
    `).join('');el.querySelectorAll('[data-edit-provider]').forEach(b=>b.onclick=()=>setProvider(b.dataset.editProvider,{refresh:true}));el.querySelectorAll('[data-refresh-provider]').forEach(b=>b.onclick=()=>refreshProviderModels(b.dataset.refreshProvider,false));el.querySelectorAll('[data-delete-provider]').forEach(b=>b.onclick=()=>deleteProvider(b.dataset.deleteProvider))} function openRun(code,lang='python'){$('#runModal').classList.remove('hidden');$('#runLang').value=['python','javascript','go'].includes(lang)?lang:'python';$('#runCode').value=code;$('#runOutput').textContent=''} async function executeCode(){const lang=$('#runLang').value,code=$('#runCode').value;if(!code.trim()||!confirm('Run this code locally on your computer?'))return;$('#runOutput').textContent='Running…';try{const r=await api('/api/run-code',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({language:lang,code})});$('#runOutput').textContent=r.output||'(no output)'}catch(e){$('#runOutput').textContent='Error: '+e.message}} async function loadTerminalInfo(){try{const r=await api('/api/terminal-info');$('#terminalInfo').textContent=r.display||r.shell||r.os;$('#terminalCwd').value=r.cwd||''}catch(e){$('#terminalInfo').textContent='Terminal unavailable'}} async function runTerminal(){const command=$('#terminalCommand').value.trim();if(!command)return;const cwd=$('#terminalCwd').value.trim();$('#terminalOutput').textContent='Running…';try{const r=await api('/api/terminal-run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({command,cwd,timeoutSeconds:60})});$('#terminalOutput').textContent=(r.output||'')+(r.error?`\n\n[${r.error}]`:'')}catch(e){$('#terminalOutput').textContent='Error: '+e.message}} function setupPaste(){const p=$('#prompt');p.addEventListener('input',autoGrowPrompt);p.addEventListener('paste',async e=>{const items=Array.from(e.clipboardData?.items||[]);const images=items.filter(i=>i.kind==='file'&&/^image\//i.test(i.type)).map(i=>i.getAsFile()).filter(Boolean);if(images.length){e.preventDefault();await uploadFiles(images);autoGrowPrompt()}})} async function load(){config=await api('/api/config');config.providers=config.providers||[];memories=await api('/api/memory');conversations=await api('/api/conversations');const ps=validProviders();const first=ps[0];renderProviderSelects(first?.id||'');renderMem();renderChats();bindChips();setupPaste();if(first)setTimeout(()=>refreshProviderModels(first.id,true),100)} document.addEventListener('DOMContentLoaded',()=>{try{$('#send').onclick=send;$('#prompt').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});$('#provider').onchange=()=>setProvider($('#provider').value,{refresh:true});$('#modelSelect').onchange=()=>{state.selectedModel=$('#modelSelect').value;updateTopMeta(current())};$('#settingsProviderSelect').onchange=()=>setProvider($('#settingsProviderSelect').value,{refresh:true});$('#newChat').onclick=resetChat;document.addEventListener('click',e=>{const n=e.target.closest('.nav');if(n){e.preventDefault();show(n.dataset.view)}});$('#doSearch').onclick=search;$('#searchInput').addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();search()}});$('#doResearch').onclick=deepResearch;$('#researchInput').addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();deepResearch()}});$('#addMem').onclick=async()=>{const t=$('#memInput').value.trim();if(!t)return;try{await api('/api/memory',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:t,kind:$('#memKind').value})});$('#memInput').value='';memories=await api('/api/memory');renderMem()}catch(e){$('#memoryImportStatus').textContent='Memory save failed: '+e.message}};$('#copyMemoryPrompt').onclick=async()=>{await navigator.clipboard.writeText(MEMORY_EXPORT_PROMPT);$('#memoryImportStatus').textContent='Memory export prompt copied.'};$('#importMemory').onclick=importMemory;$('#saveProvider').onclick=saveProvider;$('#newProvider').onclick=newProvider;$('#testProvider').onclick=testProvider;$('#discoverModels').onclick=discoverModels;$('#addFile').onclick=()=>$('#fileInput').click();$('#fileInput').onchange=e=>uploadFiles(e.target.files);$('#closeRun').onclick=()=>$('#runModal').classList.add('hidden');$('#cancelRun').onclick=()=>$('#runModal').classList.add('hidden');$('#executeCode').onclick=executeCode;$('#runTerminal').onclick=runTerminal;$('#terminalUseHome').onclick=loadTerminalInfo;$('#terminalCommand').addEventListener('keydown',e=>{if(e.key==='Enter'&&e.ctrlKey){e.preventDefault();runTerminal()}});load().catch(e=>{const el=$('#testResult');if(el)el.textContent='Startup warning: '+e.message})}catch(e){console.error(e)}})