agenttwin 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -15
- package/dist/api.js +796 -68
- package/dist/cli.js +693 -290
- package/dist/dashboard/assets/dashboard/agent-home.css +405 -0
- package/dist/dashboard/assets/dashboard/agent-home.js +264 -0
- package/dist/dashboard/assets/dashboard/dashboard.js +118 -133
- package/dist/dashboard/assets/dashboard/hosted.js +56 -17
- package/dist/dashboard/assets/dashboard/moonlit-coast.png +0 -0
- package/dist/dashboard/assets/dashboard/workflows.js +39 -0
- package/dist/dashboard/assets/dashboard/workspace.css +156 -0
- package/dist/dashboard/assets/vendor/dompurify.LICENSE +202 -0
- package/dist/dashboard/assets/vendor/marked.LICENSE +44 -0
- package/dist/dashboard/assets/vendor/marked.js +82 -0
- package/dist/dashboard/assets/vendor/purify.min.js +7 -0
- package/dist/dashboard/index.html +109 -32
- package/dist/gateway.js +798 -70
- package/package.json +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
(()=>{
|
|
2
|
+
// Agent home view: night-sky hero, task composer and workspace widgets.
|
|
3
|
+
// Every widget renders recorded receipts, discovered inventory or live hosted-API
|
|
4
|
+
// jobs. Nothing on this page is synthesized: when a fact is unknown it is absent.
|
|
5
|
+
const $ = s => document.querySelector(s);
|
|
6
|
+
const esc = v => String(v ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
7
|
+
let runs = [];
|
|
8
|
+
let runsLoadError = '';
|
|
9
|
+
let hosted = { connected: false, jobs: [] };
|
|
10
|
+
|
|
11
|
+
/* ---- Helpers ---- */
|
|
12
|
+
const rel = v => {const d=Date.now()-new Date(v).getTime();if(!Number.isFinite(d)||!v)return '';const m=Math.round(d/60000);return m<1?'just now':m<60?`${m}m ago`:m<1440?`${Math.round(m/60)}h ago`:`${Math.round(m/1440)}d ago`;};
|
|
13
|
+
const dur = ms => !Number.isFinite(ms)?'':ms<1000?`${Math.round(ms)}ms`:ms<60000?`${(ms/1000).toFixed(ms%1000?1:0)}s`:`${Math.floor(ms/60000)}m ${Math.round(ms%60000/1000)}s`;
|
|
14
|
+
const title = r => r.dashboardSource?.startsWith('Prepared in dashboard')?'Dashboard-prepared workflow':r.name==='sandbox_boundaries'?'Sandbox boundary check':r.name.replaceAll('_',' ').replace(/^./,c=>c.toUpperCase());
|
|
15
|
+
const statusIco = s => s==='passed'?'<svg class="ok-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 3 3 5-6"/></svg>':s==='failed'||s==='error'?'<svg class="bad-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="m9 9 6 6M15 9l-6 6"/></svg>':'<svg class="neutral-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16v.5"/></svg>';
|
|
16
|
+
const spinIco = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin"><path d="M12 3a9 9 0 1 0 9 9" stroke-linecap="round"/></svg>';
|
|
17
|
+
const liveJobs = () => hosted.jobs.filter(j=>['queued','running','cancelling'].includes(j.status));
|
|
18
|
+
const envStore = () => {try{return JSON.parse(localStorage.getItem('agenttwin.environment-layout.v2'+($('#agenttwin-discovery')?':'+location.pathname:''))||'null');}catch{return null;}};
|
|
19
|
+
|
|
20
|
+
/* ---- Widgets ---- */
|
|
21
|
+
// One malformed receipt must not take down the whole page.
|
|
22
|
+
function renderAll(){
|
|
23
|
+
for(const render of [renderNextStep,renderProject,renderWorking,renderReceipts,renderSandbox,renderGit,renderServices,renderFailures,renderBell,renderComposerCounts]){
|
|
24
|
+
try{render();}catch(error){console.error('AgentTwin home: widget failed to render.',error);}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const receiptError = () => `<p class="w-empty">Recorded receipts could not be loaded (${esc(runsLoadError)}). Import a run.json in <button type="button" class="linklike" data-goto="history">Runs</button> or check the connection.</p>`;
|
|
28
|
+
function renderNextStep(){
|
|
29
|
+
const discovery=window.agentTwinDashboard?.getDiscovery(),connected=!!hosted.connected;
|
|
30
|
+
const step=!connected?['Connect your project','Start in your repository with npx agenttwin init, then connect your account.','Connect project','environment']:!discovery?.discovered?['Bring in your project','Run npx agenttwin sync in your repository to load its tools and workflows.','See setup steps','environment']:discovery.workflows.length?['Choose what to test',`${discovery.name} has ${discovery.workflows.length} workflow candidates. Pick one to prepare and run.`,'Choose a workflow','configure']:['Find your workflows','Analyze your repository with a coding agent, then sync its findings.','See setup steps','environment'];
|
|
31
|
+
$('#home-next-step').innerHTML=`<div><h2>${esc(step[0])}</h2><p>${esc(step[1])}</p></div><button type="button" class="primary" data-goto="${step[3]}">${esc(step[2])} →</button>`;
|
|
32
|
+
}
|
|
33
|
+
function renderProject(){
|
|
34
|
+
const project=window.agentTwinDashboard?.getDiscovery();
|
|
35
|
+
$('#project-body').innerHTML=project?.discovered?`<strong class="project-title">${esc(project.name)}</strong><div class="project-summary"><button type="button" data-goto="environment"><b>${project.tools.filter(t=>!t.declared).length}</b>Detected resources</button><button type="button" data-goto="configure"><b>${project.workflows.length}</b>Workflow candidates</button><button type="button" data-goto="history"><b>${runs.length}</b>Run results</button></div><p class="w-empty">Discovery describes code paths. Run receipts show what has actually been tested.</p>`:'<p class="w-empty">Connect and sync your project to see its detected resources and workflow candidates. <button type="button" class="linklike" data-goto="environment">Connect project</button></p>';
|
|
36
|
+
}
|
|
37
|
+
function renderWorking(){
|
|
38
|
+
const active=liveJobs();
|
|
39
|
+
$('#working-count').textContent=active.length||'';
|
|
40
|
+
$('#working-meta').textContent='';
|
|
41
|
+
const html=active.length?active.map(j=>`<button type="button" class="review-row" data-open-job="${esc(j.id)}"><span class="task-spin">${spinIco}</span><strong>Cloud run ${esc(String(j.id).slice(0,8))}</strong><span class="w-when">${esc(j.status)}</span></button>`).join(''):'<p class="w-empty">No cloud runs in progress. Test a workflow with <span class="mono">npx agenttwin run</span>, then sync its results.</p>';
|
|
42
|
+
$('#working-body').innerHTML=html;
|
|
43
|
+
}
|
|
44
|
+
function renderReceipts(){
|
|
45
|
+
const failed=runs.filter(r=>r.status==='failed'||r.status==='error');
|
|
46
|
+
const rest=runs.filter(r=>r.status==='passed');
|
|
47
|
+
const incomplete=runs.filter(r=>r.status==='incomplete');
|
|
48
|
+
$('#receipts-count').textContent=failed.length||'';
|
|
49
|
+
$('#receipts-spin').hidden=!liveJobs().length;
|
|
50
|
+
if(runsLoadError&&!runs.length){$('#receipts-body').innerHTML=receiptError();return;}
|
|
51
|
+
const row=r=>`<button type="button" class="review-row" data-open-receipt="${esc(r.runId)}"><span class="r-ico">${statusIco(r.status)}</span><strong>${esc(title(r))}</strong><span class="ref" title="${esc(window.agentTwinDashboard?.getReceiptSource(r.runId))}">${esc(String(r.runId).slice(0,8))} · ${esc(window.agentTwinDashboard?.getReceiptSource(r.runId))}</span><span class="r-status ${esc(r.status)}">${esc(r.status)}</span><span class="r-time">${esc(rel(r.createdAt))}</span></button>`;
|
|
52
|
+
let html='';
|
|
53
|
+
if(failed.length)html+=`<p class="group-label warn">Needs review · <b>${failed.length}</b> failed receipt${failed.length===1?'':'s'}</p>${failed.slice(0,2).map(row).join('')}`;
|
|
54
|
+
if(rest.length)html+=`<p class="group-label">Passed · <b>${rest.length}</b></p>${rest.slice(0,4).map(row).join('')}`;
|
|
55
|
+
if(incomplete.length)html+=`<p class="group-label">Incomplete · <b>${incomplete.length}</b></p>${incomplete.slice(0,2).map(row).join('')}`;
|
|
56
|
+
$('#receipts-body').innerHTML=html||'<p class="w-empty">Run receipts appear here once a workflow executes. <button type="button" class="linklike" data-goto="settings">Connect an account</button> or run <span class="mono">npx agenttwin run</span> from your project.</p>';
|
|
57
|
+
}
|
|
58
|
+
function renderSandbox(){
|
|
59
|
+
const r=runs[0];
|
|
60
|
+
if(!r){$('#sandbox-meta').textContent='';$('#sandbox-body').innerHTML='<p class="w-empty">No tests recorded yet. Run a workflow to see its outcome, runner and tested revision.</p>';return;}
|
|
61
|
+
$('#sandbox-meta').textContent=`latest receipt · ${rel(r.createdAt)}`;
|
|
62
|
+
const iso=r.isolation??{};
|
|
63
|
+
const facts=[['Runner',r.runtime==='local'?'CLI':r.runtime==='e2b'?'Cloud':r.runtime],['Result',r.status],['Commit',r.git?.head?.slice(0,8)],['Source',r.git?.dirty?'Includes uncommitted changes':''],['Duration',dur(r.durationMs)],['Checks',(r.assertions??[]).filter(a=>a.passed).length+'/'+(r.assertions??[]).length]].filter(([,v])=>v);
|
|
64
|
+
$('#sandbox-body').innerHTML=`<div class="fact-grid">${facts.map(([k,v])=>`<div class="fact-item"><span>${esc(k)}</span><b title="${esc(v)}">${esc(v)}</b></div>`).join('')}</div>`;
|
|
65
|
+
}
|
|
66
|
+
function renderGit(){
|
|
67
|
+
const d=window.agentTwinDashboard?.getDiscovery(),g=d?.git;
|
|
68
|
+
$('#git-meta').textContent=g?g.branch:'';
|
|
69
|
+
if(!g){$('#git-body').innerHTML='<p class="w-empty">Sync your repository to see commits and workflow changes.</p><code class="sync-command">npx agenttwin sync</code>';return;}
|
|
70
|
+
const days=Array.from({length:14},(_,i)=>{const day=new Date(Date.now()-(13-i)*86400000).toISOString().slice(0,10);return {day,count:g.commits.filter(c=>c.date.slice(0,10)===day).length};});
|
|
71
|
+
const max=Math.max(1,...days.map(d=>d.count));
|
|
72
|
+
const changes=(d.workflows??[]).filter(w=>g.changedFiles.includes((w.entrypoint??'').split('#')[0]));
|
|
73
|
+
$('#git-body').innerHTML=`<div class="git-total"><strong>${g.commits.length}${g.commits.length===100?'+':''}</strong><span>commits · last 14 days</span></div><div class="git-bars" aria-label="Commit activity over 14 days">${days.map(d=>`<span style="--bar:${Math.max(5,d.count/max*100)}%" title="${esc(d.day)}: ${d.count} commits"></span>`).join('')}</div><div class="git-foot"><span>${g.totalCommits} commits in repository</span><code>${esc(g.head.slice(0,8))}${g.dirty?' + changes':''}</code></div><p class="w-empty">${changes.length?changes.length+' workflow entrypoints changed since the previous sync. Retest to record new outcomes.':'No workflow entrypoint changes detected since the previous sync.'}</p>`;
|
|
74
|
+
}
|
|
75
|
+
function renderServices(){
|
|
76
|
+
const events=runs.flatMap(r=>(r.events??[]).map(e=>({r,e}))).slice(0,5);
|
|
77
|
+
$('#services-body').innerHTML=events.length?events.map(({r,e})=>`<button type="button" class="activity-row" data-open-receipt="${esc(r.runId)}"><span class="activity-dot ${esc(e.operation)}"></span><span><strong>${esc(e.operation)} · ${esc(e.service)}</strong><small>${esc(e.path)} · ${esc(e.detail)}</small></span><time>${esc(rel(r.createdAt))}</time></button>`).join(''):'<p class="w-empty">Reads, writes, retries and injected failures appear here after a test records them.</p>';
|
|
78
|
+
}
|
|
79
|
+
function renderFailures(){
|
|
80
|
+
const checks=runs.flatMap(r=>[...(r.assertions??[]),...(r.infrastructureAssertions??[])].map(a=>({r,a})));
|
|
81
|
+
const failed=checks.filter(({a})=>a.passed===false);
|
|
82
|
+
$('#failures-meta').textContent=checks.length?`${failed.length} failed / ${checks.length} checks`:'';
|
|
83
|
+
const value=v=>typeof v==='object'?JSON.stringify(v):String(v??'not recorded');
|
|
84
|
+
$('#failures-body').innerHTML=failed.length?failed.slice(0,6).map(({r,a})=>`<button type="button" class="failure-row" data-open-receipt="${esc(r.runId)}"><span>${statusIco('failed')}</span><span><strong>${esc(a.service??'Workflow')}</strong><span class="f-path"> ${esc(a.path??a.query??a.key??'')}</span><span class="f-path">Expected ${esc(value(a.count??a.equals??a.expected))} · observed ${esc(value(a.actual))}</span><span class="f-path">${esc(title(r))} → View run</span></span></button>`).join(''):runsLoadError&&!runs.length?receiptError():!runs.length?'<p class="w-empty">No checks recorded yet. <button type="button" class="linklike" data-goto="configure">Run a workflow</button> to see what passes and what breaks.</p>':!checks.length?'<p class="w-empty">These runs contain no recorded assertions. Add expected outcomes to your workflow before retesting.</p>':`<p class="w-empty">${checks.filter(({a})=>a.passed===true).length} recorded checks passed. ${checks.filter(({a})=>typeof a.passed!=='boolean').length?'Some checks have no recorded outcome.':'No assertion failures in the loaded runs.'} <button type="button" class="linklike" data-goto="history">View evidence</button></p>`;
|
|
85
|
+
}
|
|
86
|
+
/* ---- Composer ---- */
|
|
87
|
+
function observedServices(){
|
|
88
|
+
const names=new Set();
|
|
89
|
+
for(const r of runs){Object.keys(r.finalState??{}).forEach(n=>names.add(n));(r.infrastructureAssertions??[]).forEach(a=>names.add(a.service));}
|
|
90
|
+
return [...names].filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
function renderComposerCounts(){
|
|
93
|
+
const project=window.agentTwinDashboard?.getProject();
|
|
94
|
+
$('#assistant-project-context').textContent=project?.discovered?project.name:'';
|
|
95
|
+
const ready=hosted.connected&&chatState?.available;
|
|
96
|
+
$('#composer-send').disabled=!ready||chatPending;
|
|
97
|
+
$('#composer-input').disabled=!ready;
|
|
98
|
+
$('#assistant-connect').hidden=hosted.connected;
|
|
99
|
+
$('#assistant-connection-status').textContent=!hosted.connected?'Connect your AgentTwin account to get replies.':!chatState?'Checking chat connection…':!chatState.available?'Chat is unavailable on this server.':'Ask about '+(project?.discovered?project.name:'testing your agent')+'. Chat can explain evidence; it does not start runs.';
|
|
100
|
+
}
|
|
101
|
+
function composerInit(){
|
|
102
|
+
const note=(text,show=true)=>{const n=$('#composer-note');n.textContent=text;n.classList.toggle('show',show&&!!text);};
|
|
103
|
+
const send=async()=>{
|
|
104
|
+
const input=$('#composer-input'),text=input.value.trim();
|
|
105
|
+
if(!text){note('Describe the workflow or failure you want help with.');return;}
|
|
106
|
+
if(chatPending)return;
|
|
107
|
+
if(!hosted.connected){note('Connect your account in Settings to chat. Your message stays here until you send it.');return;}
|
|
108
|
+
const generation=chatGeneration;
|
|
109
|
+
chatPending=true;$('#composer-send').disabled=true;note('Reading your synced discovery and run evidence…');
|
|
110
|
+
try{
|
|
111
|
+
if(!chatState)chatState=await window.agentTwinAccount.chat();
|
|
112
|
+
if(!chatState.available)throw new Error('Chat is not configured. Set OPENAI_API_KEY and AGENTTWIN_CHAT_MODEL on the API service.');
|
|
113
|
+
if(generation!==chatGeneration)throw new Error('Account or project changed. Send this message in the current project.');
|
|
114
|
+
if(!lastRequest||lastRequest.content!==text||lastRequest.project!==chatState.project||lastRequest.jobId!==chatJobId)lastRequest={content:text,project:chatState.project,requestId:crypto.randomUUID(),jobId:chatJobId};
|
|
115
|
+
const data=await window.agentTwinAccount.sendChat(lastRequest);
|
|
116
|
+
if(generation!==chatGeneration)return;
|
|
117
|
+
lastRequest=null;
|
|
118
|
+
chatState={...chatState,thread:data.thread};input.value='';renderChat();note('Response saved.');
|
|
119
|
+
}catch(error){if(generation!==chatGeneration)return;if(error.data?.thread){chatState={...chatState,thread:error.data.thread};lastRequest=null;renderChat();}note(error.message);}
|
|
120
|
+
finally{chatPending=false;renderComposerCounts();}
|
|
121
|
+
};
|
|
122
|
+
$('#composer-send').addEventListener('click',send);
|
|
123
|
+
$('#composer-input').addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){e.preventDefault();send();}});
|
|
124
|
+
$('#assistant-connect').addEventListener('click',()=>{window.agentTwinDashboard?.go('settings');$('#api-token').focus();});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ---- Top bar: switcher, bell, palette, fullscreen, customize, loop pill ---- */
|
|
128
|
+
function closeMenus(){$('#home-bell-menu').hidden=true;$('#home-bell').setAttribute('aria-expanded','false');}
|
|
129
|
+
function renderBell(){
|
|
130
|
+
const waiting=runs.filter(r=>r.status==='failed'||r.status==='error');
|
|
131
|
+
const errored=hosted.connected?hosted.jobs.filter(j=>j.status==='error'||j.status==='cancelled'):[];
|
|
132
|
+
const badge=$('#bell-badge'),count=waiting.length+errored.length;
|
|
133
|
+
badge.textContent=count;badge.hidden=!count;
|
|
134
|
+
const homeBadge=$('#home-badge'),homeCount=waiting.length+liveJobs().length;
|
|
135
|
+
homeBadge.textContent=homeCount;homeBadge.hidden=!homeCount;
|
|
136
|
+
let items=errored.slice(0,2).map(j=>`<button type="button" class="bell-item" data-open-runs><span class="dot" style="background:var(--h-red)"></span><span>Cloud run ${esc(String(j.id).slice(0,8))} ${esc(j.status)}<small>submitted ${esc(rel(j.createdAt))}</small></span></button>`).join('');
|
|
137
|
+
items+=waiting.slice(0,3).map(r=>`<button type="button" class="bell-item" data-open-runs><span class="dot"></span><span>${esc(title(r))} receipt is ready<small>${esc(r.status)} · ${esc(rel(r.createdAt))} · inspect its assertions</small></span></button>`).join('');
|
|
138
|
+
$('#bell-items').innerHTML=items||'<p class="palette-empty" style="padding:8px">Nothing needs attention. Failed receipts and errored jobs appear here.</p>';
|
|
139
|
+
}
|
|
140
|
+
function topbarInit(){
|
|
141
|
+
const toggleMenu=(btn,menu)=>{
|
|
142
|
+
btn.addEventListener('click',e=>{
|
|
143
|
+
e.stopPropagation();const open=menu.hidden;closeMenus();menu.hidden=!open;btn.setAttribute('aria-expanded',String(open));
|
|
144
|
+
});
|
|
145
|
+
menu.addEventListener('keydown',e=>{
|
|
146
|
+
if(!['ArrowDown','ArrowUp'].includes(e.key))return;
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
const items=[...menu.querySelectorAll('button')];
|
|
149
|
+
const index=items.indexOf(document.activeElement);
|
|
150
|
+
const next=e.key==='ArrowDown'?(index+1)%items.length:(index-1+items.length)%items.length;
|
|
151
|
+
items[next].focus();
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
toggleMenu($('#home-bell'),$('#home-bell-menu'));
|
|
155
|
+
document.addEventListener('click',e=>{if(!e.target.closest('.home-bell-wrap'))closeMenus();});
|
|
156
|
+
document.addEventListener('keydown',e=>{
|
|
157
|
+
if(e.key!=='Escape')return;
|
|
158
|
+
if(!$('#home-bell-menu').hidden){closeMenus();$('#home-bell').focus();}
|
|
159
|
+
});
|
|
160
|
+
document.addEventListener('click',e=>{
|
|
161
|
+
const sw=e.target.closest('.home-switcher [data-view]');
|
|
162
|
+
if(sw){window.agentTwinDashboard?.go(sw.dataset.view);return;}
|
|
163
|
+
const discuss=e.target.closest('[data-discuss-job]');if(discuss&&discuss.dataset.discussJob){chatJobId=discuss.dataset.discussJob;window.agentTwinDashboard?.go('home');$('#composer-input').value='Review this run’s results and coverage. Explain any failures, identify missing evidence, and suggest the next step.';$('#assistant-dock-panel').hidden=false;$('#assistant-dock-toggle').hidden=true;$('#assistant-dock-toggle').setAttribute('aria-expanded','true');$('#composer-input').focus();return;}
|
|
164
|
+
const receipt=e.target.closest('[data-open-receipt]');if(receipt){closeMenus();window.agentTwinDashboard?.openReceipt(receipt.dataset.openReceipt);return;}
|
|
165
|
+
const job=e.target.closest('[data-open-job]');if(job){closeMenus();window.agentTwinAccount?.inspectJob(job.dataset.openJob).catch(error=>{$('#composer-note').textContent=error.message;$('#composer-note').classList.add('show');});return;}
|
|
166
|
+
if(e.target.closest('[data-open-runs]')){closeMenus();window.agentTwinDashboard?.go('history');return;}
|
|
167
|
+
const goto=e.target.closest('[data-goto]');
|
|
168
|
+
if(goto)window.agentTwinDashboard?.go(goto.dataset.goto);
|
|
169
|
+
});
|
|
170
|
+
$('#home-search').addEventListener('click',openPalette);
|
|
171
|
+
document.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==='k'){e.preventDefault();openPalette();}});
|
|
172
|
+
$('#home-expand').addEventListener('click',()=>{document.fullscreenElement?document.exitFullscreen():document.documentElement.requestFullscreen().catch(()=>{});});
|
|
173
|
+
const widgetKey='agenttwin:home-widgets:v1';
|
|
174
|
+
let hiddenWidgets=[];
|
|
175
|
+
try{hiddenWidgets=JSON.parse(localStorage.getItem(widgetKey)||'[]');if(!Array.isArray(hiddenWidgets))hiddenWidgets=[];}catch{}
|
|
176
|
+
const widgets=[...document.querySelectorAll('.home-widgets .widget')];
|
|
177
|
+
const controls=$('#home-widget-controls');
|
|
178
|
+
const applyWidgets=()=>widgets.forEach(w=>{w.hidden=hiddenWidgets.includes(w.id);});
|
|
179
|
+
controls.innerHTML=widgets.map(w=>`<label><input type="checkbox" data-widget="${w.id}" ${hiddenWidgets.includes(w.id)?'':'checked'}> ${esc(w.querySelector('h2').textContent)}</label>`).join('')+'<button type="button" id="home-widgets-reset">Show all</button>';
|
|
180
|
+
const saveWidgets=()=>{applyWidgets();try{localStorage.setItem(widgetKey,JSON.stringify(hiddenWidgets));}catch{}};
|
|
181
|
+
applyWidgets();
|
|
182
|
+
$('#home-customize').addEventListener('click',()=>{controls.hidden=!controls.hidden;$('#home-customize').setAttribute('aria-pressed',String(!controls.hidden));});
|
|
183
|
+
controls.addEventListener('change',e=>{const id=e.target.dataset.widget;if(!id)return;hiddenWidgets=hiddenWidgets.filter(v=>v!==id);if(!e.target.checked)hiddenWidgets.push(id);saveWidgets();});
|
|
184
|
+
$('#home-widgets-reset').addEventListener('click',()=>{hiddenWidgets=[];controls.querySelectorAll('input').forEach(i=>i.checked=true);saveWidgets();});
|
|
185
|
+
const setDock=open=>{$('#assistant-dock-panel').hidden=!open;$('#assistant-dock-toggle').hidden=open;$('#assistant-dock-toggle').setAttribute('aria-expanded',String(open));if(open){$('#chat-thread').scrollTop=$('#chat-thread').scrollHeight;$('#composer-input').focus();}else $('#assistant-dock-toggle').focus();};
|
|
186
|
+
$('#assistant-dock-toggle').addEventListener('click',()=>setDock(true));
|
|
187
|
+
$('#assistant-dock-close').addEventListener('click',()=>setDock(false));
|
|
188
|
+
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!$('#assistant-dock-panel').hidden)setDock(false);});
|
|
189
|
+
|
|
190
|
+
}
|
|
191
|
+
/* ---- Command palette ---- */
|
|
192
|
+
function openPalette(){
|
|
193
|
+
closeMenus();
|
|
194
|
+
if($('.palette'))return;
|
|
195
|
+
const overlay=document.createElement('div');overlay.className='palette';
|
|
196
|
+
const views=[['home','Home','Overview with live widgets'],['environment','Environment','Discovered resources and coverage'],['configure','Workflows','Prepare and execute workflows'],['history','Runs','Recorded receipts and evidence'],['settings','Settings','Workspace and account connection']];
|
|
197
|
+
if(!hosted.connected)views.push(['settings','Connect account','Link an AgentTwin API to submit runs']);
|
|
198
|
+
overlay.innerHTML=`<div class="palette-panel" role="dialog" aria-label="Search"><input type="search" placeholder="Search views and receipts…" aria-label="Search"><div class="palette-list"></div><div class="palette-hint"><span><kbd>↑↓</kbd>navigate</span><span><kbd>↵</kbd>open</span><span><kbd>esc</kbd>close</span></div></div>`;
|
|
199
|
+
document.body.append(overlay);
|
|
200
|
+
const input=overlay.querySelector('input'),list=overlay.querySelector('.palette-list');
|
|
201
|
+
const items=()=>[...views.map(([id,label,hint])=>({label,hint,run:()=>window.agentTwinDashboard?.go(id)})),...runs.map(r=>({label:title(r),hint:`${r.status} · ${rel(r.createdAt)}`,run:()=>window.agentTwinDashboard?.openReceipt(r.runId)}))];
|
|
202
|
+
const renderList=()=>{
|
|
203
|
+
const q=input.value.trim().toLowerCase();
|
|
204
|
+
const visible=items().filter(i=>(i.label+' '+i.hint).toLowerCase().includes(q));
|
|
205
|
+
list.innerHTML=visible.length?visible.map((i,n)=>`<button type="button" class="palette-item${n===0?' active':''}" data-index="${n}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="m5 12h14M13 6l6 6-6 6"/></svg>${esc(i.label)}<small>${esc(i.hint)}</small></button>`).join(''):'<p class="palette-empty">Nothing matches.</p>';
|
|
206
|
+
};
|
|
207
|
+
renderList();
|
|
208
|
+
let activeIndex=0;
|
|
209
|
+
const visible=()=>items().filter(i=>(i.label+' '+i.hint).toLowerCase().includes(input.value.trim().toLowerCase()));
|
|
210
|
+
const setActive=n=>{
|
|
211
|
+
const buttons=[...list.querySelectorAll('.palette-item')];
|
|
212
|
+
if(!buttons.length)return;
|
|
213
|
+
activeIndex=(n+buttons.length)%buttons.length;
|
|
214
|
+
buttons.forEach((b,i)=>b.classList.toggle('active',i===activeIndex));
|
|
215
|
+
buttons[activeIndex].scrollIntoView({block:'nearest'});
|
|
216
|
+
};
|
|
217
|
+
input.addEventListener('input',()=>{renderList();activeIndex=0;});
|
|
218
|
+
input.addEventListener('keydown',e=>{
|
|
219
|
+
if(e.key==='Escape'){overlay.remove();return;}
|
|
220
|
+
if(e.key==='ArrowDown'){e.preventDefault();setActive(activeIndex+1);return;}
|
|
221
|
+
if(e.key==='ArrowUp'){e.preventDefault();setActive(activeIndex-1);return;}
|
|
222
|
+
if(e.key==='Enter'){e.preventDefault();const item=visible()[activeIndex];if(item){item.run();overlay.remove();}}
|
|
223
|
+
});
|
|
224
|
+
list.addEventListener('click',e=>{const item=e.target.closest('.palette-item');if(!item)return;visible()[Number(item.dataset.index)]?.run();overlay.remove();});
|
|
225
|
+
overlay.addEventListener('click',e=>{if(e.target===overlay)overlay.remove();});
|
|
226
|
+
input.focus();
|
|
227
|
+
}
|
|
228
|
+
/* ---- Boot ---- */
|
|
229
|
+
let chatState=null,chatPending=false,chatGeneration=0,chatJobId=null,lastRequest=null;
|
|
230
|
+
function renderChat(){
|
|
231
|
+
const messages=chatState?.thread?.messages??[];
|
|
232
|
+
$('#chat-thread').innerHTML=messages.map(m=>`<article class="chat-message ${esc(m.role)}"><header>${m.role==='user'?'You':'AgentTwin'}${m.error?' · response failed':''}</header><div class="chat-content">${m.role==='assistant'&&window.marked&&window.DOMPurify?DOMPurify.sanitize(marked.parse(String(m.content??''),{gfm:true,breaks:true}),{ALLOWED_TAGS:['p','br','strong','em','del','code','pre','blockquote','ul','ol','li','h1','h2','h3','h4','table','thead','tbody','tr','th','td','hr','a'],ALLOWED_ATTR:['href','title'],ALLOW_DATA_ATTR:false}):'<p>'+esc(m.content)+'</p>'}</div>${m.context?.jobId?`<button type="button" class="linklike" data-open-job="${esc(m.context.jobId)}">Inspect evidence · ${esc(m.context.jobId.slice(0,8))}</button>`:''}</article>`).join('');
|
|
233
|
+
$('#chat-thread').hidden=!messages.length;
|
|
234
|
+
renderComposerCounts();
|
|
235
|
+
$('.hero-content').classList.toggle('has-conversation',messages.length>0);
|
|
236
|
+
$('#chat-thread').scrollTop=$('#chat-thread').scrollHeight;
|
|
237
|
+
$('#settings-chat-status').textContent=chatState?(chatState.available?'Connected model: '+chatState.model:'Chat is unavailable. The API operator needs to configure OPENAI_API_KEY and AGENTTWIN_CHAT_MODEL.'):'Connect your account to check model availability.';
|
|
238
|
+
}
|
|
239
|
+
async function refreshChat(){
|
|
240
|
+
const generation=++chatGeneration;chatState=null;chatJobId=null;lastRequest=null;renderChat();
|
|
241
|
+
if(!hosted.connected)return;
|
|
242
|
+
try{const data=await window.agentTwinAccount.chat();if(generation!==chatGeneration)return;chatState=data;renderChat();}
|
|
243
|
+
catch(error){if(generation===chatGeneration){$('#assistant-connection-status').textContent='Chat connection failed: '+error.message;}}
|
|
244
|
+
}
|
|
245
|
+
function onReceipts(){const name=window.agentTwinDashboard?.getProject()?.name;runs=(window.agentTwinDashboard?.getReceipts()??[]).filter(r=>!r.project||r.project===name);renderAll();}
|
|
246
|
+
function onHostedJobs(){
|
|
247
|
+
const connected=hosted.connected;
|
|
248
|
+
hosted=window.agentTwinHosted??hosted;
|
|
249
|
+
if(connected!==hosted.connected)void refreshChat();
|
|
250
|
+
renderAll();
|
|
251
|
+
}
|
|
252
|
+
if($('#home-view')){
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
composerInit();
|
|
256
|
+
topbarInit();
|
|
257
|
+
addEventListener('agenttwin:jobs',onHostedJobs);
|
|
258
|
+
if(window.agentTwinHosted)onHostedJobs();
|
|
259
|
+
addEventListener('agenttwin:receipts',onReceipts);
|
|
260
|
+
addEventListener('agenttwin:discovery',()=>{onReceipts();void refreshChat();});
|
|
261
|
+
onReceipts();renderChat();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
})();
|