agenttwin 0.3.0 → 0.3.2

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.
@@ -48,7 +48,7 @@ function renderReceipts(){
48
48
  $('#receipts-count').textContent=failed.length||'';
49
49
  $('#receipts-spin').hidden=!liveJobs().length;
50
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>`;
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))}">${r.runtime==='local'?'CLI':r.runtime==='e2b'?'Cloud':'Imported'} · ${r.git?.head?esc(r.git.head.slice(0,8))+(r.git.dirty?' + changes':''):esc(String(r.runId).slice(0,8))}</span><span class="r-status ${esc(r.status)}">${esc(r.status)}</span><span class="r-time">${esc(rel(r.createdAt))}</span></button>`;
52
52
  let html='';
53
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
54
  if(rest.length)html+=`<p class="group-label">Passed · <b>${rest.length}</b></p>${rest.slice(0,4).map(row).join('')}`;
@@ -70,7 +70,7 @@ function renderGit(){
70
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
71
  const max=Math.max(1,...days.map(d=>d.count));
72
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>`;
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">${!g.previousHead?'First Git snapshot. Change comparisons begin with the next sync.':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
74
  }
75
75
  function renderServices(){
76
76
  const events=runs.flatMap(r=>(r.events??[]).map(e=>({r,e}))).slice(0,5);
@@ -250,7 +250,7 @@ function onHostedJobs(){
250
250
  renderAll();
251
251
  }
252
252
  if($('#home-view')){
253
-
253
+
254
254
 
255
255
  composerInit();
256
256
  topbarInit();
@@ -187,3 +187,5 @@ button,.upload{background:#202329;border-color:#353944;color:var(--ink);border-r
187
187
  .discovery-onboarding{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);gap:24px;margin:0 0 28px}.discovery-onboarding .terminal-panel{position:static;min-width:0}.discovery-onboarding .workflow-area{min-width:0}.workflow-search{display:block;margin:18px 0}.workflow-search input{display:block;width:100%;margin-top:8px}#workflow-candidates{max-height:650px;overflow:auto}.candidate-row{gap:24px}.candidate-row>div{min-width:0}.candidate-row .mono{overflow-wrap:anywhere}@media(max-width:900px){.discovery-onboarding{grid-template-columns:1fr}}
188
188
 
189
189
  #environment-workflows{max-height:320px;overflow:auto}.discovery-onboarding .terminal-boundary code{overflow-wrap:anywhere}
190
+
191
+ .sidebar-workflow-group{margin:4px 0 12px}.sidebar-workflow-group summary{cursor:pointer;color:var(--text-muted,#93949f);padding:8px 10px;font-size:12px;display:flex;justify-content:space-between}.sidebar-workflow-group summary span{opacity:.65}.sidebar-workflow-group .sidebar-workflow{padding-left:20px;line-height:1.5;white-space:normal;text-align:left}.sidebar-workflow-group summary:hover{color:var(--text,#eee)}
@@ -91,13 +91,25 @@ document.querySelector('.project-tabs').addEventListener('click',event=>{const b
91
91
  const downloadFile=(name,text)=>{const url=URL.createObjectURL(new Blob([text],{type:name.endsWith('.json')?'application/json':'text/plain'}));const a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};
92
92
  let environmentTools=[], environmentWorkflows=[], inventoryImported=false;
93
93
  let environmentProject='Your project', environmentGit=null;
94
+ // Names describe the callable operation; IDs and entrypoints remain unchanged.
95
+ const workflowIntentNames={sendDriveFile:'Send a Drive file by email',gmailForwardMessage:'Forward an email',modifyGmailMessages:'Update email labels',trashGmailMessages:'Move emails to trash',gmailSendMessage:'Send an email',gmailCreateDraft:'Create an email draft',googleEventsRaw:'Read calendar events',findFreeSlots:'Find available meeting times',calendarHold:'Reserve a calendar slot',mutateCalendarEvent:'Update a calendar event'};
96
+ function readableName(value){return String(value).replace(/([a-z0-9])([A-Z])/g,'$1 $2').replace(/([A-Z])([A-Z][a-z])/g,'$1 $2').replace(/[_-]+/g,' ').replace(/\s+/g,' ').trim();}
97
+ function workflowName(w){const fn=(w.entrypoint??'').split('#')[1];if(workflowIntentNames[fn])return workflowIntentNames[fn];let text=readableName(w.title??w.name??fn??w.id);if(/routes?$/i.test(text))text=text.replace(/ routes?$/i,' API routes');return text.replace(/^./,c=>c.toUpperCase()).replace(/\bgmail\b/gi,'Gmail');}
98
+ function workflowGroup(w){const fn=(w.entrypoint??'').split('#')[1]??'';if(/(?:^|\/)routes\//.test(w.entrypoint??''))return 'API routes';if(/Drive|File/.test(fn))return 'Files';if(/gmail|Gmail|Mail|Email/.test(fn))return 'Email';if(/calendar|Calendar|Events|FreeSlots|Meeting/.test(fn))return 'Calendar';return 'Other workflows';}
99
+ function sortedWorkflows(list){const order=['Email','Calendar','Files','API routes','Other workflows'];return [...list].sort((a,b)=>order.indexOf(workflowGroup(a))-order.indexOf(workflowGroup(b))||workflowName(a).localeCompare(workflowName(b)));}
94
100
  function controlPath(tool){
95
101
  const mode=tool.interface;
96
102
  const title=tool.title.toLowerCase();
103
+ if(tool.kind==='database')return ['Database','Discovered database dependency. Review the workflow’s queries and starting state before configuring its runtime.'];
104
+ if(tool.kind==='ai')return ['Model provider','Discovered model dependency. Choose explicit test responses and assertions; discovery does not reproduce model behavior.'];
105
+ if(title==='kernel')return ['Browser service','Kernel SDK detected. Browser execution needs a reviewed browser workflow and compatible runner.'];
106
+ if(title==='e2b')return ['Cloud runtime SDK','The project declares E2B. This does not launch a VM or require AgentTwin tests to use cloud execution.'];
107
+ if(title==='composio')return ['Integration gateway','Composio SDK detected. Review which connected services and operations this workflow invokes.'];
108
+ if(title==='1password')return ['Secret manager','1Password SDK detected. Test fixtures must replace secrets; no vault access is implied.'];
97
109
  if(tool.category==='access')return ['Configuration','Connector configuration was detected. This does not establish application usage or verify account access.'];
98
110
  if(mode==='desktop')return ['Desktop','Needs desktop automation and a compatible hosted OS. Native Apple apps require macOS; this E2B runtime is Linux. Desktop execution is not implemented.'];
99
111
  if(mode==='browser')return ['Browser','No API is required to identify this app. Browser automation in the hosted runtime is planned, not implemented.'];
100
- if(mode==='terminal'||tool.kind==='runtime')return ['Terminal','Discovered tooling is part of the environment. Bundled Node workflows can run in E2B; arbitrary command-line tool replication is not yet supported.'];
112
+ if(mode==='terminal'||tool.kind==='runtime')return ['Terminal','Discovered tooling is part of the environment. Bundled Node workflows run through the CLI; arbitrary command-line tool replication is not yet supported.'];
101
113
  if(title==='slack')return ['API subset','Slack message/channel/history operations have a bounded stateful adapter. Map the actual operations before executing a workflow.'];
102
114
  if(/postgres/.test(title))return ['Real service','PostgreSQL 15 is available in hosted worlds with synthetic seed SQL and state assertions. Discovery does not provision it automatically.'];
103
115
  if(title==='redis')return ['Real service','Redis is available in hosted worlds with fresh credentials and string-key state assertions.'];
@@ -108,7 +120,7 @@ function renderEnvironment(){
108
120
  const ready=environmentTools.length>0||environmentWorkflows.length>0;
109
121
  $("#environment-view").classList.toggle("has-discovery",ready);
110
122
  $("#environment-title").textContent=inventoryImported?environmentProject+" environment":ready?"Your environment":"A test world for your agent.";
111
- $('#environment-subtitle').textContent=inventoryImported?`${environmentTools.length} dependencies discovered. Choose a workflow to review what its test world needs.`:'Discover your project. Test what your agent does before it touches production.';
123
+ $('#environment-subtitle').textContent=inventoryImported?`${environmentTools.length} project resources discovered. Choose a workflow to review what its test world needs.`:'Discover your project. Test what your agent does before it touches production.';
112
124
  $('.discovery-onboarding').hidden=inventoryImported;
113
125
  $('#configure-run').textContent=environmentWorkflows.length?`View all ${environmentWorkflows.length} workflows →`:'Review workflows →';
114
126
  $('#discovery-workflows-title').textContent=inventoryImported?'Workflow candidates':'Discovery';
@@ -124,7 +136,7 @@ function renderEnvironment(){
124
136
  $('#inventory-context').textContent=tools.length?`${tools.length} resources in your environment. Review coverage before execution.`:'No software selected. Select software or import the inventory from your project.';
125
137
  layoutMap();
126
138
  saveEnvironment();
127
- $('#environment-workflows').innerHTML=environmentWorkflows.length?environmentWorkflows.slice(0,3).map(w=>`<article class="tool-entry"><header><strong>${escape(w.title??w.name??w.id)}</strong><span class="tool-route">Candidate</span></header><small>${escape(w.id)}</small><p>${escape(w.entrypoint||"Entrypoint needs review")}</p></article>`).join(''):'<div class="workflow-empty"><img src="assets/icons/ui/git-branch.svg" alt=""><h3>No workflows yet</h3><p>Import project discovery to review candidate code paths.</p></div>';
139
+ $('#environment-workflows').innerHTML=environmentWorkflows.length?environmentWorkflows.slice(0,3).map(w=>`<article class="tool-entry"><header><strong>${escape(workflowName(w))}</strong><span class="tool-route">Candidate</span></header><small>${escape(w.id)}</small><p>${escape(w.entrypoint||"Entrypoint needs review")}</p></article>`).join(''):'<div class="workflow-empty"><img src="assets/icons/ui/git-branch.svg" alt=""><h3>No workflows yet</h3><p>Import project discovery to review candidate code paths.</p></div>';
128
140
  if(!environmentTools.length)$('#terminal-output').innerHTML='<p>Discovery stays on your machine until you sync.</p>';
129
141
  if(environmentTools.length)$('#terminal-output').innerHTML=`<p>${inventoryImported?'Imported discovery inventory.':'Added user declarations; no scan was performed.'}</p><p>${environmentTools.length} apps / tools · ${environmentWorkflows.length} workflow candidates</p>`;
130
142
  }
@@ -140,7 +152,8 @@ function applyDiscovery(inventory){
140
152
  environmentProject=typeof inventory.name==='string'?inventory.name:'Your project';
141
153
  const revision=`${environmentProject}:${inventory.generatedAt??signature}`;
142
154
  if(discoveryRevision!==revision){dismissedDiscovery.clear();discoveryRevision=revision;}
143
- const detected=inventory.tools.filter(t=>!dismissedDiscovery.has(t.id??t.title)).map(t=>({...t,declared:false,catalog:false}));
155
+ const toolOrder=['Gmail','Google Calendar','Google Drive','Google Docs','Google Sheets','Composio','Kernel'];
156
+ const detected=inventory.tools.filter(t=>!dismissedDiscovery.has(t.id??t.title)).map(t=>({...t,declared:false,catalog:false})).sort((a,b)=>{const rank=t=>toolOrder.includes(t.title)?toolOrder.indexOf(t.title):t.category==='tooling'?100:20;return rank(a)-rank(b)||a.title.localeCompare(b.title);});
144
157
  environmentTools=[...detected,...environmentTools.filter(t=>t.declared&&!detected.some(d=>d.title.toLowerCase()===t.title.toLowerCase()))];
145
158
  environmentWorkflows=Array.isArray(inventory.workflows)?inventory.workflows.filter(w=>w&&typeof w.id==='string'):[];
146
159
  inventoryImported=true;mapFocused=-1;renderEnvironment();renderConnectors();renderProjectSidebar();
@@ -313,8 +326,8 @@ function renderWorkspace(){
313
326
  $('#environment-coverage').innerHTML=`<span><b>${environmentTools.length}</b> selected resources</span><span><b>${supported.length}</b> with runtime support</span><span><b>${gaps.length}</b> require an adapter or runtime</span><span><b>${configs.length}</b> configuration only</span>`;
314
327
  $('#configure-view').classList.toggle('has-workflows',environmentWorkflows.length>0);
315
328
  if(!environmentWorkflows.length){$('#workflow-workspace').innerHTML='<div class="workflow-intro"><span class="intro-mark" aria-hidden="true">↳</span><h2>Start with what your agent does.</h2><p>Discover your project to find code paths and their dependencies. Then choose a workflow to test.</p><button type="button" data-action="environment">Discover workflows →</button></div>';return;}
316
- const visible=environmentWorkflows.filter(w=>JSON.stringify(w).toLowerCase().includes(workflowQuery.toLowerCase()));const pageSize=10;workflowPage=Math.min(workflowPage,Math.max(0,Math.ceil(visible.length/pageSize)-1));const page=visible.slice(workflowPage*pageSize,(workflowPage+1)*pageSize);
317
- $('#workflow-workspace').innerHTML=`<section class="workflow-register"><div class="section-title"><h2>Your workflows</h2><span>${visible.length} of ${environmentWorkflows.length}</span></div><label class="workflow-search">Find a workflow<input id="workflow-search" type="search" placeholder="Search name, entrypoint or operation" value="${escape(workflowQuery)}"></label><div id="workflow-candidates">${visible.length?page.map(w=>`<article class="candidate-row"><div><h3>${escape(w.title??w.name??w.id)}</h3><p class="mono">${escape(w.entrypoint||w.id)}</p><p class="muted">${w.supported?'Dependency links detected':'Dependencies need review'} · ${workflowEvidence(w)}</p></div><div class="actions">${runs.some(r=>r.workflowId===w.id)?`<button type="button" data-candidate-results="${escape(w.id)}">View ${runs.filter(r=>r.workflowId===w.id).length} run results →</button>`:''}<button type="button" data-prepare-candidate="${escape(w.id)}">Prepare workflow →</button></div></article>`).join(''):'<p class="coverage">No workflows match your search.</p>'}</div>${visible.length>pageSize?`<div class="workflow-pagination"><button type="button" data-workflow-page="-1" ${workflowPage===0?'disabled':''}>Previous</button><span>Page ${workflowPage+1} of ${Math.ceil(visible.length/pageSize)}</span><button type="button" data-workflow-page="1" ${(workflowPage+1)*pageSize>=visible.length?'disabled':''}>Next</button></div>`:''}</section>`;if(preparation)$('#workflow-workspace').prepend(preparation);
329
+ const visible=sortedWorkflows(environmentWorkflows).filter(w=>(workflowName(w)+' '+workflowGroup(w)+' '+JSON.stringify(w)).toLowerCase().includes(workflowQuery.toLowerCase()));const pageSize=10;workflowPage=Math.min(workflowPage,Math.max(0,Math.ceil(visible.length/pageSize)-1));const page=visible.slice(workflowPage*pageSize,(workflowPage+1)*pageSize);
330
+ $('#workflow-workspace').innerHTML=`<section class="workflow-register"><div class="section-title"><h2>Your workflows</h2><span>${visible.length} of ${environmentWorkflows.length}</span></div><label class="workflow-search">Find a workflow<input id="workflow-search" type="search" placeholder="Search name, entrypoint or operation" value="${escape(workflowQuery)}"></label><div id="workflow-candidates">${visible.length?page.map(w=>`<article class="candidate-row"><div><h3>${escape(workflowName(w))}</h3><p class="muted">${escape(workflowGroup(w))}${w.tools.length?' · '+escape(w.tools.map(id=>environmentTools.find(t=>t.id===id)?.title??id).join(', ')):' · Dependencies need review'}</p><p class="mono">${escape(w.entrypoint||w.id)}</p><p class="muted">${w.supported?'Dependency links detected':'Dependencies need review'} · ${workflowEvidence(w)}</p></div><div class="actions">${runs.some(r=>r.workflowId===w.id)?`<button type="button" data-candidate-results="${escape(w.id)}">View ${runs.filter(r=>r.workflowId===w.id).length} run results →</button>`:''}<button type="button" data-prepare-candidate="${escape(w.id)}">Prepare workflow →</button></div></article>`).join(''):'<p class="coverage">No workflows match your search.</p>'}</div>${visible.length>pageSize?`<div class="workflow-pagination"><button type="button" data-workflow-page="-1" ${workflowPage===0?'disabled':''}>Previous</button><span>Page ${workflowPage+1} of ${Math.ceil(visible.length/pageSize)}</span><button type="button" data-workflow-page="1" ${(workflowPage+1)*pageSize>=visible.length?'disabled':''}>Next</button></div>`:''}</section>`;if(preparation)$('#workflow-workspace').prepend(preparation);
318
331
  }
319
332
  $('#main').addEventListener('input',event=>{if(event.target.id!=='workflow-search')return;workflowQuery=event.target.value;workflowPage=0;const cursor=event.target.selectionStart;renderWorkspace();$('#workflow-search').focus();try{$('#workflow-search').setSelectionRange(cursor,cursor);}catch{}});
320
333
  $('#main').addEventListener('click',event=>{
@@ -408,7 +421,7 @@ document.querySelector('.topbar .brand').addEventListener('click',event=>{if(eve
408
421
  function renderProjectSidebar(){
409
422
  const target=document.getElementById('sidebar-projects');if(!target)return;
410
423
  const projects=window.agentTwinProjects?.length?window.agentTwinProjects:(inventoryImported?[{name:environmentProject}]:[]);
411
- target.innerHTML='<p class="sidebar-label">Repositories</p>'+projects.map(p=>`<button type="button" class="sidebar-project ${p.name===environmentProject?'active':''}" data-select-project="${escape(p.name)}">${escape(p.name)}</button>`).join('')+(inventoryImported?'<p class="sidebar-label">Workflows</p>'+environmentWorkflows.slice(0,12).map(w=>`<button type="button" class="sidebar-workflow" data-sidebar-workflow="${escape(w.id)}">${escape(w.title??w.name??w.id)}</button>`).join(''):'<p class="sidebar-empty">Synced repositories appear here.</p>');
424
+ target.innerHTML='<p class="sidebar-label">Repositories</p>'+projects.map(p=>`<button type="button" class="sidebar-project ${p.name===environmentProject?'active':''}" data-select-project="${escape(p.name)}">${escape(p.name)}</button>`).join('')+(inventoryImported?'<p class="sidebar-label">Workflows · '+environmentWorkflows.length+'</p>'+['Email','Calendar','Files','API routes','Other workflows'].map(group=>{const items=sortedWorkflows(environmentWorkflows).filter(w=>workflowGroup(w)===group);return items.length?`<details class="sidebar-workflow-group" ${group==='Email'?'open':''}><summary>${escape(group)} <span>${items.length}</span></summary>${items.map(w=>`<button type="button" class="sidebar-workflow" title="${escape(w.entrypoint??w.id)}" data-sidebar-workflow="${escape(w.id)}">${escape(workflowName(w))}</button>`).join('')}</details>`:'';}).join(''):'<p class="sidebar-empty">Synced repositories appear here.</p>');
412
425
  }
413
426
  addEventListener('agenttwin:projects',renderProjectSidebar);
414
427
  addEventListener('agenttwin:discovery',renderProjectSidebar);
@@ -6,9 +6,9 @@
6
6
  <meta name="description" content="AgentTwin workspace: workflow tests, observed activities, Git revisions and failed checks — all from recorded evidence.">
7
7
  <meta name="theme-color" content="#101013">
8
8
  <link rel="icon" href="assets/logo/favicon.svg"><link rel="stylesheet" href="assets/brand.css"><link rel="stylesheet" href="assets/dashboard/dashboard.css"><link rel="stylesheet" href="assets/dashboard/workspace.css?v=0.10.0"><link rel="stylesheet" href="assets/dashboard/agent-home.css?v=0.10.0">
9
- <script src="assets/dashboard/dashboard.js?v=0.10.0" type="module"></script><script src="assets/vendor/marked.js" defer></script>
9
+ <script src="assets/dashboard/dashboard.js?v=0.10.2" type="module"></script><script src="assets/vendor/marked.js" defer></script>
10
10
  <script src="assets/vendor/purify.min.js" defer></script>
11
- <script src="assets/dashboard/agent-home.js?v=0.10.0" type="module"></script><script src="assets/dashboard/hosted.js?v=0.4.0" defer></script><script src="assets/dashboard/workflows.js?v=0.2.0" type="module"></script>
11
+ <script src="assets/dashboard/agent-home.js?v=0.10.1" type="module"></script><script src="assets/dashboard/hosted.js?v=0.4.0" defer></script><script src="assets/dashboard/workflows.js?v=0.2.0" type="module"></script>
12
12
  </head>
13
13
  <body>
14
14
  <a class="skip" href="#main">Skip to content</a>