@agentdeck/bridge 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/fm-helper/agentdeck-fm-helper +0 -0
- package/dist/apme/collector.d.ts +10 -0
- package/dist/apme/collector.d.ts.map +1 -1
- package/dist/apme/collector.js +39 -10
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/dashboard-html.d.ts.map +1 -1
- package/dist/apme/dashboard-html.js +289 -0
- package/dist/apme/dashboard-html.js.map +1 -1
- package/dist/apme/graph.d.ts +48 -0
- package/dist/apme/graph.d.ts.map +1 -0
- package/dist/apme/graph.js +226 -0
- package/dist/apme/graph.js.map +1 -0
- package/dist/apme/http.d.ts.map +1 -1
- package/dist/apme/http.js +52 -0
- package/dist/apme/http.js.map +1 -1
- package/dist/apme/judge-detect.d.ts.map +1 -1
- package/dist/apme/judge-detect.js +26 -2
- package/dist/apme/judge-detect.js.map +1 -1
- package/dist/apme/store.d.ts +69 -1
- package/dist/apme/store.d.ts.map +1 -1
- package/dist/apme/store.js +238 -4
- package/dist/apme/store.js.map +1 -1
- package/dist/apme/types.d.ts +1 -1
- package/dist/apme/types.d.ts.map +1 -1
- package/dist/apme/types.js.map +1 -1
- package/dist/ble-sync-spawn.d.ts +25 -2
- package/dist/ble-sync-spawn.d.ts.map +1 -1
- package/dist/ble-sync-spawn.js +38 -2
- package/dist/ble-sync-spawn.js.map +1 -1
- package/dist/ble-sync-status.d.ts +77 -0
- package/dist/ble-sync-status.d.ts.map +1 -0
- package/dist/ble-sync-status.js +157 -0
- package/dist/ble-sync-status.js.map +1 -0
- package/dist/bridge-core.d.ts.map +1 -1
- package/dist/bridge-core.js +7 -1
- package/dist/bridge-core.js.map +1 -1
- package/dist/cli.js +0 -0
- package/dist/codex-rate-limits-live.d.ts +60 -0
- package/dist/codex-rate-limits-live.d.ts.map +1 -0
- package/dist/codex-rate-limits-live.js +282 -0
- package/dist/codex-rate-limits-live.js.map +1 -0
- package/dist/d200h/hid-protocol.d.ts +62 -0
- package/dist/d200h/hid-protocol.d.ts.map +1 -0
- package/dist/d200h/hid-protocol.js +124 -0
- package/dist/d200h/hid-protocol.js.map +1 -0
- package/dist/d200h/image-renderer.d.ts +24 -0
- package/dist/d200h/image-renderer.d.ts.map +1 -0
- package/dist/d200h/image-renderer.js +321 -0
- package/dist/d200h/image-renderer.js.map +1 -0
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +163 -10
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/device-voice-reply.d.ts +1 -10
- package/dist/device-voice-reply.d.ts.map +1 -1
- package/dist/device-voice-reply.js +4 -33
- package/dist/device-voice-reply.js.map +1 -1
- package/dist/foundation-models-helper.d.ts.map +1 -1
- package/dist/foundation-models-helper.js +59 -22
- package/dist/foundation-models-helper.js.map +1 -1
- package/dist/hook-migration.d.ts +2 -0
- package/dist/hook-migration.d.ts.map +1 -0
- package/dist/hook-migration.js +116 -0
- package/dist/hook-migration.js.map +1 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +8 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -1
- package/dist/idotmatrix/idotmatrix-daemon-sync.js +19 -3
- package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
- package/dist/modules/d200h-module.d.ts +65 -0
- package/dist/modules/d200h-module.d.ts.map +1 -0
- package/dist/modules/d200h-module.js +495 -0
- package/dist/modules/d200h-module.js.map +1 -0
- package/dist/modules/d200h-renderer.d.ts +7 -0
- package/dist/modules/d200h-renderer.d.ts.map +1 -0
- package/dist/modules/d200h-renderer.js +321 -0
- package/dist/modules/d200h-renderer.js.map +1 -0
- package/dist/modules/idotmatrix-module.d.ts.map +1 -1
- package/dist/modules/idotmatrix-module.js +12 -1
- package/dist/modules/idotmatrix-module.js.map +1 -1
- package/dist/modules/timebox-module.d.ts.map +1 -1
- package/dist/modules/timebox-module.js +12 -1
- package/dist/modules/timebox-module.js.map +1 -1
- package/dist/modules/trmnl-module.d.ts +38 -0
- package/dist/modules/trmnl-module.d.ts.map +1 -0
- package/dist/modules/trmnl-module.js +111 -0
- package/dist/modules/trmnl-module.js.map +1 -0
- package/dist/openclaw-session.d.ts +6 -0
- package/dist/openclaw-session.d.ts.map +1 -1
- package/dist/openclaw-session.js +7 -1
- package/dist/openclaw-session.js.map +1 -1
- package/dist/timebox/timebox-daemon-sync.d.ts +8 -0
- package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -1
- package/dist/timebox/timebox-daemon-sync.js +29 -1
- package/dist/timebox/timebox-daemon-sync.js.map +1 -1
- package/dist/timeline-summarizer.d.ts +12 -2
- package/dist/timeline-summarizer.d.ts.map +1 -1
- package/dist/timeline-summarizer.js +66 -4
- package/dist/timeline-summarizer.js.map +1 -1
- package/dist/trmnl/byos-server.d.ts +39 -0
- package/dist/trmnl/byos-server.d.ts.map +1 -0
- package/dist/trmnl/byos-server.js +301 -0
- package/dist/trmnl/byos-server.js.map +1 -0
- package/dist/trmnl/frame-cache.d.ts +37 -0
- package/dist/trmnl/frame-cache.d.ts.map +1 -0
- package/dist/trmnl/frame-cache.js +157 -0
- package/dist/trmnl/frame-cache.js.map +1 -0
- package/dist/trmnl/image-renderer.d.ts +27 -0
- package/dist/trmnl/image-renderer.d.ts.map +1 -0
- package/dist/trmnl/image-renderer.js +235 -0
- package/dist/trmnl/image-renderer.js.map +1 -0
- package/dist/trmnl/trmnl-settings.d.ts +96 -0
- package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
- package/dist/trmnl/trmnl-settings.js +177 -0
- package/dist/trmnl/trmnl-settings.js.map +1 -0
- package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
- package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
- package/dist/trmnl/trmnl-telemetry.js +56 -0
- package/dist/trmnl/trmnl-telemetry.js.map +1 -0
- package/dist/whisper-server-manager.d.ts +19 -0
- package/dist/whisper-server-manager.d.ts.map +1 -0
- package/dist/whisper-server-manager.js +171 -0
- package/dist/whisper-server-manager.js.map +1 -0
- package/fm-helper/Info.plist +27 -0
- package/package.json +13 -14
- package/scripts/build-fm-helper.mjs +35 -3
- package/src/idotmatrix/sync.py +16 -1
- package/src/pysync/matrix_sync_common.py +83 -0
- package/src/timebox/sync_ble.py +16 -0
|
@@ -98,6 +98,8 @@ tr.selected td{background:rgba(99,102,241,0.15);border-left:2px solid var(--acce
|
|
|
98
98
|
.vibe-current{font-size:11px;color:var(--dim);display:flex;align-items:center;margin-left:8px}
|
|
99
99
|
|
|
100
100
|
.panel{display:none}.panel.visible{display:block;height:100%}
|
|
101
|
+
/* Graph needs a column flex box so the canvas can claim the leftover height. */
|
|
102
|
+
.panel#panel-graph.visible{display:flex;flex-direction:column}
|
|
101
103
|
.empty{color:var(--dim);font-style:italic;padding:20px;text-align:center}
|
|
102
104
|
</style>
|
|
103
105
|
</head>
|
|
@@ -112,6 +114,8 @@ tr.selected td{background:rgba(99,102,241,0.15);border-left:2px solid var(--acce
|
|
|
112
114
|
<div class="left">
|
|
113
115
|
<div class="tabs">
|
|
114
116
|
<button class="tab active" onclick="showTab('runs')">Runs</button>
|
|
117
|
+
<button class="tab" onclick="showTab('tasks')">Tasks</button>
|
|
118
|
+
<button class="tab" onclick="showTab('graph')">Graph</button>
|
|
115
119
|
<button class="tab" onclick="showTab('recommend')">Recommend</button>
|
|
116
120
|
<button class="tab" onclick="showTab('scorecard')">Scorecard</button>
|
|
117
121
|
<button class="tab" onclick="showTab('categories')">Categories</button>
|
|
@@ -129,6 +133,37 @@ tr.selected td{background:rgba(99,102,241,0.15);border-left:2px solid var(--acce
|
|
|
129
133
|
<th>Agent</th><th>Model</th><th>Project</th><th>Category</th><th>Score</th><th>Outcome</th><th>Vibe</th><th>Task</th><th>Time</th>
|
|
130
134
|
</tr></thead><tbody id="runs-body"></tbody></table>
|
|
131
135
|
</div>
|
|
136
|
+
<!-- Tasks: every processed work unit, paged server-side. The Runs tab
|
|
137
|
+
lists sessions; this lists the units the judge actually scores. -->
|
|
138
|
+
<div class="panel" id="panel-tasks">
|
|
139
|
+
<div style="display:flex;gap:6px;padding:8px 10px;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0;flex-wrap:wrap;align-items:center">
|
|
140
|
+
<input id="t-q" placeholder="Search prompt / summary" oninput="taskSearchDebounced()" style="background:var(--bg);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:4px 8px;font-size:11px;flex:1;min-width:140px">
|
|
141
|
+
<select id="t-agent" onchange="loadTasks(0)" style="background:var(--bg);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:4px 8px;font-size:11px"><option value="">All Agents</option></select>
|
|
142
|
+
<select id="t-project" onchange="loadTasks(0)" style="background:var(--bg);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:4px 8px;font-size:11px"><option value="">All Projects</option></select>
|
|
143
|
+
<select id="t-cat" onchange="loadTasks(0)" style="background:var(--bg);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:4px 8px;font-size:11px"><option value="">All Categories</option></select>
|
|
144
|
+
<select id="t-state" onchange="loadTasks(0)" style="background:var(--bg);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:4px 8px;font-size:11px"><option value="">All</option><option value="closed">Closed</option><option value="open">Open</option></select>
|
|
145
|
+
</div>
|
|
146
|
+
<table><thead><tr>
|
|
147
|
+
<th>Task</th><th>Agent</th><th>Project</th><th>Category</th><th>Turns</th><th>Tools</th><th>Score</th><th>Outcome</th><th>Time</th>
|
|
148
|
+
</tr></thead><tbody id="tasks-body"></tbody></table>
|
|
149
|
+
<div id="tasks-pager" style="display:flex;gap:8px;align-items:center;justify-content:center;padding:10px;font-size:11px;color:var(--dim)"></div>
|
|
150
|
+
</div>
|
|
151
|
+
<!-- Graph: the row store as a property graph. Self-contained force
|
|
152
|
+
layout — the dashboard ships as one inlined HTML document. -->
|
|
153
|
+
<div class="panel" id="panel-graph">
|
|
154
|
+
<div style="display:flex;gap:6px;padding:8px 10px;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0;flex-wrap:wrap;align-items:center">
|
|
155
|
+
<label style="font-size:11px;color:var(--dim)">Tasks <input id="g-limit" type="number" min="1" max="400" value="40" onchange="loadGraph()" style="width:56px;background:var(--bg);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:3px 6px;font-size:11px"></label>
|
|
156
|
+
<label style="font-size:11px;color:var(--dim)">Min hub <input id="g-hub" type="number" min="1" max="20" value="2" onchange="loadGraph()" style="width:48px;background:var(--bg);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:3px 6px;font-size:11px"></label>
|
|
157
|
+
<label style="font-size:11px;color:var(--dim)"><input id="g-turns" type="checkbox" onchange="loadGraph()"> turns</label>
|
|
158
|
+
<label style="font-size:11px;color:var(--dim)"><input id="g-files" type="checkbox" checked onchange="loadGraph()"> files</label>
|
|
159
|
+
<span id="g-stats" style="font-size:11px;color:var(--dim);margin-left:auto"></span>
|
|
160
|
+
</div>
|
|
161
|
+
<div style="position:relative;flex:1;min-height:420px">
|
|
162
|
+
<canvas id="g-canvas" style="width:100%;height:100%;display:block;cursor:grab"></canvas>
|
|
163
|
+
<div id="g-legend" style="position:absolute;left:10px;bottom:10px;font-size:10px;color:var(--dim);background:rgba(0,0,0,0.35);padding:6px 8px;border-radius:4px;line-height:1.7"></div>
|
|
164
|
+
<div id="g-tip" style="position:absolute;display:none;pointer-events:none;background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:6px 8px;font-size:11px;color:var(--muted);max-width:280px"></div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
132
167
|
<div class="panel" id="panel-recommend"><div id="recommend-content" class="empty">Loading...</div></div>
|
|
133
168
|
<div class="panel" id="panel-scorecard"><div id="scorecard-content" class="empty">Loading...</div></div>
|
|
134
169
|
<div class="panel" id="panel-categories"><div id="categories-content" class="empty">Loading...</div></div>
|
|
@@ -143,6 +178,8 @@ tr.selected td{background:rgba(99,102,241,0.15);border-left:2px solid var(--acce
|
|
|
143
178
|
|
|
144
179
|
<script>
|
|
145
180
|
const B=location.origin;let selId=null;let allRuns=[];
|
|
181
|
+
let selTaskId=null,taskOffset=0,taskTotal=0,tasksLoaded=false,taskSearchTimer=null;const TASK_PAGE=50;
|
|
182
|
+
let graphSim=null;
|
|
146
183
|
const AUTH=new URLSearchParams(location.search).get('token')||'';
|
|
147
184
|
function api(path){const sep=path.includes('?')?'&':'?';return B+path+(AUTH?sep+'token='+encodeURIComponent(AUTH):'')}
|
|
148
185
|
|
|
@@ -151,6 +188,11 @@ function showTab(n){
|
|
|
151
188
|
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
|
152
189
|
document.getElementById('panel-'+n).classList.add('visible');
|
|
153
190
|
event.target.classList.add('active');
|
|
191
|
+
// Tasks and Graph load on first reveal, not at boot: both are server-paged
|
|
192
|
+
// queries over the whole store, and the graph canvas cannot size itself
|
|
193
|
+
// while its panel is display:none.
|
|
194
|
+
if(n==='tasks'&&!tasksLoaded){tasksLoaded=true;loadTasks(0)}
|
|
195
|
+
if(n==='graph'){requestAnimationFrame(()=>loadGraph())}
|
|
154
196
|
}
|
|
155
197
|
function fs(s){if(s==null)return'<span class="score score-na">—</span>';const p=Math.round(s*100),c=p>=70?'high':p>=40?'mid':'low';return'<span class="score score-'+c+'">'+p+'%</span>'}
|
|
156
198
|
function fo(o){if(!o)return'';return'<span class="badge badge-'+o+'">'+o+'</span>'}
|
|
@@ -460,6 +502,108 @@ async function submitVibe(rid,v){
|
|
|
460
502
|
catch(e){alert('Error: '+e.message)}
|
|
461
503
|
}
|
|
462
504
|
|
|
505
|
+
/* ── Tasks tab ──────────────────────────────────────────────────────────────
|
|
506
|
+
The task unit is what the judge scores, but it used to be reachable only by
|
|
507
|
+
drilling into a run — so there was no way to see everything processed. This
|
|
508
|
+
pages server-side (the store holds thousands of units) and keeps its filters
|
|
509
|
+
in the query rather than filtering a fetched page client-side, which would
|
|
510
|
+
silently hide matches beyond the page. */
|
|
511
|
+
async function loadTasks(offset){
|
|
512
|
+
taskOffset=offset||0;
|
|
513
|
+
const body=document.getElementById('tasks-body');
|
|
514
|
+
const p=new URLSearchParams();
|
|
515
|
+
p.set('limit',String(TASK_PAGE));p.set('offset',String(taskOffset));
|
|
516
|
+
const q=document.getElementById('t-q').value.trim();if(q)p.set('q',q);
|
|
517
|
+
const ag=document.getElementById('t-agent').value;if(ag)p.set('agent',ag);
|
|
518
|
+
const pr=document.getElementById('t-project').value;if(pr)p.set('project',pr);
|
|
519
|
+
const ct=document.getElementById('t-cat').value;if(ct)p.set('category',ct);
|
|
520
|
+
const st=document.getElementById('t-state').value;if(st)p.set('state',st);
|
|
521
|
+
try{
|
|
522
|
+
const r=await fetch(api('/apme/tasks?'+p.toString()));const d=await r.json();
|
|
523
|
+
const tasks=d.tasks||[];taskTotal=d.total||0;
|
|
524
|
+
fillTaskFacets(d.facets||{});
|
|
525
|
+
if(!tasks.length){body.innerHTML='<tr><td colspan="9" class="empty">No tasks match</td></tr>';document.getElementById('tasks-pager').textContent='';return}
|
|
526
|
+
let h='';
|
|
527
|
+
for(const t of tasks){
|
|
528
|
+
const label=t.summary||t.firstPrompt||('Task '+t.taskIndex);
|
|
529
|
+
// A unit whose replies were never archived can only be partly judged —
|
|
530
|
+
// say so on the row instead of letting a blank score read as "not run".
|
|
531
|
+
const gap=t.turnCount>t.answeredTurns
|
|
532
|
+
?' <span title="'+(t.turnCount-t.answeredTurns)+' turn(s) archived without a reply" style="color:var(--yellow)">◍</span>':'';
|
|
533
|
+
h+='<tr'+(selTaskId===t.id?' class="selected"':'')+' onclick="selectTask(\\''+t.id+'\\')">'+
|
|
534
|
+
'<td title="'+esc(label)+'">'+esc(label.slice(0,52))+gap+'</td>'+
|
|
535
|
+
'<td>'+esc(t.agentType||'')+'</td>'+
|
|
536
|
+
'<td>'+esc(t.projectName||'—')+'</td>'+
|
|
537
|
+
'<td>'+(t.taskCategory?'<span class="badge-cat">'+esc(t.taskCategory)+'</span>':'—')+'</td>'+
|
|
538
|
+
'<td>'+t.turnCount+'</td><td>'+t.toolCount+'</td>'+
|
|
539
|
+
'<td>'+fs(t.overallScore)+'</td>'+
|
|
540
|
+
'<td>'+(t.outcome?fo(t.outcome):(t.endedAt?'':'<span class="badge">open</span>'))+'</td>'+
|
|
541
|
+
'<td>'+(t.startedAt?new Date(t.startedAt).toLocaleString():'—')+'</td></tr>';
|
|
542
|
+
}
|
|
543
|
+
body.innerHTML=h;
|
|
544
|
+
const from=taskTotal?taskOffset+1:0,to=Math.min(taskOffset+tasks.length,taskTotal);
|
|
545
|
+
document.getElementById('tasks-pager').innerHTML=
|
|
546
|
+
'<button onclick="loadTasks('+Math.max(0,taskOffset-TASK_PAGE)+')" '+(taskOffset<=0?'disabled':'')+' style="background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:3px 10px;cursor:pointer">Prev</button>'+
|
|
547
|
+
'<span>'+from+'–'+to+' of '+taskTotal+'</span>'+
|
|
548
|
+
'<button onclick="loadTasks('+(taskOffset+TASK_PAGE)+')" '+(to>=taskTotal?'disabled':'')+' style="background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:3px 10px;cursor:pointer">Next</button>';
|
|
549
|
+
}catch(e){body.innerHTML='<tr><td colspan="9" class="empty">Error: '+esc(e.message)+'</td></tr>'}
|
|
550
|
+
}
|
|
551
|
+
function fillTaskFacets(f){
|
|
552
|
+
// Facets come from the whole store, not the current page, so a filter never
|
|
553
|
+
// offers only what happens to be visible.
|
|
554
|
+
const fill=(id,vals)=>{const s=document.getElementById(id);const cur=s.value;
|
|
555
|
+
while(s.options.length>1)s.remove(1);
|
|
556
|
+
(vals||[]).forEach(v=>{const o=document.createElement('option');o.value=v;o.textContent=v;s.add(o)});
|
|
557
|
+
s.value=cur;};
|
|
558
|
+
fill('t-agent',f.agents);fill('t-project',f.projects);fill('t-cat',f.categories);
|
|
559
|
+
}
|
|
560
|
+
function taskSearchDebounced(){clearTimeout(taskSearchTimer);taskSearchTimer=setTimeout(()=>loadTasks(0),250)}
|
|
561
|
+
|
|
562
|
+
async function selectTask(id){
|
|
563
|
+
selTaskId=id;
|
|
564
|
+
const el=document.getElementById('detail-panel');
|
|
565
|
+
el.innerHTML='<div class="detail-empty">Loading...</div>';
|
|
566
|
+
try{
|
|
567
|
+
const r=await fetch(api('/apme/tasks/'+id));const d=await r.json();
|
|
568
|
+
const t=d.task||{},run=d.run||{},turns=d.turns||[],evals=d.evals||[],sample=d.sample;
|
|
569
|
+
let h='';
|
|
570
|
+
h+='<div class="detail-header"><h2>'+esc(t.summary||('Task '+t.taskIndex))+'</h2>';
|
|
571
|
+
h+='<div class="meta-row"><span>'+esc(run.agentType||'')+'</span><span>'+esc(run.modelId||'—')+'</span>'+
|
|
572
|
+
'<span>'+esc(run.projectName||'—')+'</span>'+
|
|
573
|
+
'<span>'+(t.startedAt?new Date(t.startedAt).toLocaleString():'—')+'</span>'+
|
|
574
|
+
'<span>'+(t.endedAt?fd(t.endedAt-t.startedAt):'<span style="color:var(--yellow)">open</span>')+'</span>'+
|
|
575
|
+
'<span>boundary '+esc(t.boundarySignal||'—')+'</span></div></div>';
|
|
576
|
+
h+='<div class="section"><div class="metric-grid">'
|
|
577
|
+
'<div class="metric-card"><div class="lbl">Score</div><div class="val">'+fs(d.overallScore??t.compositeScore)+'</div></div>'+
|
|
578
|
+
'<div class="metric-card"><div class="lbl">Turns</div><div class="val">'+turns.length+'</div></div>'+
|
|
579
|
+
'<div class="metric-card"><div class="lbl">Events</div><div class="val">'+((sample&&sample.events?sample.events.length:0))+'</div></div>'+
|
|
580
|
+
'<div class="metric-card"><div class="lbl">Cost</div><div class="val">'+(t.costUsd!=null?'$'+t.costUsd.toFixed(4):'—')+'</div></div>'+
|
|
581
|
+
'</div></div>';
|
|
582
|
+
if(evals.length){
|
|
583
|
+
h+='<div class="section"><div class="section-head"><span>Evals</span></div><table><tbody>';
|
|
584
|
+
for(const e of evals)h+='<tr><td>'+esc(e.layer)+'</td><td>'+esc(e.metric)+'</td><td>'+fs(e.score)+'</td></tr>';
|
|
585
|
+
h+='</tbody></table></div>';
|
|
586
|
+
}
|
|
587
|
+
// The conversation. This is what the response-capture gap used to make
|
|
588
|
+
// impossible to show: prompts with no replies beside them.
|
|
589
|
+
h+='<div class="section"><div class="section-head"><span>Conversation</span></div>';
|
|
590
|
+
if(!turns.length)h+='<div class="empty">No turns recorded</div>';
|
|
591
|
+
for(const tu of turns){
|
|
592
|
+
const prompt=tu.prompt||'',resp=tu.response;
|
|
593
|
+
h+='<div style="margin-bottom:12px">'+
|
|
594
|
+
'<div style="font-size:11px;color:var(--dim);margin-bottom:3px">turn '+tu.turn_index+'</div>'+
|
|
595
|
+
'<div style="white-space:pre-wrap;font-size:12px;color:var(--muted);border-left:2px solid var(--accent);padding-left:8px">'+esc(prompt.slice(0,1200))+'</div>';
|
|
596
|
+
h+= resp
|
|
597
|
+
? '<div style="white-space:pre-wrap;font-size:12px;color:var(--dim);border-left:2px solid var(--border);padding-left:8px;margin-top:6px">'+esc(resp.slice(0,2000))+'</div>'
|
|
598
|
+
: '<div style="font-size:11px;color:var(--yellow);padding-left:10px;margin-top:6px">reply not archived</div>';
|
|
599
|
+
h+='</div>';
|
|
600
|
+
}
|
|
601
|
+
h+='</div>';
|
|
602
|
+
el.innerHTML=h;
|
|
603
|
+
}catch(e){el.innerHTML='<div class="detail-empty">Error: '+esc(e.message)+'</div>'}
|
|
604
|
+
loadTasks(taskOffset);
|
|
605
|
+
}
|
|
606
|
+
|
|
463
607
|
async function loadScorecard(){
|
|
464
608
|
try{const r=await fetch(api('/apme/scorecard'));const d=await r.json();const c=d.scorecards||[];
|
|
465
609
|
if(!c.length){document.getElementById('scorecard-content').innerHTML='<div class="empty">No scorecard data yet</div>';return}
|
|
@@ -513,8 +657,153 @@ async function loadRecommend(){
|
|
|
513
657
|
}catch(e){document.getElementById('recommend-content').innerHTML='<div class="empty">Error: '+e.message+'</div>'}
|
|
514
658
|
}
|
|
515
659
|
|
|
660
|
+
/* ── Graph tab ──────────────────────────────────────────────────────────────
|
|
661
|
+
A force-directed view of /apme/graph. Hand-rolled rather than imported: the
|
|
662
|
+
dashboard is served as one self-contained HTML document with no external
|
|
663
|
+
fetches, so a CDN layout library is not an option.
|
|
664
|
+
|
|
665
|
+
Colour is by node kind; radius by degree, so the derived hubs (a file many
|
|
666
|
+
tasks touched, a tool used everywhere) are the ones that read as structural. */
|
|
667
|
+
const G_COLORS={run:'#6166E0',task:'#22c55e',turn:'#64748b',session:'#eab308',project:'#06b6d4',model:'#a855f7',agent:'#C07058',tool:'#f97316',file:'#94a3b8'};
|
|
668
|
+
const G_EDGE={contains:'rgba(148,163,184,0.35)',continues:'rgba(97,102,224,0.75)',produced:'rgba(234,179,8,0.30)',used:'rgba(249,115,22,0.30)',touched:'rgba(148,163,184,0.25)'};
|
|
669
|
+
|
|
670
|
+
async function loadGraph(){
|
|
671
|
+
const stats=document.getElementById('g-stats');
|
|
672
|
+
const p=new URLSearchParams();
|
|
673
|
+
p.set('limit',document.getElementById('g-limit').value||'40');
|
|
674
|
+
p.set('minHubDegree',document.getElementById('g-hub').value||'2');
|
|
675
|
+
p.set('turns',document.getElementById('g-turns').checked?'1':'0');
|
|
676
|
+
p.set('files',document.getElementById('g-files').checked?'1':'0');
|
|
677
|
+
stats.textContent='Loading...';
|
|
678
|
+
try{
|
|
679
|
+
const r=await fetch(api('/apme/graph?'+p.toString()));const d=await r.json();
|
|
680
|
+
const s=d.stats||{};
|
|
681
|
+
// Say what was left out. A truncated graph that looks complete is worse
|
|
682
|
+
// than no graph — the shape would read as the whole history.
|
|
683
|
+
stats.textContent=s.nodeCount+' nodes · '+s.edgeCount+' edges · '+s.taskCount+' tasks'+
|
|
684
|
+
(s.truncatedTasks?' (+'+s.truncatedTasks+' not shown)':'')+
|
|
685
|
+
(s.fileCoverage?' · file paths on '+s.fileCoverage.withPath+'/'+s.fileCoverage.toolEvents+' tool events':'');
|
|
686
|
+
renderLegend(d.nodes||[]);
|
|
687
|
+
startGraphSim(d.nodes||[],d.edges||[]);
|
|
688
|
+
}catch(e){stats.textContent='Error: '+e.message}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function renderLegend(nodes){
|
|
692
|
+
const counts={};for(const n of nodes)counts[n.kind]=(counts[n.kind]||0)+1;
|
|
693
|
+
document.getElementById('g-legend').innerHTML=Object.keys(counts).sort().map(k=>
|
|
694
|
+
'<div><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+G_COLORS[k]+';margin-right:6px"></span>'+k+' ('+counts[k]+')</div>').join('');
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function startGraphSim(nodes,edges){
|
|
698
|
+
const canvas=document.getElementById('g-canvas');
|
|
699
|
+
const dpr=window.devicePixelRatio||1;
|
|
700
|
+
const w=canvas.clientWidth||800,h=canvas.clientHeight||500;
|
|
701
|
+
canvas.width=w*dpr;canvas.height=h*dpr;
|
|
702
|
+
const ctx=canvas.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);
|
|
703
|
+
if(graphSim)cancelAnimationFrame(graphSim.raf);
|
|
704
|
+
|
|
705
|
+
const byId=new Map();
|
|
706
|
+
nodes.forEach((n,i)=>{
|
|
707
|
+
// Deterministic seeding — a golden-angle spiral, not Math.random(), so a
|
|
708
|
+
// reload of the same slice settles into the same picture and the layout is
|
|
709
|
+
// comparable between refreshes.
|
|
710
|
+
const a=i*2.399963,rad=6*Math.sqrt(i);
|
|
711
|
+
n.x=w/2+rad*Math.cos(a);n.y=h/2+rad*Math.sin(a);n.vx=0;n.vy=0;
|
|
712
|
+
n.r=Math.min(14,3+Math.sqrt(n.degree||1)*1.6);
|
|
713
|
+
byId.set(n.id,n);
|
|
714
|
+
});
|
|
715
|
+
const links=edges.map(e=>({s:byId.get(e.from),t:byId.get(e.to),kind:e.kind,w:e.weight||1}))
|
|
716
|
+
.filter(l=>l.s&&l.t);
|
|
717
|
+
|
|
718
|
+
const view={x:0,y:0,k:1};let drag=null,hover=null;
|
|
719
|
+
canvas.onmousedown=(ev)=>{drag={x:ev.clientX,y:ev.clientY,vx:view.x,vy:view.y};canvas.style.cursor='grabbing'};
|
|
720
|
+
window.addEventListener('mouseup',()=>{drag=null;canvas.style.cursor='grab'});
|
|
721
|
+
canvas.onmousemove=(ev)=>{
|
|
722
|
+
const rect=canvas.getBoundingClientRect();
|
|
723
|
+
if(drag){view.x=drag.vx+(ev.clientX-drag.x);view.y=drag.vy+(ev.clientY-drag.y);return}
|
|
724
|
+
const mx=(ev.clientX-rect.left-view.x)/view.k,my=(ev.clientY-rect.top-view.y)/view.k;
|
|
725
|
+
hover=null;let best=1e9;
|
|
726
|
+
for(const n of nodes){const dx=n.x-mx,dy=n.y-my,d2=dx*dx+dy*dy;if(d2<Math.max(64,n.r*n.r*4)&&d2<best){best=d2;hover=n}}
|
|
727
|
+
const tip=document.getElementById('g-tip');
|
|
728
|
+
if(hover){
|
|
729
|
+
const meta=Object.entries(hover.meta||{}).filter(([,v])=>v!=null&&v!=='').map(([k,v])=>k+': '+v).join(' · ');
|
|
730
|
+
tip.style.display='block';tip.style.left=(ev.clientX-rect.left+12)+'px';tip.style.top=(ev.clientY-rect.top+12)+'px';
|
|
731
|
+
tip.innerHTML='<b style="color:'+G_COLORS[hover.kind]+'">'+esc(hover.kind)+'</b> '+esc(hover.label)+
|
|
732
|
+
'<div style="color:var(--dim);margin-top:3px">degree '+(hover.degree||0)+(meta?' · '+esc(meta):'')+'</div>';
|
|
733
|
+
} else tip.style.display='none';
|
|
734
|
+
};
|
|
735
|
+
canvas.onwheel=(ev)=>{ev.preventDefault();const f=ev.deltaY<0?1.1:0.9;view.k=Math.max(0.2,Math.min(4,view.k*f))};
|
|
736
|
+
canvas.onclick=()=>{if(hover&&hover.kind==='task'){showTabById('tasks');selectTask(hover.id.slice(5))}};
|
|
737
|
+
|
|
738
|
+
// Barnes-Hut would be overkill: the slice is capped at a few hundred nodes,
|
|
739
|
+
// so an O(n²) repulsion pass stays well inside a frame budget.
|
|
740
|
+
// Repulsion and rest length scale with node count: constants tuned on a
|
|
741
|
+
// 30-node slice collapse into an unreadable ball at 300, and the whole point
|
|
742
|
+
// of the view is that the hub structure is visible.
|
|
743
|
+
const repel=900+nodes.length*14;
|
|
744
|
+
const linkLen=50+Math.min(60,nodes.length*0.25);
|
|
745
|
+
let alpha=1;
|
|
746
|
+
function step(){
|
|
747
|
+
if(alpha>0.005){
|
|
748
|
+
for(let i=0;i<nodes.length;i++){
|
|
749
|
+
const a=nodes[i];
|
|
750
|
+
for(let j=i+1;j<nodes.length;j++){
|
|
751
|
+
const b=nodes[j];let dx=b.x-a.x,dy=b.y-a.y;let d2=dx*dx+dy*dy;
|
|
752
|
+
if(d2<0.01){dx=(i-j)*0.1;dy=0.1;d2=0.02}
|
|
753
|
+
if(d2>250000)continue;
|
|
754
|
+
const f=repel/d2,d=Math.sqrt(d2);
|
|
755
|
+
const fx=f*dx/d,fy=f*dy/d;
|
|
756
|
+
a.vx-=fx;a.vy-=fy;b.vx+=fx;b.vy+=fy;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
for(const l of links){
|
|
760
|
+
const dx=l.t.x-l.s.x,dy=l.t.y-l.s.y,d=Math.sqrt(dx*dx+dy*dy)||0.01;
|
|
761
|
+
const f=(d-linkLen)*0.02;
|
|
762
|
+
const fx=f*dx/d,fy=f*dy/d;
|
|
763
|
+
l.s.vx+=fx;l.s.vy+=fy;l.t.vx-=fx;l.t.vy-=fy;
|
|
764
|
+
}
|
|
765
|
+
for(const n of nodes){
|
|
766
|
+
n.vx+=(w/2-n.x)*0.002;n.vy+=(h/2-n.y)*0.002; // gravity keeps it on screen
|
|
767
|
+
n.x+=(n.vx*=0.82)*alpha;n.y+=(n.vy*=0.82)*alpha;
|
|
768
|
+
}
|
|
769
|
+
alpha*=0.985;
|
|
770
|
+
}
|
|
771
|
+
ctx.setTransform(dpr,0,0,dpr,0,0);
|
|
772
|
+
ctx.clearRect(0,0,w,h);
|
|
773
|
+
ctx.save();ctx.translate(view.x,view.y);ctx.scale(view.k,view.k);
|
|
774
|
+
for(const l of links){
|
|
775
|
+
ctx.strokeStyle=G_EDGE[l.kind]||'rgba(148,163,184,0.25)';
|
|
776
|
+
ctx.lineWidth=l.kind==='continues'?2:Math.min(3,0.6+Math.log2(l.w));
|
|
777
|
+
ctx.beginPath();ctx.moveTo(l.s.x,l.s.y);ctx.lineTo(l.t.x,l.t.y);ctx.stroke();
|
|
778
|
+
}
|
|
779
|
+
for(const n of nodes){
|
|
780
|
+
ctx.fillStyle=G_COLORS[n.kind]||'#94a3b8';
|
|
781
|
+
ctx.globalAlpha=hover&&hover!==n?0.55:1;
|
|
782
|
+
ctx.beginPath();ctx.arc(n.x,n.y,n.r,0,6.2832);ctx.fill();
|
|
783
|
+
// Label only what carries the structure, else the canvas is unreadable.
|
|
784
|
+
if(n.r>7||n===hover){
|
|
785
|
+
ctx.globalAlpha=1;ctx.fillStyle='#cbd5e1';ctx.font='10px ui-monospace,monospace';
|
|
786
|
+
ctx.fillText(String(n.label||'').slice(0,26),n.x+n.r+3,n.y+3);
|
|
787
|
+
}
|
|
788
|
+
ctx.globalAlpha=1;
|
|
789
|
+
}
|
|
790
|
+
ctx.restore();
|
|
791
|
+
graphSim.raf=requestAnimationFrame(step);
|
|
792
|
+
}
|
|
793
|
+
graphSim={raf:0};step();
|
|
794
|
+
}
|
|
795
|
+
/* showTab() reads the clicked element off the global event; switching tabs
|
|
796
|
+
from code needs the button, so find it rather than fake an event. */
|
|
797
|
+
function showTabById(n){
|
|
798
|
+
const btn=[...document.querySelectorAll('.tab')].find(b=>b.getAttribute('onclick')==="showTab('"+n+"')");
|
|
799
|
+
if(btn)btn.click();
|
|
800
|
+
}
|
|
801
|
+
|
|
516
802
|
loadRuns();loadRecommend();loadScorecard();loadCategories();
|
|
517
803
|
setInterval(loadRuns,15000);setInterval(loadRecommend,30000);setInterval(loadScorecard,30000);setInterval(loadCategories,30000);
|
|
804
|
+
// Tasks refresh only while its tab is up — the query is server-paged and there
|
|
805
|
+
// is no reason to run it against the store every 15s in the background.
|
|
806
|
+
setInterval(()=>{if(document.getElementById('panel-tasks').classList.contains('visible'))loadTasks(taskOffset)},15000);
|
|
518
807
|
</script>
|
|
519
808
|
</body>
|
|
520
809
|
</html>`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard-html.js","sourceRoot":"","sources":["../../src/apme/dashboard-html.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,UAAU,iBAAiB;IAC/B,OAAO
|
|
1
|
+
{"version":3,"file":"dashboard-html.js","sourceRoot":"","sources":["../../src/apme/dashboard-html.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,UAAU,iBAAiB;IAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAgyBD,CAAC;AACT,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects APME rows into a property graph (see shared/src/apme-graph.ts for
|
|
3
|
+
* the model and for what the row schema does and does not already express).
|
|
4
|
+
*
|
|
5
|
+
* Derived on demand, never persisted: the containment spine comes straight from
|
|
6
|
+
* foreign keys, while session / project / model / agent / tool / file hubs are
|
|
7
|
+
* materialized from denormalized columns and tool payloads. Those hubs are the
|
|
8
|
+
* reason this exists — they are the only edges that connect work units sharing
|
|
9
|
+
* no ancestor.
|
|
10
|
+
*/
|
|
11
|
+
import type { ApmeGraphSlice } from '@agentdeck/shared';
|
|
12
|
+
import type { ApmeStore } from './store.js';
|
|
13
|
+
export interface ApmeGraphOptions {
|
|
14
|
+
/** Task units to include, newest first. The graph is anchored on tasks
|
|
15
|
+
* because they are the canonical work unit. */
|
|
16
|
+
limit?: number;
|
|
17
|
+
agentType?: string;
|
|
18
|
+
projectName?: string;
|
|
19
|
+
category?: string;
|
|
20
|
+
/** Include turn nodes and their tool/file edges. Off keeps the slice at
|
|
21
|
+
* run/task altitude, which is what a whole-history overview wants. */
|
|
22
|
+
includeTurns?: boolean;
|
|
23
|
+
/** Include file nodes derived from tool payloads. */
|
|
24
|
+
includeFiles?: boolean;
|
|
25
|
+
/** Drop tool/file hubs referenced fewer than this many times — the long tail
|
|
26
|
+
* of one-off paths otherwise buries the structure. */
|
|
27
|
+
minHubDegree?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Pull a file path out of a stored tool payload. Returns null for tools that
|
|
30
|
+
* do not take one (Bash, WebFetch, …) — partial coverage is expected, and the
|
|
31
|
+
* slice reports it rather than implying every tool touched a file. */
|
|
32
|
+
export declare function filePathFromToolPayload(payload: string | null | undefined): string | null;
|
|
33
|
+
/** Identify a file by its path relative to the run's project root, so the same
|
|
34
|
+
* file edited from a git worktree and from the main checkout lands on ONE node.
|
|
35
|
+
*
|
|
36
|
+
* The project root is what makes this correct rather than lucky: a fixed
|
|
37
|
+
* "last N segments" rule merges two checkouts only when their repo-relative
|
|
38
|
+
* paths happen to be exactly N deep, and quietly splits the node otherwise
|
|
39
|
+
* (`.../AgentDeck/src/a.ts` vs `.../__worktrees/wt1/src/a.ts` — same file,
|
|
40
|
+
* different tails). Worktrees are routine here, so that failure would be too.
|
|
41
|
+
*
|
|
42
|
+
* With no known root — or a path outside it, like a system header — fall back
|
|
43
|
+
* to the last three segments: unambiguous enough to read, short enough to
|
|
44
|
+
* render, and it never claims two paths are the same file. */
|
|
45
|
+
export declare function shortenPath(path: string, projectPath?: string | null): string;
|
|
46
|
+
/** Build a graph slice anchored on the most recent task units. */
|
|
47
|
+
export declare function buildApmeGraph(store: ApmeStore, opts?: ApmeGraphOptions): ApmeGraphSlice;
|
|
48
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/apme/graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACuC,cAAc,EAChE,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,WAAW,gBAAgB;IAC/B;oDACgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;2EACuE;IACvE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,qDAAqD;IACrD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;2DACuD;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAKD;;uEAEuE;AACvE,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAYzF;AAED;;;;;;;;;;;+DAW+D;AAC/D,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAO7E;AAsED,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,GAAE,gBAAqB,GAAG,cAAc,CAoG5F"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects APME rows into a property graph (see shared/src/apme-graph.ts for
|
|
3
|
+
* the model and for what the row schema does and does not already express).
|
|
4
|
+
*
|
|
5
|
+
* Derived on demand, never persisted: the containment spine comes straight from
|
|
6
|
+
* foreign keys, while session / project / model / agent / tool / file hubs are
|
|
7
|
+
* materialized from denormalized columns and tool payloads. Those hubs are the
|
|
8
|
+
* reason this exists — they are the only edges that connect work units sharing
|
|
9
|
+
* no ancestor.
|
|
10
|
+
*/
|
|
11
|
+
import { apmeGraphNodeId as nid } from '@agentdeck/shared';
|
|
12
|
+
/** Tool-input keys that name a filesystem path, in priority order. */
|
|
13
|
+
const PATH_KEYS = ['file_path', 'filePath', 'path', 'notebook_path', 'file'];
|
|
14
|
+
/** Pull a file path out of a stored tool payload. Returns null for tools that
|
|
15
|
+
* do not take one (Bash, WebFetch, …) — partial coverage is expected, and the
|
|
16
|
+
* slice reports it rather than implying every tool touched a file. */
|
|
17
|
+
export function filePathFromToolPayload(payload) {
|
|
18
|
+
if (!payload)
|
|
19
|
+
return null;
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(payload);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (!parsed || typeof parsed !== 'object')
|
|
28
|
+
return null;
|
|
29
|
+
const input = parsed.input;
|
|
30
|
+
const src = (input && typeof input === 'object' ? input : parsed);
|
|
31
|
+
for (const key of PATH_KEYS) {
|
|
32
|
+
const v = src[key];
|
|
33
|
+
if (typeof v === 'string' && v.trim().length > 0)
|
|
34
|
+
return v.trim();
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/** Identify a file by its path relative to the run's project root, so the same
|
|
39
|
+
* file edited from a git worktree and from the main checkout lands on ONE node.
|
|
40
|
+
*
|
|
41
|
+
* The project root is what makes this correct rather than lucky: a fixed
|
|
42
|
+
* "last N segments" rule merges two checkouts only when their repo-relative
|
|
43
|
+
* paths happen to be exactly N deep, and quietly splits the node otherwise
|
|
44
|
+
* (`.../AgentDeck/src/a.ts` vs `.../__worktrees/wt1/src/a.ts` — same file,
|
|
45
|
+
* different tails). Worktrees are routine here, so that failure would be too.
|
|
46
|
+
*
|
|
47
|
+
* With no known root — or a path outside it, like a system header — fall back
|
|
48
|
+
* to the last three segments: unambiguous enough to read, short enough to
|
|
49
|
+
* render, and it never claims two paths are the same file. */
|
|
50
|
+
export function shortenPath(path, projectPath) {
|
|
51
|
+
if (projectPath) {
|
|
52
|
+
const root = projectPath.endsWith('/') ? projectPath : projectPath + '/';
|
|
53
|
+
if (path.startsWith(root))
|
|
54
|
+
return path.slice(root.length);
|
|
55
|
+
}
|
|
56
|
+
const parts = path.split('/').filter(Boolean);
|
|
57
|
+
return parts.length <= 3 ? parts.join('/') : parts.slice(-3).join('/');
|
|
58
|
+
}
|
|
59
|
+
class GraphBuilder {
|
|
60
|
+
nodes = new Map();
|
|
61
|
+
edges = new Map();
|
|
62
|
+
node(n) {
|
|
63
|
+
const existing = this.nodes.get(n.id);
|
|
64
|
+
if (existing) {
|
|
65
|
+
// First writer wins on labels; later mentions only enrich empty fields so
|
|
66
|
+
// a hub referenced from many rows keeps one stable identity.
|
|
67
|
+
if (existing.ts == null && n.ts != null)
|
|
68
|
+
existing.ts = n.ts;
|
|
69
|
+
if (existing.score == null && n.score != null)
|
|
70
|
+
existing.score = n.score;
|
|
71
|
+
return n.id;
|
|
72
|
+
}
|
|
73
|
+
this.nodes.set(n.id, n);
|
|
74
|
+
return n.id;
|
|
75
|
+
}
|
|
76
|
+
edge(from, to, kind) {
|
|
77
|
+
const key = `${from}|${to}|${kind}`;
|
|
78
|
+
const existing = this.edges.get(key);
|
|
79
|
+
// Parallel edges collapse into one weighted edge — 12 Read calls on the
|
|
80
|
+
// same file is one relationship of strength 12, not 12 relationships.
|
|
81
|
+
if (existing) {
|
|
82
|
+
existing.weight = (existing.weight ?? 1) + 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.edges.set(key, { from, to, kind, weight: 1 });
|
|
86
|
+
}
|
|
87
|
+
/** Drop low-degree hubs and any edge left dangling by that removal.
|
|
88
|
+
* Containment nodes are never pruned — they are the slice's subject. */
|
|
89
|
+
pruneHubs(minDegree) {
|
|
90
|
+
if (minDegree <= 1)
|
|
91
|
+
return;
|
|
92
|
+
const degree = new Map();
|
|
93
|
+
for (const e of this.edges.values()) {
|
|
94
|
+
degree.set(e.from, (degree.get(e.from) ?? 0) + 1);
|
|
95
|
+
degree.set(e.to, (degree.get(e.to) ?? 0) + 1);
|
|
96
|
+
}
|
|
97
|
+
const prunable = new Set(['tool', 'file']);
|
|
98
|
+
const dropped = new Set();
|
|
99
|
+
for (const n of this.nodes.values()) {
|
|
100
|
+
if (prunable.has(n.kind) && (degree.get(n.id) ?? 0) < minDegree) {
|
|
101
|
+
dropped.add(n.id);
|
|
102
|
+
this.nodes.delete(n.id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (dropped.size === 0)
|
|
106
|
+
return;
|
|
107
|
+
for (const [key, e] of this.edges) {
|
|
108
|
+
if (dropped.has(e.from) || dropped.has(e.to))
|
|
109
|
+
this.edges.delete(key);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
finish() {
|
|
113
|
+
const degree = new Map();
|
|
114
|
+
for (const e of this.edges.values()) {
|
|
115
|
+
degree.set(e.from, (degree.get(e.from) ?? 0) + 1);
|
|
116
|
+
degree.set(e.to, (degree.get(e.to) ?? 0) + 1);
|
|
117
|
+
}
|
|
118
|
+
const nodes = [...this.nodes.values()].map((n) => ({ ...n, degree: degree.get(n.id) ?? 0 }));
|
|
119
|
+
return { nodes, edges: [...this.edges.values()] };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function num(v) {
|
|
123
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
124
|
+
}
|
|
125
|
+
function str(v) {
|
|
126
|
+
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
127
|
+
}
|
|
128
|
+
/** Build a graph slice anchored on the most recent task units. */
|
|
129
|
+
export function buildApmeGraph(store, opts = {}) {
|
|
130
|
+
const limit = Math.min(Math.max(opts.limit ?? 60, 1), 400);
|
|
131
|
+
const includeTurns = opts.includeTurns ?? true;
|
|
132
|
+
const includeFiles = opts.includeFiles ?? true;
|
|
133
|
+
const minHubDegree = Math.max(opts.minHubDegree ?? 2, 1);
|
|
134
|
+
const page = store.listTaskPage({
|
|
135
|
+
limit,
|
|
136
|
+
...(opts.agentType ? { agentType: opts.agentType } : {}),
|
|
137
|
+
...(opts.projectName ? { projectName: opts.projectName } : {}),
|
|
138
|
+
...(opts.category ? { category: opts.category } : {}),
|
|
139
|
+
});
|
|
140
|
+
const g = new GraphBuilder();
|
|
141
|
+
let toolEvents = 0;
|
|
142
|
+
let toolEventsWithPath = 0;
|
|
143
|
+
for (const t of page.tasks) {
|
|
144
|
+
const runId = t.runId;
|
|
145
|
+
const runNode = g.node({
|
|
146
|
+
id: nid.run(runId), kind: 'run', label: runId.slice(0, 8),
|
|
147
|
+
ts: t.startedAt,
|
|
148
|
+
meta: { project: t.projectName, agent: t.agentType, model: t.modelId },
|
|
149
|
+
});
|
|
150
|
+
const taskNode = g.node({
|
|
151
|
+
id: nid.task(t.id), kind: 'task',
|
|
152
|
+
label: (t.summary || t.firstPrompt || `Task ${t.taskIndex}`).slice(0, 60),
|
|
153
|
+
ts: t.startedAt,
|
|
154
|
+
score: t.overallScore,
|
|
155
|
+
meta: {
|
|
156
|
+
outcome: t.outcome ?? null,
|
|
157
|
+
category: t.taskCategory ?? null,
|
|
158
|
+
boundary: t.boundarySignal ?? null,
|
|
159
|
+
turns: t.turnCount,
|
|
160
|
+
answered: t.answeredTurns,
|
|
161
|
+
tools: t.toolCount,
|
|
162
|
+
cost: t.costUsd ?? null,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
g.edge(runNode, taskNode, 'contains');
|
|
166
|
+
// ── Derived entity hubs ──
|
|
167
|
+
g.edge(g.node({ id: nid.session(t.sessionId), kind: 'session', label: t.sessionId.slice(0, 12) }), runNode, 'produced');
|
|
168
|
+
g.edge(g.node({ id: nid.agent(t.agentType), kind: 'agent', label: t.agentType }), runNode, 'produced');
|
|
169
|
+
if (t.projectName) {
|
|
170
|
+
g.edge(g.node({ id: nid.project(t.projectName), kind: 'project', label: t.projectName }), runNode, 'produced');
|
|
171
|
+
}
|
|
172
|
+
if (t.modelId) {
|
|
173
|
+
g.edge(g.node({ id: nid.model(t.modelId), kind: 'model', label: t.modelId }), runNode, 'produced');
|
|
174
|
+
}
|
|
175
|
+
// `/clear` continuation. The parent run is only drawn when it is already in
|
|
176
|
+
// the slice — a stub node for an out-of-slice run would read as real work.
|
|
177
|
+
if (t.parentRunId && page.tasks.some((o) => o.runId === t.parentRunId)) {
|
|
178
|
+
g.edge(nid.run(t.parentRunId), runNode, 'continues');
|
|
179
|
+
}
|
|
180
|
+
// ── Trajectory: tools and files ──
|
|
181
|
+
const events = store.listSampleEventRows(t.id);
|
|
182
|
+
for (const ev of events) {
|
|
183
|
+
if (ev.kind !== 'tool')
|
|
184
|
+
continue;
|
|
185
|
+
toolEvents++;
|
|
186
|
+
const toolName = str(ev.toolName);
|
|
187
|
+
const turnId = str(ev.turnId);
|
|
188
|
+
// Attach to the turn when we have one and turns are shown; otherwise the
|
|
189
|
+
// task carries the relationship so the edge is never silently dropped.
|
|
190
|
+
let anchor = taskNode;
|
|
191
|
+
if (includeTurns && turnId) {
|
|
192
|
+
anchor = g.node({
|
|
193
|
+
id: nid.turn(turnId), kind: 'turn',
|
|
194
|
+
label: `turn ${ev.turnIndex ?? '?'}`,
|
|
195
|
+
ts: ev.ts,
|
|
196
|
+
meta: { index: num(ev.turnIndex) },
|
|
197
|
+
});
|
|
198
|
+
g.edge(taskNode, anchor, 'contains');
|
|
199
|
+
}
|
|
200
|
+
if (toolName) {
|
|
201
|
+
g.edge(anchor, g.node({ id: nid.tool(toolName), kind: 'tool', label: toolName }), 'used');
|
|
202
|
+
}
|
|
203
|
+
const path = filePathFromToolPayload(ev.payload);
|
|
204
|
+
if (path)
|
|
205
|
+
toolEventsWithPath++;
|
|
206
|
+
if (path && includeFiles) {
|
|
207
|
+
const short = shortenPath(path, t.projectPath);
|
|
208
|
+
g.edge(anchor, g.node({ id: nid.file(short), kind: 'file', label: short, meta: { full: path } }), 'touched');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
g.pruneHubs(minHubDegree);
|
|
213
|
+
const { nodes, edges } = g.finish();
|
|
214
|
+
return {
|
|
215
|
+
nodes,
|
|
216
|
+
edges,
|
|
217
|
+
stats: {
|
|
218
|
+
taskCount: page.tasks.length,
|
|
219
|
+
nodeCount: nodes.length,
|
|
220
|
+
edgeCount: edges.length,
|
|
221
|
+
truncatedTasks: Math.max(0, page.total - page.tasks.length),
|
|
222
|
+
fileCoverage: { toolEvents, withPath: toolEventsWithPath },
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=graph.js.map
|