glove-foundry 0.0.0 → 0.2.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.
Files changed (35) hide show
  1. package/dist/{chunk-UNHGCCSA.js → chunk-P65RT7H5.js} +486 -49
  2. package/dist/cli.js +536 -352
  3. package/dist/index.js +1 -1
  4. package/docs/building-with-foundry.md +39 -2
  5. package/docs/inspector.md +35 -2
  6. package/package.json +7 -6
  7. package/templates/minimal/README.md +58 -0
  8. package/templates/minimal/agents/assistant/agent.ts +28 -0
  9. package/templates/minimal/agents/assistant/tools/current-time.tool.ts +17 -0
  10. package/templates/minimal/env.example +2 -0
  11. package/templates/minimal/foundry.application.ts +12 -0
  12. package/templates/minimal/foundry.config.ts +6 -0
  13. package/templates/minimal/gitignore +3 -0
  14. package/templates/travel-concierge/README.md +203 -0
  15. package/templates/travel-concierge/agents/concierge/actions/reply.action.ts +6 -0
  16. package/templates/travel-concierge/agents/concierge/agent.ts +106 -0
  17. package/templates/travel-concierge/agents/concierge/apps/calendar.app.ts +67 -0
  18. package/templates/travel-concierge/agents/concierge/composition.ts +18 -0
  19. package/templates/travel-concierge/agents/concierge/events/message-received.event.ts +7 -0
  20. package/templates/travel-concierge/agents/concierge/events/message-sent.event.ts +7 -0
  21. package/templates/travel-concierge/agents/concierge/layers/trip-context.layer.ts +23 -0
  22. package/templates/travel-concierge/agents/concierge/memory/traveller.memory.ts +19 -0
  23. package/templates/travel-concierge/agents/concierge/predicates/mentions-trip.predicate.ts +17 -0
  24. package/templates/travel-concierge/agents/concierge/schedules/trip-countdown.ts +16 -0
  25. package/templates/travel-concierge/agents/concierge/subscribers/usage.subscriber.ts +16 -0
  26. package/templates/travel-concierge/agents/concierge/tools/find-flights.tool.ts +49 -0
  27. package/templates/travel-concierge/agents/concierge/topology.ts +35 -0
  28. package/templates/travel-concierge/agents/concierge/transmissions/messaging.transmission.ts +57 -0
  29. package/templates/travel-concierge/agents/concierge/workbench.ts +26 -0
  30. package/templates/travel-concierge/env.example +3 -0
  31. package/templates/travel-concierge/foundry.application.ts +18 -0
  32. package/templates/travel-concierge/foundry.config.ts +9 -0
  33. package/templates/travel-concierge/gitignore +3 -0
  34. package/templates/travel-concierge/lib/demo-model.ts +72 -0
  35. package/templates/travel-concierge/src/client.ts +22 -0
@@ -3159,7 +3159,14 @@ var PHOSPHOR_ICON_PATHS = {
3159
3159
  search: "M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z",
3160
3160
  definitions: "M229.66,189.66l-32,32a8,8,0,0,1-11.32,0l-32-32a8,8,0,0,1,11.32-11.32L184,196.69V139.31l-56-56-56,56v57.38l18.34-18.35a8,8,0,0,1,11.32,11.32l-32,32a8,8,0,0,1-11.32,0l-32-32a8,8,0,0,1,11.32-11.32L56,196.69V136a8,8,0,0,1,2.34-5.66L120,68.69V24a8,8,0,0,1,16,0V68.69l61.66,61.65A8,8,0,0,1,200,136v60.69l18.34-18.35a8,8,0,0,1,11.32,11.32Z",
3161
3161
  secure: "M208,40H48A16,16,0,0,0,32,56v56c0,52.72,25.52,84.67,46.93,102.19,23.06,18.86,46,25.26,47,25.53a8,8,0,0,0,4.2,0c1-.27,23.91-6.67,47-25.53C198.48,196.67,224,164.72,224,112V56A16,16,0,0,0,208,40Zm0,72c0,37.07-13.66,67.16-40.6,89.42A129.3,129.3,0,0,1,128,223.62a128.25,128.25,0,0,1-38.92-21.81C61.82,179.51,48,149.3,48,112l0-56,160,0ZM82.34,141.66a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32l-56,56a8,8,0,0,1-11.32,0Z",
3162
- external: "M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z"
3162
+ external: "M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z",
3163
+ copy: "M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z",
3164
+ check: "M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z",
3165
+ clock: "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm64-88a8,8,0,0,1-8,8H128a8,8,0,0,1-8-8V72a8,8,0,0,1,16,0v48h48A8,8,0,0,1,192,128Z",
3166
+ refresh: "M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z",
3167
+ warning: "M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z",
3168
+ caretDown: "M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z",
3169
+ funnel: "M200,136a8,8,0,0,1-8,8H64a8,8,0,0,1,0-16H192A8,8,0,0,1,200,136Zm32-56H24a8,8,0,0,0,0,16H232a8,8,0,0,0,0-16Zm-80,96H104a8,8,0,0,0,0,16h48a8,8,0,0,0,0-16Z"
3163
3170
  };
