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,'
`;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?``:'';return`
`).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)}})