3164
3171
  function renderPhosphorIcon(name, className = "icon") {
3165
3172
  return `<svg class="${className}" data-phosphor="${name}" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="${PHOSPHOR_ICON_PATHS[name]}"/></svg>`;
@@ -3174,7 +3181,7 @@ function renderGloveMark() {
3174
3181
  var ICON_PATHS_JSON = JSON.stringify(PHOSPHOR_ICON_PATHS);
3175
3182
  var DASHBOARD_SCRIPT = String.raw`
3176
3183
  const ICON_PATHS=${ICON_PATHS_JSON};
3177
- const state={manifest:null,instances:[],subscriptions:[],connections:[],runs:[],events:[],health:null,transmissions:[],accounts:[],routes:[],bindings:[],activations:[],conversations:{},workspaces:{},filters:{runs:"all"},workspaceTab:"entries",showAllEvents:false};
3184
+ const state={manifest:null,instances:[],subscriptions:[],connections:[],runs:[],events:[],health:null,transmissions:[],accounts:[],routes:[],bindings:[],activations:[],conversations:{},workspaces:{},showAllEvents:false,eventCategory:"all",ready:false,cursor:-1,liveAt:null};
3178
3185
  const $=id=>document.getElementById(id);
3179
3186
  const esc=value=>String(value??"").replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]));
3180
3187
  const attr=value=>esc(value).replace(/\x60/g,"&#96;");
@@ -3185,6 +3192,98 @@ const jsonText=value=>JSON.stringify(value,null,2);
3185
3192
  const count=(n,word)=>n+" "+word+(n===1?"":"s");
3186
3193
  const icon=(name,className)=>'<svg class="'+(className||"icon")+'" data-phosphor="'+name+'" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="'+ICON_PATHS[name]+'"/></svg>';
3187
3194
 
3195
+ /* ---- Elapsed time. Operators read runs in "how long ago" and "how long did it take". ---- */
3196
+ const DAY=86400000;
3197
+ function spanText(ms){
3198
+ const seconds=Math.round(ms/1000);if(seconds<60)return seconds+"s";
3199
+ const minutes=Math.round(seconds/60);if(minutes<60)return minutes+"m";
3200
+ const hours=Math.round(minutes/60);if(hours<24)return hours+"h";
3201
+ const days=Math.round(hours/24);if(days<30)return days+"d";
3202
+ return Math.round(days/30)+"mo";
3203
+ }
3204
+ function relTime(iso){
3205
+ if(!iso)return"—";
3206
+ const then=new Date(iso).getTime();if(!Number.isFinite(then))return"—";
3207
+ const delta=Date.now()-then;
3208
+ if(delta<-1000)return"in "+spanText(-delta);
3209
+ if(delta<5000)return"just now";
3210
+ return spanText(delta)+" ago";
3211
+ }
3212
+ function durationText(ms){
3213
+ if(!Number.isFinite(ms)||ms<0)return"—";
3214
+ if(ms<1000)return Math.round(ms)+"ms";
3215
+ if(ms<60000)return(ms/1000).toFixed(ms<10000?2:1)+"s";
3216
+ const minutes=Math.floor(ms/60000);const seconds=Math.round(ms%60000/1000);
3217
+ if(minutes<60)return minutes+"m "+seconds+"s";
3218
+ return Math.floor(minutes/60)+"h "+minutes%60+"m";
3219
+ }
3220
+ function fullDate(iso){return iso?new Date(iso).toLocaleString([],{weekday:"short",month:"short",day:"numeric",year:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"}):"Not recorded"}
3221
+ function isLiveRun(run){return run.status==="running"||run.status==="pending"}
3222
+ function runStartedAt(run){return run.startedAt||run.createdAt}
3223
+ function runDurationMs(run){
3224
+ const start=new Date(runStartedAt(run)).getTime();if(!Number.isFinite(start))return NaN;
3225
+ const finished=run.completedAt?new Date(run.completedAt).getTime():isLiveRun(run)?Date.now():NaN;
3226
+ return Number.isFinite(finished)?finished-start:NaN;
3227
+ }
3228
+ /* A relative timestamp that the one-second ticker keeps current without a re-render. */
3229
+ function relCell(iso){return iso?'<time class="rel" data-rel="'+attr(iso)+'" title="'+attr(fullDate(iso))+'">'+esc(relTime(iso))+'</time>':'<span class="muted">—</span>'}
3230
+ function durCell(run){
3231
+ const live=isLiveRun(run)&&Boolean(runStartedAt(run));
3232
+ const text=durationText(runDurationMs(run));
3233
+ return'<span class="dur'+(live?" live":"")+'"'+(live?' data-since="'+attr(runStartedAt(run))+'"':"")+' title="'+attr(live?"Still running":"Total run duration")+'">'+esc(text)+'</span>';
3234
+ }
3235
+ function tickTimes(){
3236
+ document.querySelectorAll("[data-rel]").forEach(node=>{const next=relTime(node.dataset.rel);if(node.textContent!==next)node.textContent=next});
3237
+ document.querySelectorAll("[data-since]").forEach(node=>{const started=new Date(node.dataset.since).getTime();const next=Number.isFinite(started)?durationText(Date.now()-started):"—";if(node.textContent!==next)node.textContent=next});
3238
+ }
3239
+
3240
+ /* ---- Copy. Ids are truncated for scanning, so every one of them stays retrievable. ---- */
3241
+ function copyButton(value,label){return'<button class="copy-btn" type="button" data-copy="'+attr(value)+'" title="Copy '+attr(label||value)+'" aria-label="Copy '+attr(label||value)+'">'+icon("copy")+icon("check","icon copy-ok")+'</button>'}
3242
+ function idCell(value,extraClass){return'<span class="id-cell '+(extraClass||"")+'"><span class="mono">'+esc(short(value))+'</span>'+copyButton(value)+'</span>'}
3243
+ async function copyValue(button){
3244
+ const value=button.dataset.copy;
3245
+ try{
3246
+ if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(value)}
3247
+ else{
3248
+ const area=document.createElement("textarea");area.value=value;area.setAttribute("readonly","");
3249
+ area.style.position="fixed";area.style.top="-1000px";area.style.opacity="0";
3250
+ document.body.appendChild(area);area.select();document.execCommand("copy");area.remove();
3251
+ }
3252
+ button.classList.add("copied");setTimeout(()=>button.classList.remove("copied"),1200);
3253
+ toast("Copied "+short(value));
3254
+ }catch{toast("Could not copy to the clipboard")}
3255
+ }
3256
+
3257
+ /* ---- View state. Live updates repaint the page, so the parts a person is
3258
+ actively using -- scroll, focus, caret, expanded rows -- survive it. ---- */
3259
+ function captureView(){
3260
+ const active=document.activeElement;
3261
+ return{
3262
+ scroll:window.scrollY,
3263
+ focus:active&&active.id?active.id:null,
3264
+ caret:active&&typeof active.selectionStart==="number"?active.selectionStart:null,
3265
+ open:[...document.querySelectorAll("[data-event-key].open")].map(node=>node.dataset.eventKey),
3266
+ details:[...document.querySelectorAll("details[data-persist]")].filter(node=>node.open).map(node=>node.dataset.persist)
3267
+ };
3268
+ }
3269
+ function restoreView(view){
3270
+ if(!view)return;
3271
+ if(view.open.length)document.querySelectorAll("[data-event-key]").forEach(node=>{if(view.open.includes(node.dataset.eventKey))node.classList.add("open")});
3272
+ if(view.details.length)document.querySelectorAll("details[data-persist]").forEach(node=>{if(view.details.includes(node.dataset.persist))node.open=true});
3273
+ if(view.focus){
3274
+ const node=$(view.focus);
3275
+ if(node&&node!==document.activeElement){
3276
+ node.focus({preventScroll:true});
3277
+ if(view.caret!==null&&typeof node.setSelectionRange==="function"){try{node.setSelectionRange(view.caret,view.caret)}catch{}}
3278
+ }
3279
+ }
3280
+ if(view.scroll)window.scrollTo(0,view.scroll);
3281
+ }
3282
+ function skeleton(rows){
3283
+ let html="";for(let index=0;index<(rows||5);index++)html+='<div class="skeleton-row"><span style="width:'+(46+index*7%34)+'%"></span><span style="width:'+(16+index*5%14)+'%"></span></div>';
3284
+ return'<div class="skeleton" aria-busy="true" aria-label="Loading">'+html+'</div>';
3285
+ }
3286
+
3188
3287
  async function api(url,options){
3189
3288
  const response=await fetch(url,options);let body;
3190
3289
  try{body=await response.json()}catch{body={error:"The runtime returned an unreadable response."}}
@@ -3202,6 +3301,31 @@ function metric(label,value,note,accent){return'<section class="card metric '+(a
3202
3301
  function dataLinks(){
3203
3302
  document.querySelectorAll("[data-link]").forEach(node=>node.onclick=event=>{if(event.metaKey||event.ctrlKey||event.shiftKey||event.altKey)return;event.preventDefault();go(node.getAttribute("href"))});
3204
3303
  document.querySelectorAll("[data-go]").forEach(node=>node.onclick=event=>{if(event.target.closest("a,button"))return;go(node.dataset.go)});
3304
+ document.querySelectorAll("[data-copy]").forEach(node=>node.onclick=event=>{event.preventDefault();event.stopPropagation();void copyValue(node)});
3305
+ syncCursor();
3306
+ }
3307
+ /* ---- Row cursor. j/k walk a list and Enter opens it, without reaching for the mouse. ---- */
3308
+ function cursorRows(){return[...document.querySelectorAll("[data-go]")]}
3309
+ function syncCursor(){
3310
+ const rows=cursorRows();
3311
+ if(state.cursor>=rows.length)state.cursor=rows.length-1;
3312
+ rows.forEach((row,index)=>row.classList.toggle("cursor",index===state.cursor));
3313
+ }
3314
+ function moveCursor(delta){
3315
+ const rows=cursorRows();if(!rows.length)return;
3316
+ state.cursor=state.cursor<0?(delta>0?0:rows.length-1):Math.min(rows.length-1,Math.max(0,state.cursor+delta));
3317
+ syncCursor();
3318
+ const row=rows[state.cursor];
3319
+ if(row)row.scrollIntoView({block:"nearest"});
3320
+ }
3321
+ function openCursor(){
3322
+ const row=cursorRows()[state.cursor];
3323
+ if(row)go(row.dataset.go);
3324
+ }
3325
+ function isTyping(){
3326
+ const node=document.activeElement;if(!node)return false;
3327
+ const tag=node.tagName;
3328
+ return tag==="INPUT"||tag==="TEXTAREA"||tag==="SELECT"||node.isContentEditable;
3205
3329
  }
3206
3330
  function workspaceIds(){const ids=new Set();state.instances.forEach(x=>ids.add(x.workspaceId));state.subscriptions.forEach(x=>ids.add(x.workspaceId));state.connections.forEach(x=>ids.add(x.workspaceId));state.runs.forEach(x=>{if(x.workspaceId)ids.add(x.workspaceId)});return[...ids].filter(Boolean).sort()}
3207
3331
  function definition(id){return state.manifest?.agents?.agents?.find(x=>x.id===id)}
@@ -3233,8 +3357,8 @@ function renderOverview(){
3233
3357
  let html=pageHead("Runtime at a glance","What is happening now","Start here. Foundry separates code-defined agents from their runtime instances, then records every invocation as a run.",'<a class="button" href="/agents" data-link>Browse agents</a><button class="button primary" data-new-run>Start a run</button>');
3234
3358
  html+='<div class="grid cols-4">'+metric("Agent definitions",definitions.length,"Discovered from the agents folder",true)+metric("Runtime instances",state.instances.length,"Persisted agent identities")+metric("Active work",live.length,live.length?"Runs currently pending or running":"No work in flight")+metric("Attention needed",failureRuns().length,"Failed runs in retained history")+'</div>';
3235
3359
  html+='<div class="grid cols-2" style="margin-top:16px">';
3236
- const activity=recentRuns.map(run=>'<tr class="clickable" data-go="/runs/'+attr(run.id)+'"><td><span class="primary-cell">'+esc(run.agent)+'</span><span class="secondary">'+esc(short(run.id))+'</span></td><td>'+status(run.status)+'</td><td>'+esc(explainRun(run))+'</td><td>'+esc(fmtDate(run.createdAt))+'</td></tr>').join("");
3237
- html+=card("Recent runs",'<div class="table-wrap"><table class="table"><thead><tr><th>Agent</th><th>Status</th><th>Why it ran</th><th>Started</th></tr></thead><tbody>'+activity+'</tbody></table></div>'+(activity?"":empty("No runs yet","Start a run to see its progress and trace here.")),count(state.runs.length,"run"),"span-2");
3360
+ const activity=recentRuns.map(run=>'<tr class="clickable'+(run.status==="failed"?" row-failed":"")+(isLiveRun(run)?" row-live":"")+'" data-go="/runs/'+attr(run.id)+'"><td><span class="primary-cell">'+esc(run.agent)+'</span><span class="secondary">'+esc(explainRun(run))+'</span></td><td>'+status(run.status)+'</td><td>'+idCell(run.id)+'</td><td>'+durCell(run)+'</td><td>'+relCell(run.createdAt)+'</td></tr>').join("");
3361
+ html+=card("Recent runs",'<div class="table-wrap"><table class="table"><thead><tr><th>Agent</th><th>Status</th><th>Run</th><th>Duration</th><th>Started</th></tr></thead><tbody>'+activity+'</tbody></table></div>'+(activity?"":(state.ready?empty("No runs yet","Start a run to see its progress and trace here."):skeleton(4)))+'<div class="card-foot"><a class="link inline-icon" href="/runs" data-link>View all runs '+icon("external")+'</a></div>',count(state.runs.length,"run"),"span-2");
3238
3362
  const map=definitions.slice(0,6).map(def=>{const instances=state.instances.filter(x=>x.definitionId===def.id);return'<tr class="clickable" data-go="/agents/'+attr(def.id)+'"><td><span class="primary-cell">'+esc(def.id)+'</span><span class="secondary">'+esc(def.description)+'</span></td><td>'+instances.length+'</td><td>'+esc((def.workingEnvironment?"Working environment · ":"")+(def.repl?def.repl+" REPL":"No REPL"))+'</td></tr>'}).join("");
3239
3363
  html+=card("Agent map",'<div class="table-wrap"><table class="table"><thead><tr><th>Definition</th><th>Instances</th><th>Mounted surface</th></tr></thead><tbody>'+map+'</tbody></table></div>',count(definitions.length,"definition"));
3240
3364
  const automationCount=state.activations.length+state.subscriptions.length+state.connections.length;
@@ -3247,7 +3371,7 @@ function renderAgents(){
3247
3371
  let html=pageHead("Code and runtime","Agents","A definition is the code-discovered recipe. An instance is persisted runtime data with its own context, apps, playbooks, and conversations.",'<button class="button primary" data-new-run>Start a run</button>');
3248
3372
  html+='<div class="callout"><span class="symbol">'+icon("definitions")+'</span><div><b>Definitions and instances are intentionally separate.</b><p>Editing a file changes what can be assembled. Updating an instance changes what one runtime identity actually has installed.</p></div></div>';
3249
3373
  html+='<div class="definition-grid">'+definitions.map(def=>{const instances=state.instances.filter(x=>x.definitionId===def.id);const features=[def.workingEnvironment?"VFS":null,def.repl?def.repl+" repl":null,def.mesh?"mesh":null].filter(Boolean);return'<a class="definition-card" href="/agents/'+attr(def.id)+'" data-link><span class="definition-icon">'+icon("agent")+'</span><span class="definition-open">'+icon("external")+'</span><h3>'+esc(def.id)+'</h3><p>'+esc(def.description)+'</p><div>'+tags(features)+'</div><div class="definition-meta"><b>'+count(instances.length,"instance")+'</b><span class="muted mono" style="font-size:9px">'+esc(def.file)+'</span></div></a>'}).join("")+'</div>';
3250
- const rows=recent(state.instances).map(instance=>'<tr class="clickable" data-go="/instances/'+attr(instance.id)+'"><td><span class="primary-cell">'+esc(short(instance.id))+'</span><span class="secondary">'+esc(instance.workspaceId)+'</span></td><td><a class="link" href="/agents/'+attr(instance.definitionId)+'" data-link>'+esc(instance.definitionId)+'</a></td><td>'+instance.installations.length+'</td><td>'+instance.playbooks.length+'</td><td>'+esc(fmtDate(instance.updatedAt))+'</td></tr>').join("");
3374
+ const rows=recent(state.instances).map(instance=>'<tr class="clickable" data-go="/instances/'+attr(instance.id)+'"><td>'+idCell(instance.id)+'<span class="secondary">'+esc(instance.workspaceId)+'</span></td><td><a class="link" href="/agents/'+attr(instance.definitionId)+'" data-link>'+esc(instance.definitionId)+'</a></td><td>'+instance.installations.length+'</td><td>'+instance.playbooks.length+'</td><td>'+relCell(instance.updatedAt)+'</td></tr>').join("");
3251
3375
  html+=card("Runtime instances",'<div class="table-wrap"><table class="table"><thead><tr><th>Instance</th><th>Definition</th><th>Installed</th><th>Playbooks</th><th>Updated</th></tr></thead><tbody>'+rows+'</tbody></table></div>'+(rows?"":empty("No runtime instances","Start a run or create an instance from a definition.")),count(state.instances.length,"instance"));
3252
3376
  $("content").innerHTML=html;document.querySelectorAll("[data-new-run]").forEach(x=>x.onclick=openRunDrawer);dataLinks();
3253
3377
  }
@@ -3258,7 +3382,7 @@ function renderDefinition(id){
3258
3382
  const instances=state.instances.filter(x=>x.definitionId===id);const caps=state.manifest?.definitions?.[id]?.capabilities||{tools:[],applications:[],mcp:[],memory:[]};const surfaces=state.manifest?.definitions?.[id]?.surfaces||{layers:[],subscribers:[]};
3259
3383
  let html=pageHead("Agent definition",id,def.description,'<button class="button" data-create-instance="'+attr(id)+'">Create instance</button><button class="button primary" data-new-run="'+attr(id)+'">Start a run</button>');
3260
3384
  html+='<div class="detail-strip"><div><label>File route</label><strong class="mono">'+esc(def.file)+'</strong></div><div><label>Assembly</label><strong>'+esc(def.assembly)+" · "+esc(def.handler)+' handler</strong></div><div><label>Runtime surfaces</label><strong>'+(def.workingEnvironment?"Working environment":"No working environment")+(def.repl?" · "+esc(def.repl)+" REPL":"")+'</strong></div><div><label>Lazy fields</label><strong>'+esc(def.lazy.length?def.lazy.join(", "):"None")+'</strong></div></div>';
3261
- const rows=instances.map(x=>'<tr class="clickable" data-go="/instances/'+attr(x.id)+'"><td><span class="primary-cell">'+esc(short(x.id))+'</span><span class="secondary">'+esc(x.workspaceId)+'</span></td><td>'+x.installations.length+'</td><td>'+x.playbooks.length+'</td><td>'+esc(fmtDate(x.updatedAt))+'</td></tr>').join("");
3385
+ const rows=instances.map(x=>'<tr class="clickable" data-go="/instances/'+attr(x.id)+'"><td>'+idCell(x.id)+'<span class="secondary">'+esc(x.workspaceId)+'</span></td><td>'+x.installations.length+'</td><td>'+x.playbooks.length+'</td><td>'+relCell(x.updatedAt)+'</td></tr>').join("");
3262
3386
  html+=card("Runtime instances",'<div class="table-wrap"><table class="table"><thead><tr><th>Instance</th><th>Installed</th><th>Playbooks</th><th>Updated</th></tr></thead><tbody>'+rows+'</tbody></table></div>'+(rows?"":empty("No instance exists","Create one now or let a playbook provision one when an inbound event matches.")),count(instances.length,"instance"));
3263
3387
  html+='<div class="grid cols-2">';
3264
3388
  const groups=[["Shared tools",caps.tools],["Applications",caps.applications],["MCP",caps.mcp],["Memory",caps.memory]];
@@ -3272,25 +3396,101 @@ function renderInstance(id){
3272
3396
  const instance=state.instances.find(x=>x.id===id);if(!instance)return renderNotFound("Agent instance");
3273
3397
  crumbs([{label:"Agents",href:"/agents"},{label:instance.definitionId,href:"/agents/"+instance.definitionId},{label:short(id)}]);
3274
3398
  const conversations=state.conversations[id]||[];const runs=recent(state.runs.filter(x=>x.agentId===id));
3275
- let html=pageHead("Runtime instance",short(id),"Persisted runtime data assembled from "+instance.definitionId+". This identity can be updated without changing the code definition.",'<button class="button primary" data-instance-run>Send message</button>');
3276
- html+='<div class="detail-strip"><div><label>Definition</label><strong><a class="link" href="/agents/'+attr(instance.definitionId)+'" data-link>'+esc(instance.definitionId)+'</a></strong></div><div><label>Workspace</label><strong><a class="link mono" href="/workspaces/'+attr(instance.workspaceId)+'" data-link>'+esc(instance.workspaceId)+'</a></strong></div><div><label>Conversations</label><strong>'+conversations.length+'</strong></div><div><label>Last updated</label><strong>'+esc(fmtDate(instance.updatedAt))+'</strong></div></div>';
3399
+ let html=pageHead("Runtime instance",short(id),"Persisted runtime data assembled from "+instance.definitionId+". This identity can be updated without changing the code definition.",'<button class="button" data-copy="'+attr(id)+'">'+icon("copy")+'<span>Copy instance id</span></button><button class="button primary" data-instance-run>Send message</button>');
3400
+ html+='<div class="detail-strip"><div><label>Definition</label><strong><a class="link" href="/agents/'+attr(instance.definitionId)+'" data-link>'+esc(instance.definitionId)+'</a></strong></div><div><label>Workspace</label><strong><a class="link mono" href="/workspaces/'+attr(instance.workspaceId)+'" data-link>'+esc(instance.workspaceId)+'</a></strong></div><div><label>Conversations</label><strong>'+conversations.length+'</strong></div><div><label>Last updated</label><strong>'+relCell(instance.updatedAt)+'</strong></div></div>';
3277
3401
  html+='<div class="grid cols-2">';
3278
- const convRows=recent(conversations).map(c=>'<tr><td><span class="primary-cell">'+esc(c.title||"Untitled conversation")+'</span><span class="secondary">'+esc(short(c.id))+'</span></td><td>'+esc(fmtDate(c.updatedAt))+'</td></tr>').join("");
3402
+ const convRows=recent(conversations).map(c=>'<tr><td><span class="primary-cell">'+esc(c.title||"Untitled conversation")+'</span>'+idCell(c.id)+'</td><td>'+relCell(c.updatedAt)+'</td></tr>').join("");
3279
3403
  html+=card("Conversations",'<div class="table-wrap"><table class="table"><thead><tr><th>Conversation</th><th>Updated</th></tr></thead><tbody>'+convRows+'</tbody></table></div>'+(convRows?"":empty("No conversations","The first message creates a conversation for this instance.")),count(conversations.length,"conversation"));
3280
- const runRows=runs.slice(0,8).map(run=>'<tr class="clickable" data-go="/runs/'+attr(run.id)+'"><td><span class="primary-cell">'+esc(short(run.id))+'</span><span class="secondary">'+esc(explainRun(run))+'</span></td><td>'+status(run.status)+'</td><td>'+esc(fmtDate(run.createdAt))+'</td></tr>').join("");
3281
- html+=card("Recent runs",'<div class="table-wrap"><table class="table"><thead><tr><th>Run</th><th>Status</th><th>Started</th></tr></thead><tbody>'+runRows+'</tbody></table></div>'+(runRows?"":empty("No runs","Send this instance a message to begin.")),count(runs.length,"run"));
3404
+ const runRows=runs.slice(0,8).map(run=>'<tr class="clickable'+(run.status==="failed"?" row-failed":"")+(isLiveRun(run)?" row-live":"")+'" data-go="/runs/'+attr(run.id)+'"><td>'+idCell(run.id)+'<span class="secondary">'+esc(explainRun(run))+'</span></td><td>'+status(run.status)+'</td><td>'+durCell(run)+'</td><td>'+relCell(run.createdAt)+'</td></tr>').join("");
3405
+ html+=card("Recent runs",'<div class="table-wrap"><table class="table"><thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Started</th></tr></thead><tbody>'+runRows+'</tbody></table></div>'+(runRows?"":empty("No runs","Send this instance a message to begin."))+(runs.length>8?'<div class="card-foot"><a class="link inline-icon" href="/runs?q='+encodeURIComponent(instance.id)+'" data-link>View all '+count(runs.length,"run")+' '+icon("external")+'</a></div>':""),count(runs.length,"run"));
3282
3406
  html+=card("Installed capabilities",'<div class="card-body">'+(instance.installations.length?instance.installations.map(x=>'<div class="cap">'+esc(x.kind)+" · "+esc(x.id)+(x.accountId?" · account "+esc(x.accountId):"")+'</div>').join(""):empty("Nothing installed","Applications, MCP servers, and shared tools are instance data."))+'</div>',count(instance.installations.length,"installation"));
3283
3407
  html+=card("Playbooks and context",'<div class="card-body"><div class="summary-box"><dl><dt>Playbooks</dt><dd>'+esc(instance.playbooks.map(x=>x.name||x.id).join(", ")||"None")+'</dd><dt>Context keys</dt><dd>'+esc(Object.keys(instance.context||{}).join(", ")||"None")+'</dd><dt>Provisioning key</dt><dd class="mono">'+esc(instance.provisioningKey||"Directly provisioned")+'</dd></dl></div><details><summary class="link">View stored instance data</summary><pre class="json">'+esc(jsonText(instance))+'</pre></details></div>',"Persisted data");
3284
3408
  html+='</div>';$("content").innerHTML=html;document.querySelector("[data-instance-run]").onclick=()=>openRunDrawer(instance.definitionId,id);dataLinks();
3285
3409
  }
3286
3410
 
3411
+ /* ---- Run filters live in the URL, so a filtered view is a link you can send. ---- */
3412
+ function runQuery(){
3413
+ const params=new URLSearchParams(location.search);
3414
+ return{status:params.get("status")||"all",source:params.get("source")||"all",q:params.get("q")||""};
3415
+ }
3416
+ function setRunQuery(patch){
3417
+ const next=Object.assign(runQuery(),patch);
3418
+ const params=new URLSearchParams();
3419
+ if(next.status!=="all")params.set("status",next.status);
3420
+ if(next.source!=="all")params.set("source",next.source);
3421
+ if(next.q)params.set("q",next.q);
3422
+ const search=params.toString();
3423
+ history.replaceState({},"",location.pathname+(search?"?"+search:""));
3424
+ return next;
3425
+ }
3426
+ function runSource(run){return run.input?.source?.kind||"direct"}
3427
+ function matchesStatus(run,status){
3428
+ if(status==="all")return true;
3429
+ if(status==="running")return isLiveRun(run);
3430
+ return run.status===status;
3431
+ }
3432
+ function filterRuns(runs,query){
3433
+ const term=query.q.trim().toLowerCase();
3434
+ return runs.filter(run=>{
3435
+ if(!matchesStatus(run,query.status))return false;
3436
+ if(query.source!=="all"&&runSource(run)!==query.source)return false;
3437
+ if(!term)return true;
3438
+ return(run.agent+" "+run.id+" "+(run.agentId||"")+" "+jsonText(run.input)).toLowerCase().includes(term);
3439
+ });
3440
+ }
3441
+ function statusSegments(runs,active){
3442
+ const options=[["all","All"],["running","Running"],["completed","Completed"],["failed","Failed"],["cancelled","Cancelled"]];
3443
+ return'<div class="segmented" role="tablist" aria-label="Filter runs by status">'+options.map(option=>{
3444
+ const total=runs.filter(run=>matchesStatus(run,option[0])).length;
3445
+ const on=active===option[0];
3446
+ return'<button type="button" role="tab" aria-selected="'+(on?"true":"false")+'" class="'+(on?"active":"")+'" data-status="'+attr(option[0])+'">'+esc(option[1])+'<span class="seg-count">'+total+'</span></button>';
3447
+ }).join("")+'</div>';
3448
+ }
3449
+ function runRow(run,showAgent){
3450
+ const failed=run.status==="failed";
3451
+ return'<tr class="clickable run-row'+(failed?" row-failed":"")+(isLiveRun(run)?" row-live":"")+'" data-go="/runs/'+attr(run.id)+'">'
3452
+ +(showAgent===false?"":'<td><span class="primary-cell">'+esc(run.agent)+'</span><span class="secondary">'+esc(explainRun(run))+'</span></td>')
3453
+ +'<td>'+status(run.status)+'</td>'
3454
+ +'<td>'+idCell(run.id)+'</td>'
3455
+ +'<td><span class="tag">'+esc(runSource(run))+'</span></td>'
3456
+ +'<td>'+durCell(run)+'</td>'
3457
+ +'<td>'+(run.attempts>1?'<span class="attempts warn">'+run.attempts+" / "+run.maxAttempts+'</span>':'<span class="attempts">'+run.attempts+" / "+run.maxAttempts+'</span>')+'</td>'
3458
+ +'<td>'+relCell(run.createdAt)+'</td></tr>';
3459
+ }
3287
3460
  function renderRuns(){
3288
- crumbs([{label:"Runs"}]);let runs=recent(state.runs);if(state.filters.runs!=="all")runs=runs.filter(x=>x.status===state.filters.runs);
3289
- let html=pageHead("Execution history","Runs","Each invocation has one status, one result, and a chronological trace. Open a run to follow assembly, model work, tools, and completion.",'<button class="button primary" data-new-run>Start a run</button>');
3290
- html+='<div class="filters"><input id="run-search" placeholder="Search agent, run id, or input…"><select id="run-filter"><option value="all">All statuses</option><option value="running">Running</option><option value="completed">Completed</option><option value="failed">Failed</option><option value="cancelled">Cancelled</option></select></div>';
3291
- const rows=runs.map(run=>'<tr class="clickable run-search-row" data-search="'+attr((run.agent+" "+run.id+" "+jsonText(run.input)).toLowerCase())+'" data-go="/runs/'+attr(run.id)+'"><td><span class="primary-cell">'+esc(run.agent)+'</span><span class="secondary">'+esc(short(run.id))+'</span></td><td>'+status(run.status)+'</td><td>'+esc(run.input?.source?.kind||"direct")+'</td><td>'+run.attempts+" / "+run.maxAttempts+'</td><td>'+esc(fmtDate(run.createdAt))+'</td></tr>').join("");
3292
- html+=card("All runs",'<div class="table-wrap"><table class="table"><thead><tr><th>Agent / run</th><th>Status</th><th>Source</th><th>Attempts</th><th>Started</th></tr></thead><tbody>'+rows+'</tbody></table></div>'+(rows?"":empty("No matching runs","Change the filter or start a new run.")),count(runs.length,"run"));
3293
- $("content").innerHTML=html;$("run-filter").value=state.filters.runs;$("run-filter").onchange=e=>{state.filters.runs=e.target.value;renderRuns()};$("run-search").oninput=e=>{const term=e.target.value.toLowerCase();document.querySelectorAll(".run-search-row").forEach(row=>row.classList.toggle("hidden",!row.dataset.search.includes(term)))};document.querySelector("[data-new-run]").onclick=openRunDrawer;dataLinks();
3461
+ crumbs([{label:"Runs"}]);
3462
+ const query=runQuery();const all=recent(state.runs);const runs=filterRuns(all,query);
3463
+ const sources=[...new Set(all.map(runSource))].sort();
3464
+ const live=all.filter(isLiveRun).length;
3465
+ let html=pageHead("Execution history","Runs","Each invocation has one status, one result, and a chronological trace. Filters stay in the address bar, so any view here is a link you can share.",'<button class="button" data-refresh>'+icon("refresh")+'<span>Refresh</span></button><button class="button primary" data-new-run>'+icon("plus")+'<span>Start a run</span></button>');
3466
+ html+='<div class="toolbar">'+statusSegments(all,query.status)
3467
+ +'<div class="toolbar-right">'
3468
+ +'<label class="search-field"><span class="search-field-icon">'+icon("search")+'</span><input id="run-search" value="'+attr(query.q)+'" placeholder="Search agent, run id, or input…" aria-label="Search runs" /></label>'
3469
+ +'<label class="select-field"><span class="select-field-icon">'+icon("funnel")+'</span><select id="run-source" aria-label="Filter runs by source"><option value="all">Any source</option>'+sources.map(kind=>'<option value="'+attr(kind)+'"'+(query.source===kind?" selected":"")+'>'+esc(kind)+'</option>').join("")+'</select></label>'
3470
+ +'</div></div>';
3471
+ const activeFilters=[query.status!=="all"?"status "+query.status:null,query.source!=="all"?"source "+query.source:null,query.q?'matching "'+query.q+'"':null].filter(Boolean);
3472
+ if(activeFilters.length)html+='<div class="filter-note"><span>Showing '+count(runs.length,"run")+' of '+all.length+' · '+esc(activeFilters.join(" · "))+'</span><button class="link" type="button" data-clear-filters>Clear filters</button></div>';
3473
+ const rows=runs.map(run=>runRow(run)).join("");
3474
+ const meta=count(runs.length,"run")+(live?" · "+live+" in flight":"");
3475
+ html+=card("All runs",'<div class="table-wrap"><table class="table runs-table"><thead><tr><th>Agent</th><th>Status</th><th>Run</th><th>Source</th><th>Duration</th><th>Attempts</th><th>Started</th></tr></thead><tbody>'+rows+'</tbody></table></div>'+(rows?"":(state.ready?empty(all.length?"No runs match these filters":"No runs yet",all.length?"Clear a filter or widen the search to see the rest.":"Start a run to watch it assemble, work, and complete here."):skeleton(5))),meta);
3476
+ html+='<p class="hint">'+esc("j / k to move · Enter to open · / to search · c to start a run · r to refresh")+'</p>';
3477
+ $("content").innerHTML=html;
3478
+ state.cursor=-1;
3479
+ document.querySelectorAll("[data-status]").forEach(node=>node.onclick=()=>{setRunQuery({status:node.dataset.status});renderRuns()});
3480
+ $("run-source").onchange=event=>{setRunQuery({source:event.target.value});renderRuns()};
3481
+ const search=$("run-search");
3482
+ search.oninput=event=>{
3483
+ const caret=event.target.selectionStart;
3484
+ setRunQuery({q:event.target.value});
3485
+ renderRuns();
3486
+ const next=$("run-search");
3487
+ if(next){next.focus();try{next.setSelectionRange(caret,caret)}catch{}}
3488
+ };
3489
+ const clear=document.querySelector("[data-clear-filters]");
3490
+ if(clear)clear.onclick=()=>{history.replaceState({},"",location.pathname);renderRuns()};
3491
+ document.querySelector("[data-new-run]").onclick=()=>openRunDrawer();
3492
+ document.querySelector("[data-refresh]").onclick=()=>void refreshActivity();
3493
+ dataLinks();
3294
3494
  }
3295
3495
 
3296
3496
  function phaseData(run,events){
@@ -3302,18 +3502,58 @@ function phaseData(run,events){
3302
3502
  {name:run.status==="failed"?"Failed":run.status==="cancelled"?"Cancelled":run.status==="completed"?"Completed":"In progress",detail:explainRun(run),state:run.status==="failed"||run.status==="cancelled"?"error":run.status==="completed"?"complete":"active",time:run.completedAt||time(/run\.(completed|failed|cancelled)/)}
3303
3503
  ];
3304
3504
  }
3505
+ function eventOffset(run,event){
3506
+ const base=new Date(run.createdAt).getTime();const at=new Date(event.timestamp).getTime();
3507
+ if(!Number.isFinite(base)||!Number.isFinite(at))return"";
3508
+ return"+"+durationText(Math.max(0,at-base));
3509
+ }
3510
+ function runErrorText(run){
3511
+ if(run.status!=="failed")return"";
3512
+ if(typeof run.error==="string")return run.error;
3513
+ if(run.error&&typeof run.error==="object")return run.error.message||jsonText(run.error);
3514
+ return"The runtime recorded a failure without a message.";
3515
+ }
3305
3516
  function renderRun(id){
3306
- const run=state.runs.find(x=>x.id===id);if(!run)return renderNotFound("Run");const events=eventsFor(id);
3307
- const visibleEvents=state.showAllEvents?events:events.filter(isKeyRunEvent);
3517
+ const run=state.runs.find(x=>x.id===id);if(!run)return renderNotFound("Run");
3518
+ const events=eventsFor(id);
3519
+ const categories=[...new Set(events.map(event=>event.category))].filter(Boolean).sort();
3520
+ let visibleEvents=state.showAllEvents?events:events.filter(isKeyRunEvent);
3521
+ if(state.eventCategory!=="all")visibleEvents=visibleEvents.filter(event=>event.category===state.eventCategory);
3308
3522
  crumbs([{label:"Runs",href:"/runs"},{label:short(id)}]);
3309
- let html=pageHead("Run detail",run.agent+" · "+short(id),"Follow the high-level phases first. Expand individual events only when you need raw adapter or model evidence.",run.status==="running"||run.status==="pending"?'<button class="button" data-cancel>Cancel run</button>':"");
3310
- html+='<div class="detail-strip"><div><label>Status</label><strong>'+status(run.status)+'</strong></div><div><label>Source</label><strong>'+esc(run.input?.source?.kind||"direct")+'</strong></div><div><label>Agent instance</label><strong>'+(run.agentId?'<a class="link mono" href="/instances/'+attr(run.agentId)+'" data-link>'+esc(short(run.agentId))+'</a>':"Not recorded")+'</strong></div><div><label>Attempts</label><strong>'+run.attempts+" of "+run.maxAttempts+'</strong></div></div>';
3311
- html+='<div class="grid cols-3"><section class="card span-2"><div class="card-head"><h2>Run spine</h2><span class="meta">'+count(events.length,"event")+'</span></div><div class="card-body"><div class="trace-note">This shows observable work intent and outcomes. It does not expose private hidden chain-of-thought.</div><div class="run-spine">'+phaseData(run,events).map(p=>'<div class="phase '+p.state+'"><span class="phase-dot"></span><div><strong>'+esc(p.name)+'</strong><p>'+esc(p.detail)+'</p></div><time>'+esc(fmtTime(p.time))+'</time></div>').join("")+'</div></div></section>';
3312
- html+=card("Result",'<div class="card-body"><div class="summary-box"><dl><dt>Created</dt><dd>'+esc(fmtDate(run.createdAt))+'</dd><dt>Started</dt><dd>'+esc(fmtDate(run.startedAt))+'</dd><dt>Finished</dt><dd>'+esc(fmtDate(run.completedAt))+'</dd><dt>Timeout</dt><dd>'+Math.round(run.timeoutMs/1000)+' seconds</dd></dl></div><details open><summary class="link">Output</summary><pre class="json">'+esc(jsonText(run.output??run.error??null))+'</pre></details><details><summary class="link">Input</summary><pre class="json">'+esc(jsonText(run.input))+'</pre></details></div>',"Recorded outcome");html+='</div>';
3313
- const eventRows=visibleEvents.map(event=>'<div class="event-row"><button class="event-toggle"><span class="event-time">'+esc(fmtTime(event.timestamp))+'</span><span class="event-category">'+esc(event.category)+'</span><span class="event-type">'+esc(event.type)+'</span><span>⌄</span></button><div class="event-detail"><pre class="json">'+esc(jsonText(event.data))+'</pre></div></div>').join("");
3314
- const eventControl='<div class="card-body" style="display:flex;align-items:center;gap:12px;padding-top:11px;padding-bottom:11px"><span class="muted" style="font-size:11px">'+(state.showAllEvents?"Showing every retained event, including assembly and process detail.":"Showing the lifecycle events that explain this run. Expand one for raw evidence.")+'</span><button class="button" style="margin-left:auto" data-toggle-events>'+(state.showAllEvents?"Show key events":"Show all "+events.length+" events")+'</button></div>';
3315
- html+=card("Observable event trace",eventControl+(eventRows||empty("No key events retained","Show all events to inspect runtime logs.")),count(visibleEvents.length,"event"));
3316
- $("content").innerHTML=html;document.querySelectorAll(".event-toggle").forEach(x=>x.onclick=()=>x.parentElement.classList.toggle("open"));document.querySelector("[data-toggle-events]").onclick=()=>{state.showAllEvents=!state.showAllEvents;renderRun(id)};const cancel=document.querySelector("[data-cancel]");if(cancel)cancel.onclick=()=>void cancelRun(id);dataLinks();
3523
+ const actions=(isLiveRun(run)?'<button class="button danger" data-cancel>Cancel run</button>':"")
3524
+ +'<button class="button" data-copy="'+attr(run.id)+'">'+icon("copy")+'<span>Copy run id</span></button>'
3525
+ +'<button class="button primary" data-rerun>'+icon("refresh")+'<span>Run again</span></button>';
3526
+ let html=pageHead("Run detail",run.agent+" · "+short(id),"Follow the high-level phases first. Expand individual events only when you need raw adapter or model evidence.",actions);
3527
+ const failure=runErrorText(run);
3528
+ if(failure)html+='<div class="alert danger"><span class="symbol">'+icon("warning")+'</span><div><b>This run failed'+(run.attempts>1?" after "+count(run.attempts,"attempt"):"")+'.</b><p>'+esc(failure)+'</p></div></div>';
3529
+ if(isLiveRun(run))html+='<div class="alert live"><span class="symbol"><i class="pulse-dot"></i></span><div><b>This run is still '+esc(run.status)+'.</b><p>New events stream in below as the runtime records them.</p></div></div>';
3530
+ html+='<div class="detail-strip">'
3531
+ +'<div><label>Status</label><strong>'+status(run.status)+'</strong></div>'
3532
+ +'<div><label>Duration</label><strong>'+durCell(run)+'</strong></div>'
3533
+ +'<div><label>Source</label><strong>'+esc(runSource(run))+'</strong></div>'
3534
+ +'<div><label>Agent instance</label><strong>'+(run.agentId?'<a class="link mono" href="/instances/'+attr(run.agentId)+'" data-link>'+esc(short(run.agentId))+'</a>':"Not recorded")+'</strong></div>'
3535
+ +'<div><label>Attempts</label><strong>'+run.attempts+" of "+run.maxAttempts+'</strong></div>'
3536
+ +'</div>';
3537
+ html+='<div class="grid cols-3"><section class="card span-2"><div class="card-head"><h2>Run spine</h2><span class="meta">'+count(events.length,"event")+'</span></div><div class="card-body"><div class="trace-note">This shows observable work intent and outcomes. It does not expose private hidden chain-of-thought.</div><div class="run-spine">'+phaseData(run,events).map(p=>'<div class="phase '+p.state+'"><span class="phase-dot"></span><div><strong>'+esc(p.name)+'</strong><p>'+esc(p.detail)+'</p></div><time title="'+attr(fullDate(p.time))+'">'+esc(fmtTime(p.time))+'</time></div>').join("")+'</div></div></section>';
3538
+ html+=card("Result",'<div class="card-body"><div class="summary-box"><dl><dt>Created</dt><dd>'+relCell(run.createdAt)+'</dd><dt>Started</dt><dd>'+relCell(run.startedAt)+'</dd><dt>Finished</dt><dd>'+relCell(run.completedAt)+'</dd><dt>Timeout</dt><dd>'+esc(durationText(run.timeoutMs))+'</dd></dl></div><details data-persist="output" open><summary class="link">Output</summary><pre class="json">'+esc(jsonText(run.output??run.error??null))+'</pre></details><details data-persist="input"><summary class="link">Input</summary><pre class="json">'+esc(jsonText(run.input))+'</pre></details></div>',"Recorded outcome");
3539
+ html+='</div>';
3540
+ const eventRows=visibleEvents.map((event,index)=>{
3541
+ const key=event.id||event.type+":"+event.timestamp+":"+index;
3542
+ return'<div class="event-row" data-event-key="'+attr(key)+'">'
3543
+ +'<button class="event-toggle" type="button"><span class="event-time">'+esc(fmtTime(event.timestamp))+'</span><span class="event-offset mono">'+esc(eventOffset(run,event))+'</span><span class="event-category">'+esc(event.category)+'</span><span class="event-type">'+esc(event.type)+'</span><span class="event-caret">'+icon("caretDown")+'</span></button>'
3544
+ +'<div class="event-detail"><div class="event-detail-head"><span class="muted mono">'+esc(event.type)+'</span>'+copyButton(jsonText(event.data),"event payload")+'</div><pre class="json">'+esc(jsonText(event.data))+'</pre></div></div>';
3545
+ }).join("");
3546
+ const categoryChips=categories.length>1?'<div class="chips">'+['all'].concat(categories).map(name=>'<button type="button" class="chip'+(state.eventCategory===name?" active":"")+'" data-category="'+attr(name)+'">'+esc(name==="all"?"All categories":name)+'</button>').join("")+'</div>':"";
3547
+ const eventControl='<div class="event-controls">'+categoryChips+'<div class="event-controls-right"><span class="muted">'+esc(state.showAllEvents?"Showing every retained event, including assembly and process detail.":"Showing the lifecycle events that explain this run.")+'</span><button class="button" type="button" data-toggle-events>'+esc(state.showAllEvents?"Show key events":"Show all "+events.length+" events")+'</button></div></div>';
3548
+ html+=card("Observable event trace",eventControl+'<div class="event-list">'+(eventRows||empty("No events to show","Clear the category filter or show every retained event."))+'</div>',count(visibleEvents.length,"event"));
3549
+ $("content").innerHTML=html;
3550
+ document.querySelectorAll(".event-toggle").forEach(node=>node.onclick=()=>node.parentElement.classList.toggle("open"));
3551
+ document.querySelectorAll("[data-category]").forEach(node=>node.onclick=()=>{state.eventCategory=node.dataset.category;renderRun(id)});
3552
+ document.querySelector("[data-toggle-events]").onclick=()=>{state.showAllEvents=!state.showAllEvents;renderRun(id)};
3553
+ const cancel=document.querySelector("[data-cancel]");if(cancel)cancel.onclick=()=>void cancelRun(id);
3554
+ const rerun=document.querySelector("[data-rerun]");
3555
+ if(rerun)rerun.onclick=()=>openRunDrawer(state.instances.find(x=>x.id===run.agentId)?.definitionId,run.agentId,typeof run.input?.message?.text==="string"?run.input.message.text:typeof run.input?.message==="string"?run.input.message:"");
3556
+ dataLinks();
3317
3557
  }
3318
3558
 
3319
3559
  function renderAutomations(){
@@ -3323,7 +3563,7 @@ function renderAutomations(){
3323
3563
  html+=card("Schedules and sleeps",'<div class="table-wrap"><table class="table"><thead><tr><th>Activation</th><th>Agent</th><th>Status</th><th>Timing</th><th>Origin</th></tr></thead><tbody>'+actRows+'</tbody></table></div>'+(actRows?"":empty("No future activations","Agents create schedules and sleeps through Foundry utility tools.")),count(state.activations.length,"activation"));
3324
3564
  const subRows=state.subscriptions.map(x=>'<tr><td><span class="primary-cell">'+esc(x.playbook?.name||x.playbook?.id||x.id)+'</span><span class="secondary">'+esc(short(x.id))+'</span></td><td>'+status(x.enabled?"enabled":"disabled")+'</td><td>'+esc(x.targets.map(t=>t.definitionId+" · "+t.provisioning.mode).join(", "))+'</td><td>'+esc(x.workspaceId)+'</td></tr>').join("");
3325
3565
  html+=card("Playbook listeners",'<div class="table-wrap"><table class="table"><thead><tr><th>Playbook</th><th>State</th><th>Targets / provisioning</th><th>Workspace</th></tr></thead><tbody>'+subRows+'</tbody></table></div>'+(subRows?"":empty("No playbook subscriptions","Runtime-defined subscriptions appear when a frontend or adapter installs them.")),count(state.subscriptions.length,"subscription"));
3326
- const connRows=state.connections.map(x=>'<tr><td><span class="primary-cell">'+esc(x.applicationId)+'</span><span class="secondary">'+esc(x.connectionId)+'</span></td><td>'+status(x.status)+'</td><td>'+esc(x.definitionId)+'</td><td>'+x.routeIds.length+'</td><td>'+esc(fmtDate(x.lastEventAt))+'</td></tr>').join("");
3566
+ const connRows=state.connections.map(x=>'<tr><td><span class="primary-cell">'+esc(x.applicationId)+'</span><span class="secondary">'+esc(x.connectionId)+'</span></td><td>'+status(x.status)+'</td><td>'+esc(x.definitionId)+'</td><td>'+x.routeIds.length+'</td><td>'+relCell(x.lastEventAt)+'</td></tr>').join("");
3327
3567
  html+=card("Inbound application workers",'<div class="table-wrap"><table class="table"><thead><tr><th>Application / connection</th><th>Status</th><th>Definition</th><th>Routes</th><th>Last event</th></tr></thead><tbody>'+connRows+'</tbody></table></div>'+(connRows?"":empty("No inbound workers","A connection starts only when an installed app has an active inbound playbook.")),count(state.connections.length,"connection"));
3328
3568
  $("content").innerHTML=html;dataLinks();
3329
3569
  }
@@ -3348,16 +3588,32 @@ function renderWorkspaces(id){
3348
3588
  const data=state.workspaces[id]||{entries:[],inbox:[],tasks:[],environment:[]};const localInstances=state.instances.filter(x=>x.workspaceId===id);
3349
3589
  let html=pageHead("Shared data","Workspace · "+id,"Inspect collaboration state shared by this workspace. Secret environment values are intentionally never exposed.",'<select class="button" id="workspace-select">'+ids.map(x=>'<option value="'+attr(x)+'" '+(x===id?"selected":"")+'>'+esc(x)+'</option>').join("")+'</select>');
3350
3590
  html+='<div class="grid cols-4">'+metric("Agent instances",localInstances.length,"Runtime identities in this workspace",true)+metric("Shared entries",data.entries.length,"Documents and structured values")+metric("Inbox items",data.inbox.length,"Cross-agent and external handoffs")+metric("Open tasks",data.tasks.filter(x=>x.status==="open"||x.status==="in-progress").length,"Work still requiring action")+'</div>';
3351
- html+='<div class="workspace-tabs"><button data-tab="entries">Entries</button><button data-tab="inbox">Inbox</button><button data-tab="tasks">Tasks</button><button data-tab="environment">Environment</button></div><div id="workspace-panel"></div>';
3352
- $("content").innerHTML=html;$("workspace-select").onchange=e=>go("/workspaces/"+encodeURIComponent(e.target.value));document.querySelectorAll("[data-tab]").forEach(x=>x.onclick=()=>{state.workspaceTab=x.dataset.tab;renderWorkspacePanel(data)});renderWorkspacePanel(data);dataLinks();
3591
+ const counts={entries:data.entries.length,inbox:data.inbox.length,tasks:data.tasks.length,environment:data.environment.length};
3592
+ html+='<div class="workspace-tabs" role="tablist">'+["entries","inbox","tasks","environment"].map(tab=>'<button type="button" role="tab" data-tab="'+attr(tab)+'">'+esc(tab[0].toUpperCase()+tab.slice(1))+'<span class="seg-count">'+counts[tab]+'</span></button>').join("")+'</div><div id="workspace-panel"></div>';
3593
+ $("content").innerHTML=html;
3594
+ $("workspace-select").onchange=e=>go("/workspaces/"+encodeURIComponent(e.target.value));
3595
+ document.querySelectorAll("[data-tab]").forEach(x=>x.onclick=()=>{
3596
+ const params=new URLSearchParams(location.search);
3597
+ if(x.dataset.tab==="entries")params.delete("tab");else params.set("tab",x.dataset.tab);
3598
+ const search=params.toString();
3599
+ history.replaceState({},"",location.pathname+(search?"?"+search:""));
3600
+ renderWorkspacePanel(data);
3601
+ });
3602
+ renderWorkspacePanel(data);dataLinks();
3603
+ }
3604
+ function workspaceTab(){
3605
+ const tab=new URLSearchParams(location.search).get("tab")||"entries";
3606
+ return["entries","inbox","tasks","environment"].includes(tab)?tab:"entries";
3353
3607
  }
3354
3608
  function renderWorkspacePanel(data){
3355
- document.querySelectorAll("[data-tab]").forEach(x=>x.classList.toggle("active",x.dataset.tab===state.workspaceTab));const values=data[state.workspaceTab]||[];let body="";
3356
- if(state.workspaceTab==="entries")body=values.length?'<div class="kv">'+values.map(x=>'<div class="kv-item"><strong>'+esc(x.key)+'</strong><small>Updated '+esc(fmtDate(x.updatedAt))+'</small><div class="kv-value">'+esc(typeof x.value==="string"?x.value:jsonText(x.value))+'</div></div>').join("")+'</div>':empty("No shared entries","Agents can place documents and structured data into this workspace.");
3357
- if(state.workspaceTab==="inbox")body=values.length?'<div class="table-wrap"><table class="table"><thead><tr><th>Topic</th><th>Agent</th><th>Status</th><th>Updated</th></tr></thead><tbody>'+recent(values).map(x=>'<tr><td><span class="primary-cell">'+esc(x.topic)+'</span><span class="secondary">'+esc(short(x.id))+'</span></td><td>'+esc(short(x.agentId||"shared"))+'</td><td>'+status(x.status)+'</td><td>'+esc(fmtDate(x.updatedAt))+'</td></tr>').join("")+'</tbody></table></div>':empty("Inbox is clear","Shared handoffs and external requests will appear here.");
3358
- if(state.workspaceTab==="tasks")body=values.length?'<div class="table-wrap"><table class="table"><thead><tr><th>Task</th><th>Owner</th><th>Status</th><th>Updated</th></tr></thead><tbody>'+recent(values).map(x=>'<tr><td><span class="primary-cell">'+esc(x.title)+'</span><span class="secondary">'+esc(x.detail||short(x.id))+'</span></td><td>'+esc(short(x.agentId||"workspace"))+'</td><td>'+status(x.status)+'</td><td>'+esc(fmtDate(x.updatedAt))+'</td></tr>').join("")+'</tbody></table></div>':empty("No tasks","Agents can create shared tasks for work that spans conversations.");
3359
- if(state.workspaceTab==="environment")body='<div class="callout"><span class="symbol">'+icon("secure")+'</span><div><b>Only safe values are visible.</b><p>Credential material remains inside user-owned adapters and is never returned by this endpoint.</p></div></div>'+(values.length?'<div class="kv">'+values.map(x=>'<div class="kv-item"><strong>'+esc(x.key)+'</strong><small>'+esc(x.scope)+'</small><div class="kv-value">'+esc(typeof x.value==="string"?x.value:jsonText(x.value))+'</div></div>').join("")+'</div>':empty("No public environment values","Mount an environment adapter to expose non-secret context."));
3360
- $("workspace-panel").innerHTML=card(state.workspaceTab[0].toUpperCase()+state.workspaceTab.slice(1),'<div class="card-body">'+body+'</div>',count(values.length,"item"));
3609
+ const active=workspaceTab();
3610
+ document.querySelectorAll("[data-tab]").forEach(x=>{const on=x.dataset.tab===active;x.classList.toggle("active",on);x.setAttribute("aria-selected",on?"true":"false")});
3611
+ const values=data[active]||[];let body="";
3612
+ if(active==="entries")body=values.length?'<div class="kv">'+values.map(x=>'<div class="kv-item"><strong>'+esc(x.key)+'</strong><small>Updated '+relTime(x.updatedAt)+'</small><div class="kv-value">'+esc(typeof x.value==="string"?x.value:jsonText(x.value))+'</div></div>').join("")+'</div>':empty("No shared entries","Agents can place documents and structured data into this workspace.");
3613
+ if(active==="inbox")body=values.length?'<div class="table-wrap"><table class="table"><thead><tr><th>Topic</th><th>Agent</th><th>Status</th><th>Updated</th></tr></thead><tbody>'+recent(values).map(x=>'<tr><td><span class="primary-cell">'+esc(x.topic)+'</span><span class="secondary">'+esc(short(x.id))+'</span></td><td>'+esc(short(x.agentId||"shared"))+'</td><td>'+status(x.status)+'</td><td>'+relCell(x.updatedAt)+'</td></tr>').join("")+'</tbody></table></div>':empty("Inbox is clear","Shared handoffs and external requests will appear here.");
3614
+ if(active==="tasks")body=values.length?'<div class="table-wrap"><table class="table"><thead><tr><th>Task</th><th>Owner</th><th>Status</th><th>Updated</th></tr></thead><tbody>'+recent(values).map(x=>'<tr><td><span class="primary-cell">'+esc(x.title)+'</span><span class="secondary">'+esc(x.detail||short(x.id))+'</span></td><td>'+esc(short(x.agentId||"workspace"))+'</td><td>'+status(x.status)+'</td><td>'+relCell(x.updatedAt)+'</td></tr>').join("")+'</tbody></table></div>':empty("No tasks","Agents can create shared tasks for work that spans conversations.");
3615
+ if(active==="environment")body='<div class="callout"><span class="symbol">'+icon("secure")+'</span><div><b>Only safe values are visible.</b><p>Credential material remains inside user-owned adapters and is never returned by this endpoint.</p></div></div>'+(values.length?'<div class="kv">'+values.map(x=>'<div class="kv-item"><strong>'+esc(x.key)+'</strong><small>'+esc(x.scope)+'</small><div class="kv-value">'+esc(typeof x.value==="string"?x.value:jsonText(x.value))+'</div></div>').join("")+'</div>':empty("No public environment values","Mount an environment adapter to expose non-secret context."));
3616
+ $("workspace-panel").innerHTML=card(active[0].toUpperCase()+active.slice(1),'<div class="card-body">'+body+'</div>',count(values.length,"item"));
3361
3617
  }
3362
3618
 
3363
3619
  function renderNotFound(label){crumbs([{label:"Not found"}]);$("content").innerHTML=pageHead("Inspector",label+" not found","The requested runtime record does not exist or is no longer retained.",'<a class="button" href="/" data-link>Return to overview</a>');dataLinks()}
@@ -3365,28 +3621,71 @@ async function loadBase(){
3365
3621
  const endpoints=["/api/manifest","/api/agent-instances","/api/playbook-subscriptions","/api/application-connections","/api/runs","/api/events","/health","/api/transmissions","/api/accounts","/api/routes","/api/bindings","/api/activations"];
3366
3622
  const results=await Promise.all(endpoints.map(url=>api(url)));[state.manifest,state.instances,state.subscriptions,state.connections,state.runs,state.events,state.health,state.transmissions,state.accounts,state.routes,state.bindings,state.activations]=results;renderHealth();
3367
3623
  }
3624
+ /* Every observability event used to trigger a full reload and repaint, which
3625
+ made a busy run unreadable. Events are coalesced into one refresh instead,
3626
+ and the repaint restores whatever the operator was doing. */
3627
+ let refreshTimer=null;let refreshing=false;let refreshQueued=false;
3628
+ function scheduleRefresh(){
3629
+ markLive();
3630
+ if(refreshTimer)return;
3631
+ refreshTimer=setTimeout(()=>{refreshTimer=null;void refreshActivity()},350);
3632
+ }
3633
+ function markLive(){
3634
+ state.liveAt=Date.now();
3635
+ const chip=document.querySelector(".live-chip");
3636
+ if(!chip)return;
3637
+ chip.classList.remove("pulse");void chip.offsetWidth;chip.classList.add("pulse");
3638
+ }
3368
3639
  async function refreshActivity(){
3369
- try{const endpoints=["/api/agent-instances","/api/playbook-subscriptions","/api/application-connections","/api/runs","/api/events","/api/activations","/health"];const values=await Promise.all(endpoints.map(url=>api(url)));[state.instances,state.subscriptions,state.connections,state.runs,state.events,state.activations,state.health]=values;renderHealth();await navigate(false)}catch{$("event-state").textContent="Refresh paused"}
3640
+ if(refreshing){refreshQueued=true;return}
3641
+ refreshing=true;
3642
+ try{
3643
+ const endpoints=["/api/agent-instances","/api/playbook-subscriptions","/api/application-connections","/api/runs","/api/events","/api/activations","/health"];
3644
+ const values=await Promise.all(endpoints.map(url=>api(url)));
3645
+ [state.instances,state.subscriptions,state.connections,state.runs,state.events,state.activations,state.health]=values;
3646
+ renderHealth();
3647
+ $("event-state").textContent="Live updates";
3648
+ await navigate(false,true);
3649
+ }catch{$("event-state").textContent="Refresh paused"}
3650
+ finally{
3651
+ refreshing=false;
3652
+ if(refreshQueued){refreshQueued=false;scheduleRefresh()}
3653
+ }
3370
3654
  }
3371
3655
  function renderHealth(){const ok=Boolean(state.health?.ok);$("runtime-state").classList.toggle("bad",!ok);$("runtime-state").querySelector("span").textContent=ok?"Runtime healthy":"Runtime needs attention"}
3372
- async function navigate(load=true){
3656
+ async function navigate(load=true,preserve=false){
3373
3657
  const current=route();navState(current.section);
3658
+ const view=preserve?captureView():null;
3374
3659
  try{
3375
3660
  if(load&&current.section==="instances"&&current.id)await ensureConversations(current.id);
3376
3661
  if(load&&current.section==="workspaces"&&current.id)await ensureWorkspace(current.id);
3377
3662
  if(current.section==="overview")renderOverview();else if(current.section==="agents"&&!current.id)renderAgents();else if(current.section==="agents")renderDefinition(current.id);else if(current.section==="instances")renderInstance(current.id);else if(current.section==="runs"&&!current.id)renderRuns();else if(current.section==="runs")renderRun(current.id);else if(current.section==="automations")renderAutomations();else if(current.section==="integrations")renderIntegrations();else if(current.section==="workspaces")renderWorkspaces(current.id);else renderNotFound("Page");
3378
3663
  }catch(error){$("content").innerHTML='<div class="form-error">'+esc(error.message)+'</div>';console.error(error)}
3664
+ restoreView(view);
3665
+ tickTimes();
3379
3666
  }
3380
3667
 
3381
- function openRunDrawer(definitionId,instanceId){
3668
+ function openRunDrawer(definitionId,instanceId,message){
3382
3669
  const definitions=state.manifest?.agents?.agents||[];$("run-definition").innerHTML=definitions.map(x=>'<option value="'+attr(x.id)+'">'+esc(x.id)+'</option>').join("");
3383
- if(typeof definitionId==="string"&&definition(definitionId))$("run-definition").value=definitionId;updateInstanceOptions(instanceId);$("run-message").value="";$("run-form-error").classList.add("hidden");$("drawer").classList.add("open");setTimeout(()=>$("run-message").focus(),20);
3670
+ if(typeof definitionId==="string"&&definition(definitionId))$("run-definition").value=definitionId;
3671
+ updateInstanceOptions(instanceId);
3672
+ $("run-message").value=typeof message==="string"?message:"";
3673
+ $("run-form-error").classList.add("hidden");
3674
+ setRunSubmitting(false);
3675
+ $("drawer").classList.add("open");
3676
+ setTimeout(()=>{const box=$("run-message");box.focus();box.setSelectionRange(box.value.length,box.value.length)},20);
3677
+ }
3678
+ function setRunSubmitting(pending){
3679
+ const submit=$("run-submit");
3680
+ submit.disabled=pending;
3681
+ submit.classList.toggle("pending",pending);
3682
+ submit.textContent=pending?"Starting…":"Start run";
3384
3683
  }
3385
3684
  function closeRunDrawer(){$("drawer").classList.remove("open")}
3386
3685
  function updateInstanceOptions(selected){const definitionId=$("run-definition").value;const instances=state.instances.filter(x=>x.definitionId===definitionId);$("run-instance").innerHTML='<option value="">Create a new instance</option>'+instances.map(x=>'<option value="'+attr(x.id)+'">'+esc(short(x.id))+" · "+esc(x.workspaceId)+'</option>').join("");if(selected&&instances.some(x=>x.id===selected))$("run-instance").value=selected}
3387
3686
  async function createInstance(definitionId){try{const workspace=workspaceIds()[0]||"foundry-dashboard";const instance=await api("/api/agent-instances",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({definitionId,workspaceId:workspace})});state.instances=[...state.instances,instance];toast("Instance created");go("/instances/"+encodeURIComponent(instance.id))}catch(error){toast(error.message)}}
3388
3687
  async function startRun(event){
3389
- event.preventDefault();const errorNode=$("run-form-error");errorNode.classList.add("hidden");
3688
+ event.preventDefault();const errorNode=$("run-form-error");errorNode.classList.add("hidden");setRunSubmitting(true);
3390
3689
  try{
3391
3690
  const definitionId=$("run-definition").value;let instance=state.instances.find(x=>x.id===$("run-instance").value);
3392
3691
  if(!instance){instance=await api("/api/agent-instances",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({definitionId,workspaceId:workspaceIds()[0]||"foundry-dashboard"})});state.instances=[...state.instances,instance]}
@@ -3394,6 +3693,7 @@ async function startRun(event){
3394
3693
  if(!conversation){conversation=await api("/api/conversations",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({agentId:instance.id,title:"Foundry inspector"})});state.conversations[instance.id]=[conversation]}
3395
3694
  const run=await api("/api/conversations/"+encodeURIComponent(conversation.id)+"/messages",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({agentId:instance.id,message:$("run-message").value})});state.runs=[...state.runs,run];closeRunDrawer();toast("Run accepted");go("/runs/"+encodeURIComponent(run.id));
3396
3695
  }catch(error){errorNode.textContent=error.message;errorNode.classList.remove("hidden")}
3696
+ finally{setRunSubmitting(false)}
3397
3697
  }
3398
3698
  async function cancelRun(id){try{await api("/api/runs/"+encodeURIComponent(id)+"/cancel",{method:"POST"});toast("Cancellation requested");await refreshActivity()}catch(error){toast(error.message)}}
3399
3699
 
@@ -3404,8 +3704,30 @@ function searchItems(){
3404
3704
  function renderSearch(){const term=$("search-input").value.trim().toLowerCase();const matches=searchItems().filter(x=>!term||(x.label+" "+x.detail).toLowerCase().includes(term)).slice(0,12);$("search-results").innerHTML=matches.map(x=>'<a class="search-result" href="'+attr(x.path)+'" data-search-link><strong>'+esc(x.label)+'</strong><small>'+esc(x.detail)+'</small></a>').join("")||empty("No matches","Try an agent id, instance id, run id, or page name.");document.querySelectorAll("[data-search-link]").forEach(x=>x.onclick=e=>{e.preventDefault();closeSearch();go(x.getAttribute("href"))})}
3405
3705
  function openSearch(){$("search-modal").classList.add("open");$("search-input").value="";renderSearch();setTimeout(()=>$("search-input").focus(),20)}function closeSearch(){$("search-modal").classList.remove("open")}
3406
3706
 
3407
- $("new-run").onclick=()=>openRunDrawer();$("close-drawer").onclick=closeRunDrawer;$("cancel-run").onclick=closeRunDrawer;$("drawer").onclick=e=>{if(e.target===$("drawer"))closeRunDrawer()};$("run-definition").onchange=()=>updateInstanceOptions();$("run-form").onsubmit=startRun;$("open-search").onclick=openSearch;$("search-input").oninput=renderSearch;$("search-modal").onclick=e=>{if(e.target===$("search-modal"))closeSearch()};$("mobile-toggle").onclick=()=>$("sidebar").classList.toggle("open");window.onpopstate=()=>void navigate();document.addEventListener("keydown",e=>{if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==="k"){e.preventDefault();openSearch()}if(e.key==="Escape"){closeSearch();closeRunDrawer()}});
3408
- loadBase().then(async()=>{await navigate();const stream=new EventSource("/api/events");stream.onopen=()=>{$("event-state").textContent="Live updates"};stream.onmessage=()=>void refreshActivity();stream.onerror=()=>{$("event-state").textContent="Reconnecting…"};setInterval(()=>void refreshActivity(),10000)}).catch(error=>{$("content").innerHTML='<div class="form-error"><strong>Could not open Foundry.</strong><br>'+esc(error.message)+'</div>';$("runtime-state").classList.add("bad");$("runtime-state").querySelector("span").textContent="Runtime unavailable"});
3707
+ $("new-run").onclick=()=>openRunDrawer();$("close-drawer").onclick=closeRunDrawer;$("cancel-run").onclick=closeRunDrawer;$("drawer").onclick=e=>{if(e.target===$("drawer"))closeRunDrawer()};$("run-definition").onchange=()=>updateInstanceOptions();$("run-form").onsubmit=startRun;$("open-search").onclick=openSearch;$("search-input").oninput=renderSearch;$("search-modal").onclick=e=>{if(e.target===$("search-modal"))closeSearch()};$("mobile-toggle").onclick=()=>$("sidebar").classList.toggle("open");window.onpopstate=()=>void navigate();
3708
+ $("run-message").addEventListener("keydown",e=>{if((e.metaKey||e.ctrlKey)&&e.key==="Enter"){e.preventDefault();$("run-form").requestSubmit()}});
3709
+ document.addEventListener("keydown",e=>{
3710
+ if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==="k"){e.preventDefault();openSearch();return}
3711
+ if(e.key==="Escape"){closeSearch();closeRunDrawer();return}
3712
+ if(e.metaKey||e.ctrlKey||e.altKey||isTyping())return;
3713
+ if($("drawer").classList.contains("open")||$("search-modal").classList.contains("open"))return;
3714
+ if(e.key==="j"){e.preventDefault();moveCursor(1)}
3715
+ else if(e.key==="k"){e.preventDefault();moveCursor(-1)}
3716
+ else if(e.key==="Enter"||e.key==="o"){if(state.cursor>=0){e.preventDefault();openCursor()}}
3717
+ else if(e.key==="/"){const box=$("run-search");if(box){e.preventDefault();box.focus();box.select()}else{e.preventDefault();openSearch()}}
3718
+ else if(e.key==="c"){e.preventDefault();openRunDrawer()}
3719
+ else if(e.key==="r"){e.preventDefault();void refreshActivity()}
3720
+ });
3721
+ setInterval(tickTimes,1000);
3722
+ loadBase().then(async()=>{
3723
+ state.ready=true;
3724
+ await navigate();
3725
+ const stream=new EventSource("/api/events");
3726
+ stream.onopen=()=>{$("event-state").textContent="Live updates"};
3727
+ stream.onmessage=scheduleRefresh;
3728
+ stream.onerror=()=>{$("event-state").textContent="Reconnecting…"};
3729
+ setInterval(()=>void refreshActivity(),10000);
3730
+ }).catch(error=>{$("content").innerHTML='<div class="form-error"><strong>Could not open Foundry.</strong><br>'+esc(error.message)+'</div>';$("runtime-state").classList.add("bad");$("runtime-state").querySelector("span").textContent="Runtime unavailable"});
3409
3731
  `;
3410
3732
 
3411
3733
  // src/dashboard-styles.ts
@@ -3441,14 +3763,119 @@ button,input,textarea,select{font:inherit;color:inherit}button,a{touch-action:ma
3441
3763
  .status{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:4px 8px;background:var(--bg-surface-2);color:var(--text-secondary);font:8px var(--mono);text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;border:1px solid var(--border)}.status:before{content:"";width:5px;height:5px;border-radius:50%;background:var(--text-tertiary)}.status.completed,.status.connected,.status.active,.status.enabled,.status.resolved{background:rgba(140,196,148,.08);color:var(--success);border-color:rgba(140,196,148,.18)}.status.completed:before,.status.connected:before,.status.active:before,.status.enabled:before,.status.resolved:before{background:var(--success)}.status.running,.status.pending,.status.connecting,.status.reconnecting,.status.in-progress{background:rgba(131,179,226,.08);color:var(--network);border-color:rgba(131,179,226,.18)}.status.running:before,.status.pending:before,.status.connecting:before,.status.reconnecting:before,.status.in-progress:before{background:var(--network)}.status.failed,.status.cancelled,.status.disconnected,.status.dismissed{background:rgba(226,155,168,.08);color:var(--danger);border-color:rgba(226,155,168,.18)}.status.failed:before,.status.cancelled:before,.status.disconnected:before,.status.dismissed:before{background:var(--danger)}
3442
3764
  .tag{display:inline-block;border:1px solid var(--border);border-radius:4px;padding:3px 6px;font:8px var(--mono);color:var(--text-secondary);margin:2px 4px 2px 0;background:var(--bg-surface)}.link{color:var(--accent);text-decoration:none;font-weight:500}.link:hover{text-decoration:underline;text-decoration-color:var(--accent-line)}.inline-icon{display:inline-flex;align-items:center;gap:5px}.inline-icon .icon{width:12px;height:12px}.empty{padding:38px 22px;text-align:center;color:var(--text-secondary);font-size:12px;line-height:1.6}.empty strong{display:block;color:var(--text-primary);font-size:14px;font-weight:500;margin-bottom:5px}.filters{display:flex;gap:8px;margin-bottom:14px}.filters input,.filters select{background:var(--bg-elevated);border:1px solid var(--border);border-radius:7px;padding:10px 12px;font-size:12px}.filters input{min-width:280px}
3443
3765
  .definition-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin-bottom:16px}.definition-card{display:block;color:inherit;text-decoration:none;background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);padding:19px;min-height:196px;position:relative;overflow:hidden;transition:transform .2s ease,border-color .2s ease,background .2s}.definition-card:before{content:"";position:absolute;inset:0;opacity:0;background:radial-gradient(110% 80% at 100% 0%,var(--accent-dim),transparent 60%);transition:opacity .2s}.definition-card:hover{transform:translateY(-2px);border-color:var(--accent-line);background:var(--bg-surface)}.definition-card:hover:before{opacity:1}.definition-icon{width:31px;height:31px;display:grid;place-items:center;border:1px solid var(--accent-line);border-radius:8px;color:var(--accent);background:var(--accent-dim);margin-bottom:15px;position:relative}.definition-icon .icon{width:17px;height:17px}.definition-open{position:absolute;right:17px;top:17px;color:var(--text-tertiary);transition:color .18s,transform .18s}.definition-open .icon{width:14px;height:14px}.definition-card:hover .definition-open{color:var(--accent);transform:translate(1px,-1px)}.definition-card h3{margin:0;font-size:17px;font-weight:500;position:relative}.definition-card p{color:var(--text-secondary);font-size:12px;font-weight:300;line-height:1.6;margin:9px 0 18px;position:relative}.definition-meta{position:absolute;left:19px;right:19px;bottom:17px;display:flex;justify-content:space-between;align-items:end}.definition-meta b{font:9px var(--mono);color:var(--accent-soft)}
3444
- .cap-list{display:flex;flex-wrap:wrap;gap:6px}.cap{border:1px solid var(--border);background:var(--bg-surface);border-radius:5px;padding:7px 9px;font:9px var(--mono);margin-bottom:6px;color:var(--text-secondary)}.detail-strip{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid var(--border-subtle);border-radius:var(--radius);background:var(--bg-elevated);margin-bottom:14px;overflow:hidden}.detail-strip>div{padding:15px;border-right:1px solid var(--border-subtle);min-width:0}.detail-strip>div:last-child{border-right:0}.detail-strip label{display:block;color:var(--text-tertiary);font:8px var(--mono);text-transform:uppercase;letter-spacing:.1em}.detail-strip strong{display:block;margin-top:8px;font-size:12px;font-weight:500;overflow-wrap:anywhere}.json{margin:0;background:#070A09;color:var(--text-secondary);border:1px solid var(--border-subtle);border-radius:7px;padding:14px;font:9px/1.65 var(--mono);white-space:pre-wrap;overflow:auto;max-height:360px}.summary-box{font-size:12px;line-height:1.65;overflow-wrap:anywhere}.summary-box dl{display:grid;grid-template-columns:130px 1fr;gap:9px 14px;margin:0}.summary-box dt{color:var(--text-tertiary);font:8px var(--mono);text-transform:uppercase;letter-spacing:.08em}.summary-box dd{margin:0;color:var(--text-secondary)}
3766
+ .cap-list{display:flex;flex-wrap:wrap;gap:6px}.cap{border:1px solid var(--border);background:var(--bg-surface);border-radius:5px;padding:7px 9px;font:9px var(--mono);margin-bottom:6px;color:var(--text-secondary)}.detail-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));border:1px solid var(--border-subtle);border-radius:var(--radius);background:var(--bg-elevated);margin-bottom:14px;overflow:hidden}.detail-strip>div{padding:15px;border-right:1px solid var(--border-subtle);min-width:0}.detail-strip>div:last-child{border-right:0}.detail-strip label{display:block;color:var(--text-tertiary);font:8px var(--mono);text-transform:uppercase;letter-spacing:.1em}.detail-strip strong{display:block;margin-top:8px;font-size:12px;font-weight:500;overflow-wrap:anywhere}.json{margin:0;background:#070A09;color:var(--text-secondary);border:1px solid var(--border-subtle);border-radius:7px;padding:14px;font:9px/1.65 var(--mono);white-space:pre-wrap;overflow:auto;max-height:360px}.summary-box{font-size:12px;line-height:1.65;overflow-wrap:anywhere}.summary-box dl{display:grid;grid-template-columns:130px 1fr;gap:9px 14px;margin:0}.summary-box dt{color:var(--text-tertiary);font:8px var(--mono);text-transform:uppercase;letter-spacing:.08em}.summary-box dd{margin:0;color:var(--text-secondary)}
3445
3767
  .run-spine{position:relative;padding:4px 0}.run-spine:before{content:"";position:absolute;left:19px;top:14px;bottom:14px;width:1px;background:var(--border)}.phase{position:relative;display:grid;grid-template-columns:40px 1fr auto;gap:12px;padding:11px 4px}.phase-dot{width:12px;height:12px;border-radius:50%;background:var(--bg-elevated);border:2px solid var(--text-tertiary);margin:2px 0 0 13px;z-index:1}.phase.complete .phase-dot{border-color:var(--success);box-shadow:0 0 8px rgba(140,196,148,.2)}.phase.active .phase-dot{border-color:var(--network);box-shadow:0 0 0 5px rgba(131,179,226,.09)}.phase.error .phase-dot{border-color:var(--danger)}.phase strong{font-size:12px;font-weight:500}.phase p{margin:4px 0 0;color:var(--text-secondary);font-size:11px;font-weight:300;line-height:1.5}.phase time{font:8px var(--mono);color:var(--text-tertiary)}
3446
3768
  .event-row{border-top:1px solid var(--border-subtle)}.event-toggle{width:100%;border:0;background:transparent;padding:12px 15px;display:grid;grid-template-columns:82px 110px minmax(0,1fr) auto;gap:11px;text-align:left;align-items:center;cursor:pointer}.event-toggle:hover,.event-row.open .event-toggle{background:var(--accent-glow)}.event-time,.event-category{font:8px var(--mono);color:var(--text-tertiary)}.event-type{font:9px var(--mono);overflow:hidden;text-overflow:ellipsis}.event-detail{display:none;padding:0 15px 14px}.event-row.open .event-detail{display:block}.trace-note{border-left:2px solid var(--accent);padding:10px 12px;background:var(--accent-dim);font-size:11px;line-height:1.5;color:var(--text-secondary);margin-bottom:14px}
3447
3769
  .workspace-tabs{display:flex;gap:2px;border-bottom:1px solid var(--border-subtle);margin:20px 0 14px}.workspace-tabs button{border:0;background:transparent;padding:11px 13px;color:var(--text-secondary);font-size:11px;cursor:pointer;border-bottom:2px solid transparent}.workspace-tabs button.active{color:var(--accent);border-color:var(--accent);font-weight:500}.kv{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.kv-item{border:1px solid var(--border-subtle);border-radius:7px;padding:13px;background:var(--bg-surface)}.kv-item strong{font:10px var(--mono)}.kv-item small{display:block;color:var(--text-tertiary);margin-top:6px}.kv-value{margin-top:12px;color:var(--text-secondary);font-size:11px;line-height:1.5;overflow-wrap:anywhere}.callout{border:1px solid var(--accent-line);background:var(--accent-glow);border-radius:8px;padding:15px 17px;display:flex;align-items:flex-start;gap:12px;margin-bottom:14px}.callout b{font-size:12px;font-weight:500}.callout p{margin:4px 0 0;color:var(--text-secondary);font-size:11px;line-height:1.5}.callout .symbol{width:31px;height:31px;display:grid;place-items:center;flex:0 0 auto;border:1px solid var(--accent-line);border-radius:7px;color:var(--accent);background:var(--accent-dim)}.callout .symbol .icon{width:16px;height:16px}
3448
3770
  .drawer-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.62);backdrop-filter:blur(3px);z-index:40;display:none}.drawer-backdrop.open{display:block}.drawer{position:absolute;right:0;top:0;bottom:0;width:min(520px,100%);background:var(--bg-elevated);border-left:1px solid var(--border);box-shadow:-20px 0 60px rgba(0,0,0,.35);padding:26px;overflow:auto}.drawer-head{display:flex;align-items:flex-start;margin-bottom:28px}.drawer h2{margin:0;font-size:23px;font-weight:400;letter-spacing:-.025em}.drawer-head button{margin-left:auto;border:0;background:transparent;color:var(--text-secondary);cursor:pointer}.drawer-head button:hover{color:var(--text-primary);background:var(--bg-surface)}.field{margin-bottom:17px}.field label{display:block;font:8px var(--mono);text-transform:uppercase;letter-spacing:.12em;color:var(--text-tertiary);margin-bottom:7px}.field input,.field textarea,.field select{width:100%;border:1px solid var(--border);background:var(--bg);border-radius:7px;padding:11px 12px;font-size:12px}.field textarea{min-height:120px;resize:vertical}.form-error{background:rgba(226,155,168,.08);color:var(--danger);border:1px solid rgba(226,155,168,.18);border-radius:7px;padding:10px 12px;font-size:11px;margin-bottom:13px}.drawer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}
3449
3771
  .search-modal{position:fixed;inset:0;background:rgba(0,0,0,.64);backdrop-filter:blur(4px);z-index:50;display:none;padding:12vh 20px}.search-modal.open{display:block}.search-box{max-width:650px;margin:auto;background:var(--bg-elevated);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow);overflow:hidden}.search-input-wrap{display:flex;align-items:center;gap:12px;padding:0 20px;border-bottom:1px solid var(--border);color:var(--text-tertiary)}.search-input-wrap>.icon{width:19px;height:19px}.search-box input{width:100%;border:0;background:var(--bg-elevated);padding:18px 0;font-size:15px}.search-results{max-height:420px;overflow:auto;padding:8px}.search-result{display:block;padding:11px 12px;border-radius:6px;text-decoration:none;color:inherit}.search-result:hover{background:var(--accent-glow)}.search-result strong{font-size:12px;font-weight:500}.search-result small{display:block;color:var(--text-tertiary);margin-top:3px}.toast{position:fixed;right:24px;bottom:24px;background:var(--accent);color:var(--bg);border-radius:7px;padding:12px 15px;font-size:11px;font-weight:600;box-shadow:var(--shadow);z-index:60;transform:translateY(20px);opacity:0;pointer-events:none;transition:.2s}.toast.show{transform:none;opacity:1}
3450
- @media(max-width:1100px){.cols-4{grid-template-columns:repeat(2,1fr)}.definition-grid{grid-template-columns:repeat(2,1fr)}.cols-3{grid-template-columns:1fr 1fr}.detail-strip{grid-template-columns:1fr 1fr}.detail-strip>div:nth-child(2){border-right:0}.detail-strip>div:nth-child(-n+2){border-bottom:1px solid var(--border-subtle)}}
3772
+ @media(max-width:1100px){.cols-4{grid-template-columns:repeat(2,1fr)}.definition-grid{grid-template-columns:repeat(2,1fr)}.cols-3{grid-template-columns:1fr 1fr}}
3451
3773
  @media(max-width:760px){:root{--sidebar:0px}.sidebar{transform:translateX(-248px);width:248px;transition:transform .2s ease}.sidebar.open{transform:none}.mobile-toggle{display:inline-flex}.topbar{padding:0 16px}.live-chip{display:none}.content{padding:24px 16px 55px}.page-head{display:block}.page-head .actions{margin:18px 0 0}.cols-4,.cols-3,.cols-2,.definition-grid,.kv{grid-template-columns:1fr}.span-2{grid-column:auto}.detail-strip{display:block}.detail-strip>div{border-right:0;border-bottom:1px solid var(--border-subtle)}.filters{flex-wrap:wrap}.filters input{min-width:100%;width:100%}.event-toggle{grid-template-columns:70px 90px 1fr}.event-toggle span:last-child{display:none}.top-actions .button.primary span{display:none}}
3774
+
3775
+ /* ---- Elapsed time and duration ---- */
3776
+ .rel{color:var(--text-secondary);white-space:nowrap;font-variant-numeric:tabular-nums;cursor:help;border-bottom:1px dotted var(--border-strong)}
3777
+ .dur{font-family:var(--mono);font-size:10px;color:var(--text-secondary);white-space:nowrap;font-variant-numeric:tabular-nums}
3778
+ .dur.live{color:var(--accent);position:relative;padding-left:12px}
3779
+ .dur.live:before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:5px;height:5px;border-radius:50%;background:var(--accent);animation:pulse-dot 1.6s ease-in-out infinite}
3780
+ @keyframes pulse-dot{0%,100%{opacity:1;box-shadow:0 0 0 0 var(--accent-line)}50%{opacity:.45;box-shadow:0 0 0 4px transparent}}
3781
+ .attempts{font-family:var(--mono);font-size:10px;color:var(--text-tertiary);font-variant-numeric:tabular-nums}
3782
+ .attempts.warn{color:var(--warn)}
3783
+
3784
+ /* ---- Copyable identifiers ---- */
3785
+ .id-cell{display:inline-flex;align-items:center;gap:5px;min-width:0}
3786
+ .id-cell .mono{font-family:var(--mono);font-size:11px;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis}
3787
+ .copy-btn{border:0;background:transparent;color:var(--text-tertiary);padding:3px;border-radius:5px;cursor:pointer;display:inline-flex;align-items:center;opacity:0;transition:opacity .14s,color .14s,background .14s}
3788
+ .copy-btn .icon{width:12px;height:12px}
3789
+ .copy-btn .copy-ok{display:none}
3790
+ tr:hover .copy-btn,.id-cell:hover .copy-btn,.copy-btn:focus-visible{opacity:1}
3791
+ .copy-btn:hover{color:var(--accent);background:var(--accent-dim)}
3792
+ .copy-btn.copied{opacity:1;color:var(--success)}
3793
+ .copy-btn.copied .icon{display:none}
3794
+ .copy-btn.copied .copy-ok{display:block}
3795
+ .button .icon{width:13px;height:13px}
3796
+
3797
+ /* ---- Filter toolbar ---- */
3798
+ .toolbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:14px}
3799
+ .toolbar-right{display:flex;align-items:center;gap:8px;margin-left:auto;flex-wrap:wrap}
3800
+ .segmented{display:inline-flex;background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:8px;padding:3px;gap:2px;overflow:auto}
3801
+ .segmented button{border:0;background:transparent;color:var(--text-secondary);font-size:11px;font-weight:500;padding:6px 10px;border-radius:6px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;white-space:nowrap;transition:background .16s,color .16s}
3802
+ .segmented button:hover{color:var(--text-primary)}
3803
+ .segmented button.active{background:var(--bg-surface-2);color:var(--text-primary);box-shadow:inset 0 0 0 1px var(--border)}
3804
+ .seg-count{font-family:var(--mono);font-size:9px;color:var(--text-tertiary);background:var(--bg);border-radius:20px;padding:2px 6px;font-variant-numeric:tabular-nums}
3805
+ .segmented button.active .seg-count{color:var(--accent);background:var(--accent-dim)}
3806
+ .search-field,.select-field{position:relative;display:inline-flex;align-items:center}
3807
+ .search-field-icon,.select-field-icon{position:absolute;left:10px;display:flex;color:var(--text-tertiary);pointer-events:none}
3808
+ .search-field-icon .icon,.select-field-icon .icon{width:13px;height:13px}
3809
+ .search-field input,.select-field select{background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:8px;padding:8px 12px 8px 30px;font-size:12px;color:var(--text-primary);min-width:150px}
3810
+ .search-field input{min-width:230px}
3811
+ .select-field select{appearance:none;cursor:pointer;padding-right:26px}
3812
+ .search-field input::placeholder{color:var(--text-tertiary)}
3813
+ .search-field input:focus,.select-field select:focus{border-color:var(--border-strong);outline:none}
3814
+ .filter-note{display:flex;align-items:center;gap:12px;flex-wrap:wrap;font-size:11px;color:var(--text-secondary);background:var(--accent-glow);border:1px solid var(--accent-line);border-radius:8px;padding:9px 13px;margin-bottom:14px}
3815
+ .filter-note .link{border:0;background:transparent;cursor:pointer;font-size:11px;padding:0;margin-left:auto}
3816
+ .hint{font:10px var(--mono);color:var(--text-tertiary);margin:14px 0 0;letter-spacing:.03em}
3817
+ .card-foot{padding:11px 15px;border-top:1px solid var(--border-subtle);font-size:11px}
3818
+
3819
+ /* ---- Row states and the keyboard cursor ---- */
3820
+ .runs-table{min-width:820px}
3821
+ tr.row-failed td:first-child{box-shadow:inset 2px 0 0 var(--danger)}
3822
+ tr.row-live td:first-child{box-shadow:inset 2px 0 0 var(--accent)}
3823
+ tr.cursor,.definition-card.cursor{outline:1px solid var(--accent-soft);outline-offset:-1px;background:var(--accent-glow)}
3824
+
3825
+ /* ---- Alerts ---- */
3826
+ .alert{display:flex;gap:13px;align-items:flex-start;border-radius:var(--radius);padding:14px 16px;margin-bottom:16px;border:1px solid var(--border)}
3827
+ .alert .symbol{display:flex;flex:0 0 auto;margin-top:1px}
3828
+ .alert .symbol .icon{width:17px;height:17px}
3829
+ .alert b{display:block;font-size:12px;font-weight:600;margin-bottom:3px}
3830
+ .alert p{margin:0;font-size:12px;color:var(--text-secondary);line-height:1.55;word-break:break-word}
3831
+ .alert.danger{background:rgba(226,155,168,.07);border-color:rgba(226,155,168,.26)}
3832
+ .alert.danger .symbol{color:var(--danger)}
3833
+ .alert.danger b{color:var(--danger)}
3834
+ .alert.live{background:var(--accent-glow);border-color:var(--accent-line)}
3835
+ .alert.live b{color:var(--accent)}
3836
+ .pulse-dot{width:9px;height:9px;border-radius:50%;background:var(--accent);display:block;margin-top:4px;animation:pulse-dot 1.6s ease-in-out infinite}
3837
+ .live-chip.pulse i{animation:live-ping .7s ease-out}
3838
+ @keyframes live-ping{0%{transform:scale(1);opacity:1}45%{transform:scale(2.1);opacity:.45}100%{transform:scale(1);opacity:1}}
3839
+
3840
+ /* ---- Event trace ---- */
3841
+ .event-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px 15px;border-bottom:1px solid var(--border-subtle)}
3842
+ .event-controls-right{display:flex;align-items:center;gap:12px;margin-left:auto;flex-wrap:wrap}
3843
+ .event-controls-right .muted{font-size:11px}
3844
+ .event-list{max-height:640px;overflow:auto}
3845
+ .chips{display:flex;gap:5px;flex-wrap:wrap}
3846
+ .chip{border:1px solid var(--border-subtle);background:var(--bg-elevated);color:var(--text-secondary);border-radius:20px;padding:5px 11px;font:9px var(--mono);text-transform:uppercase;letter-spacing:.07em;cursor:pointer;transition:border-color .16s,color .16s,background .16s}
3847
+ .chip:hover{color:var(--text-primary);border-color:var(--border)}
3848
+ .chip.active{background:var(--accent-dim);border-color:var(--accent-line);color:var(--accent)}
3849
+ .event-toggle{grid-template-columns:74px 62px 104px minmax(0,1fr) auto!important}
3850
+ .event-offset{color:var(--accent-soft);font-size:9px;font-variant-numeric:tabular-nums}
3851
+ .event-caret{display:flex;color:var(--text-tertiary)}
3852
+ .event-caret .icon{width:13px;height:13px;transition:transform .18s}
3853
+ .event-row.open .event-caret .icon{transform:rotate(180deg)}
3854
+ .event-detail-head{display:flex;align-items:center;gap:8px;padding:9px 15px 0}
3855
+ .event-detail-head .copy-btn{opacity:1;margin-left:auto}
3856
+
3857
+ /* ---- Skeletons ---- */
3858
+ .skeleton{padding:6px 0}
3859
+ .skeleton-row{display:flex;align-items:center;gap:16px;padding:13px 15px;border-bottom:1px solid var(--border-subtle)}
3860
+ .skeleton-row span{height:9px;border-radius:20px;background:linear-gradient(90deg,var(--bg-surface) 0%,var(--bg-surface-2) 50%,var(--bg-surface) 100%);background-size:200% 100%;animation:shimmer 1.5s linear infinite}
3861
+ .skeleton-row span:last-child{margin-left:auto}
3862
+ @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
3863
+ .boot-skeleton{padding:32px}
3864
+ .boot-skeleton p{font-size:12px;margin-top:18px}
3865
+
3866
+ /* ---- Run drawer ---- */
3867
+ .drawer-hint{font-size:9px;color:var(--text-tertiary);margin-right:auto;letter-spacing:.06em}
3868
+ .button.pending{opacity:.65;cursor:progress}
3869
+ .button.danger{color:var(--danger);border-color:rgba(226,155,168,.3)}
3870
+ .button.danger:hover{background:rgba(226,155,168,.09);border-color:var(--danger)}
3871
+
3872
+ @media(max-width:760px){
3873
+ .toolbar-right{margin-left:0;width:100%}
3874
+ .search-field,.search-field input{width:100%;min-width:0}
3875
+ .event-toggle{grid-template-columns:60px 54px minmax(0,1fr) auto!important}
3876
+ .event-toggle .event-category{display:none}
3877
+ .hint{display:none}
3878
+ }
3452
3879
  @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
3453
3880
  `;
3454
3881
 
@@ -3497,12 +3924,12 @@ function renderDashboard() {
3497
3924
  <button class="button icon-button mobile-toggle" id="mobile-toggle" aria-label="Open navigation">${renderPhosphorIcon("menu")}</button>
3498
3925
  <div class="breadcrumbs" id="breadcrumbs"></div>
3499
3926
  <div class="top-actions">
3500
- <span class="live-chip"><i></i><span id="event-state">Live updates</span></span>
3927
+ <span class="live-chip" title="The inspector streams runtime events and reconciles on a low-frequency timer."><i></i><span id="event-state">Connecting\u2026</span></span>
3501
3928
  <button class="button primary" id="new-run">${renderPhosphorIcon("plus")} <span>New run</span></button>
3502
3929
  </div>
3503
3930
  </header>
3504
3931
  <main class="content" id="content">
3505
- <div class="empty"><strong>Opening the Foundry\u2026</strong>Reading definitions, instances, and runtime activity.</div>
3932
+ <div class="boot-skeleton"><div class="skeleton" aria-busy="true" aria-label="Opening the Foundry"><div class="skeleton-row"><span style="width:38%"></span><span style="width:14%"></span></div><div class="skeleton-row"><span style="width:62%"></span><span style="width:18%"></span></div><div class="skeleton-row"><span style="width:51%"></span><span style="width:11%"></span></div><div class="skeleton-row"><span style="width:70%"></span><span style="width:16%"></span></div></div><p class="muted">Reading definitions, instances, and runtime activity\u2026</p></div>
3506
3933
  </main>
3507
3934
  </section>
3508
3935
  </div>
@@ -3518,7 +3945,7 @@ function renderDashboard() {
3518
3945
  <div class="field"><label for="run-definition">Agent definition</label><select id="run-definition"></select></div>
3519
3946
  <div class="field"><label for="run-instance">Runtime instance</label><select id="run-instance"></select><small class="muted">Choose \u201CCreate a new instance\u201D to provision one.</small></div>
3520
3947
  <div class="field"><label for="run-message">Message</label><textarea id="run-message" required placeholder="What should this agent work on?"></textarea></div>
3521
- <div class="drawer-actions"><button type="button" class="button" id="cancel-run">Cancel</button><button type="submit" class="button primary">Start run</button></div>
3948
+ <div class="drawer-actions"><span class="drawer-hint mono">\u2318 + Enter</span><button type="button" class="button" id="cancel-run">Cancel</button><button type="submit" class="button primary" id="run-submit">Start run</button></div>
3522
3949
  </form>
3523
3950
  </section>
3524
3951
  </div>
@@ -3709,7 +4136,17 @@ var FoundryServer = class {
3709
4136
  response.writeHead(200, {
3710
4137
  "content-type": "text/html; charset=utf-8",
3711
4138
  "cache-control": "no-store",
3712
- "content-security-policy": "default-src 'self'; script-src 'unsafe-inline' 'self'; style-src 'unsafe-inline' 'self'; connect-src 'self'",
4139
+ // The inspector's stylesheet pulls the Glove brand typefaces from
4140
+ // Google Fonts, so style-src and font-src name those two origins and
4141
+ // nothing else. Everything executable stays same-origin.
4142
+ "content-security-policy": [
4143
+ "default-src 'self'",
4144
+ "script-src 'unsafe-inline' 'self'",
4145
+ "style-src 'unsafe-inline' 'self' https://fonts.googleapis.com",
4146
+ "font-src 'self' https://fonts.gstatic.com",
4147
+ "img-src 'self' data:",
4148
+ "connect-src 'self'"
4149
+ ].join("; "),
3713
4150
  "x-content-type-options": "nosniff",
3714
4151
  "x-frame-options": "DENY"
3715
4152
  });