@agentdeck/bridge 1.1.0 → 1.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 (107) hide show
  1. package/README.md +14 -6
  2. package/assets/fonts/NOTICE-PocketSansWorld.md +10 -0
  3. package/assets/fonts/OFL-PocketSansWorld.txt +93 -0
  4. package/assets/fonts/PocketSansWorld_12.cpfont +0 -0
  5. package/assets/fonts/pocket-sans-world.manifest.json +49 -0
  6. package/assets/learning/LICENSE-CC-BY-SA-4.0.txt +10 -0
  7. package/assets/learning/NOTICE.md +33 -0
  8. package/assets/learning/jp-n3-ko.manifest.json +27 -0
  9. package/assets/learning/jp-n3-ko.pdl +0 -0
  10. package/dist/agent-cli-diagnostics.d.ts +20 -0
  11. package/dist/agent-cli-diagnostics.d.ts.map +1 -0
  12. package/dist/agent-cli-diagnostics.js +100 -0
  13. package/dist/agent-cli-diagnostics.js.map +1 -0
  14. package/dist/apme/collector.d.ts +18 -1
  15. package/dist/apme/collector.d.ts.map +1 -1
  16. package/dist/apme/collector.js +48 -3
  17. package/dist/apme/collector.js.map +1 -1
  18. package/dist/apme/dashboard-html.d.ts.map +1 -1
  19. package/dist/apme/dashboard-html.js +275 -35
  20. package/dist/apme/dashboard-html.js.map +1 -1
  21. package/dist/apme/http.d.ts.map +1 -1
  22. package/dist/apme/http.js +33 -5
  23. package/dist/apme/http.js.map +1 -1
  24. package/dist/apme/index.d.ts.map +1 -1
  25. package/dist/apme/index.js +6 -2
  26. package/dist/apme/index.js.map +1 -1
  27. package/dist/apme/store.d.ts +41 -1
  28. package/dist/apme/store.d.ts.map +1 -1
  29. package/dist/apme/store.js +153 -27
  30. package/dist/apme/store.js.map +1 -1
  31. package/dist/bridge-core.d.ts +15 -0
  32. package/dist/bridge-core.d.ts.map +1 -1
  33. package/dist/bridge-core.js +30 -4
  34. package/dist/bridge-core.js.map +1 -1
  35. package/dist/cli.d.ts +11 -0
  36. package/dist/cli.d.ts.map +1 -1
  37. package/dist/cli.js +55 -8
  38. package/dist/cli.js.map +1 -1
  39. package/dist/codex-rate-limits-live.d.ts +180 -10
  40. package/dist/codex-rate-limits-live.d.ts.map +1 -1
  41. package/dist/codex-rate-limits-live.js +475 -22
  42. package/dist/codex-rate-limits-live.js.map +1 -1
  43. package/dist/daemon-server.d.ts.map +1 -1
  44. package/dist/daemon-server.js +263 -1
  45. package/dist/daemon-server.js.map +1 -1
  46. package/dist/daemon-ws-client.d.ts +1 -0
  47. package/dist/daemon-ws-client.d.ts.map +1 -1
  48. package/dist/daemon-ws-client.js +25 -16
  49. package/dist/daemon-ws-client.js.map +1 -1
  50. package/dist/daemon.js +1 -1
  51. package/dist/esp32-serial.d.ts +4 -0
  52. package/dist/esp32-serial.d.ts.map +1 -1
  53. package/dist/esp32-serial.js +4 -0
  54. package/dist/esp32-serial.js.map +1 -1
  55. package/dist/font-pack.d.ts +15 -0
  56. package/dist/font-pack.d.ts.map +1 -0
  57. package/dist/font-pack.js +75 -0
  58. package/dist/font-pack.js.map +1 -0
  59. package/dist/glance-frame.d.ts.map +1 -1
  60. package/dist/glance-frame.js +2 -0
  61. package/dist/glance-frame.js.map +1 -1
  62. package/dist/http-auth-gate.d.ts.map +1 -1
  63. package/dist/http-auth-gate.js +9 -1
  64. package/dist/http-auth-gate.js.map +1 -1
  65. package/dist/index.js +2 -2
  66. package/dist/index.js.map +1 -1
  67. package/dist/learning-pack.d.ts +24 -0
  68. package/dist/learning-pack.d.ts.map +1 -0
  69. package/dist/learning-pack.js +125 -0
  70. package/dist/learning-pack.js.map +1 -0
  71. package/dist/mdns.js +1 -1
  72. package/dist/mdns.js.map +1 -1
  73. package/dist/pairing-knocks.d.ts +80 -0
  74. package/dist/pairing-knocks.d.ts.map +1 -0
  75. package/dist/pairing-knocks.js +312 -0
  76. package/dist/pairing-knocks.js.map +1 -0
  77. package/dist/pixoo/micro-glyphs.d.ts +4 -0
  78. package/dist/pixoo/micro-glyphs.d.ts.map +1 -1
  79. package/dist/pixoo/micro-glyphs.js +29 -0
  80. package/dist/pixoo/micro-glyphs.js.map +1 -1
  81. package/dist/pixoo/pixoo-font.d.ts.map +1 -1
  82. package/dist/pixoo/pixoo-font.js +3 -1
  83. package/dist/pixoo/pixoo-font.js.map +1 -1
  84. package/dist/pixoo/pixoo-renderer.d.ts +1 -1
  85. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  86. package/dist/pixoo/pixoo-renderer.js +17 -2
  87. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  88. package/dist/pty-manager.d.ts +9 -0
  89. package/dist/pty-manager.d.ts.map +1 -1
  90. package/dist/pty-manager.js +63 -4
  91. package/dist/pty-manager.js.map +1 -1
  92. package/dist/relayed-usage.d.ts +30 -0
  93. package/dist/relayed-usage.d.ts.map +1 -1
  94. package/dist/relayed-usage.js +25 -2
  95. package/dist/relayed-usage.js.map +1 -1
  96. package/dist/surface-protocol.d.ts.map +1 -1
  97. package/dist/surface-protocol.js +2 -0
  98. package/dist/surface-protocol.js.map +1 -1
  99. package/dist/tui/dashboard.d.ts.map +1 -1
  100. package/dist/tui/dashboard.js +8 -1
  101. package/dist/tui/dashboard.js.map +1 -1
  102. package/dist/ws-server.d.ts +18 -0
  103. package/dist/ws-server.d.ts.map +1 -1
  104. package/dist/ws-server.js +40 -2
  105. package/dist/ws-server.js.map +1 -1
  106. package/package.json +4 -3
  107. package/src/timebox/sync_ble.py +35 -16
@@ -13,7 +13,26 @@ export function apmeDashboardHtml() {
13
13
  <meta name="viewport" content="width=device-width,initial-scale=1">
14
14
  <title>AgentDeck — Agent Performance Monitoring & Evaluation</title>
15
15
  <style>
16
- :root{--bg:#0f172a;--surface:#1e293b;--border:#334155;--text:#e2e8f0;--muted:#94a3b8;--dim:#64748b;--green:#22c55e;--yellow:#f59e0b;--red:#ef4444;--blue:#3b82f6;--accent:#818cf8}
16
+ /* Palette derived from design/tokens.css. The dashboard ships as one
17
+ * self-contained HTML string (WKWebView loads the bundled copy via the
18
+ * in-process daemon), so the canonical CSS file can't be <link>'d: the
19
+ * :root primitives below are a manual mirror of design/tokens.css §2,
20
+ * gated by design/verify-tokens-sync.py against the bundled resource —
21
+ * which is byte-identical to this renderer's output. Everything below
22
+ * :root references var(--…) only; the checker flags any stray hex. */
23
+ :root{
24
+ /* === Design system primitives (mirrors design/tokens.css §2) === */
25
+ --ink-800:#15302f;--ink-700:#1f4544;--ink-500:#426664;--ink-300:#7c9694;
26
+ --kelp-500:#2f8a7c;--kelp-300:#6fb6a8;
27
+ --coral-500:#c0573a;--amber-500:#c8923a;
28
+ --ui-ok:#52D988;--ui-attn:#FFA93D;--ui-error:#FF6B6B;--ui-cyan:#3ED6E8;
29
+ --ui-idle-dark:#7a8a9c;--ui-popup-bg-dark:#0a1a2a;--ui-tty-text:#c8d0d8;
30
+ /* === Dashboard-local semantic aliases === */
31
+ --bg:var(--ui-popup-bg-dark);--surface:var(--ink-800);--border:var(--ink-700);
32
+ --text:var(--ui-tty-text);--muted:var(--ui-idle-dark);--dim:var(--ink-500);
33
+ --green:var(--ui-ok);--yellow:var(--ui-attn);--red:var(--ui-error);--blue:var(--ui-cyan);
34
+ --accent:var(--kelp-500);--amber:var(--amber-500);--orange:var(--ui-attn)
35
+ }
17
36
  *{margin:0;padding:0;box-sizing:border-box}
18
37
  body{background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,sans-serif;font-size:13px;height:100vh;overflow:hidden}
19
38
 
@@ -39,10 +58,10 @@ header .status{margin-left:auto;font-size:11px;color:var(--dim)}
39
58
  .table-wrap{flex:1;overflow-y:auto}
40
59
  table{width:100%;border-collapse:collapse;font-size:12px}
41
60
  th{position:sticky;top:0;text-align:left;color:var(--dim);font-weight:500;padding:8px 10px;background:var(--surface);border-bottom:1px solid var(--border);font-size:11px;text-transform:uppercase;letter-spacing:0.3px}
42
- td{padding:7px 10px;border-bottom:1px solid rgba(51,65,85,0.5);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}
61
+ td{padding:7px 10px;border-bottom:1px solid color-mix(in srgb,var(--border) 50%,transparent);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}
43
62
  tr{cursor:pointer;transition:background 0.1s}
44
- tr:hover td{background:rgba(30,42,59,0.8)}
45
- tr.selected td{background:rgba(99,102,241,0.15);border-left:2px solid var(--accent)}
63
+ tr:hover td{background:color-mix(in srgb,var(--surface) 80%,transparent)}
64
+ tr.selected td{background:color-mix(in srgb,var(--accent) 15%,transparent);border-left:2px solid var(--accent)}
46
65
  .task-col{max-width:250px;color:var(--muted);font-style:italic}
47
66
 
48
67
  /* ── Scores / badges ── */
@@ -52,12 +71,12 @@ tr.selected td{background:rgba(99,102,241,0.15);border-left:2px solid var(--acce
52
71
  .score-low{color:var(--red)}
53
72
  .score-na{color:var(--dim)}
54
73
  .badge{font-size:10px;padding:2px 8px;border-radius:10px;font-weight:600;display:inline-block}
55
- .badge-committed{background:#166534;color:#bbf7d0}
56
- .badge-abandoned{background:#7f1d1d;color:#fecaca}
57
- .badge-iterated{background:#78350f;color:#fef3c7}
74
+ .badge-committed{background:color-mix(in srgb,var(--green) 18%,var(--bg));color:var(--green)}
75
+ .badge-abandoned{background:color-mix(in srgb,var(--red) 18%,var(--bg));color:var(--red)}
76
+ .badge-iterated{background:color-mix(in srgb,var(--yellow) 18%,var(--bg));color:var(--yellow)}
58
77
  .badge-exploratory{background:var(--surface);color:var(--muted);border:1px solid var(--border)}
59
78
  .badge-pending{background:var(--surface);color:var(--dim)}
60
- .badge-cat{background:var(--surface);color:var(--accent);border:1px solid rgba(129,140,248,0.3);font-size:10px;padding:1px 6px;border-radius:4px}
79
+ .badge-cat{background:var(--surface);color:var(--accent);border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);font-size:10px;padding:1px 6px;border-radius:4px}
61
80
 
62
81
  /* ── Detail panel ── */
63
82
  .detail-empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dim);font-size:14px}
@@ -93,11 +112,35 @@ tr.selected td{background:rgba(99,102,241,0.15);border-left:2px solid var(--acce
93
112
  .vibe-bar{display:flex;gap:8px;margin-top:16px;padding-top:12px;border-top:1px solid var(--border)}
94
113
  .vibe-btn{padding:8px 20px;border-radius:8px;border:none;cursor:pointer;font-size:13px;font-weight:600;transition:all 0.15s}
95
114
  .vibe-btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.3)}
96
- .vibe-approve{background:#166534;color:#bbf7d0}
97
- .vibe-reject{background:#7f1d1d;color:#fecaca}
115
+ .vibe-approve{background:color-mix(in srgb,var(--green) 22%,var(--bg));color:var(--green)}
116
+ .vibe-reject{background:color-mix(in srgb,var(--red) 22%,var(--bg));color:var(--red)}
98
117
  .vibe-current{font-size:11px;color:var(--dim);display:flex;align-items:center;margin-left:8px}
99
118
 
100
119
  .panel{display:none}.panel.visible{display:block;height:100%}
120
+
121
+ /* ── Work board ── */
122
+ .work-tabs{display:flex;gap:4px;padding:8px 10px;background:var(--surface);border-bottom:1px solid var(--border);flex-wrap:wrap;align-items:center}
123
+ .work-tab{padding:3px 10px;border-radius:10px;font-size:11px;color:var(--dim);cursor:pointer;border:1px solid transparent;background:none}
124
+ .work-tab:hover{color:var(--muted)}
125
+ .work-tab.active{background:color-mix(in srgb,var(--accent) 15%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 30%,transparent)}
126
+ .work-tab .n{opacity:0.75;margin-left:3px;font-variant-numeric:tabular-nums}
127
+ .work-row{padding:9px 12px;border-bottom:1px solid color-mix(in srgb,var(--border) 50%,transparent);cursor:pointer}
128
+ .work-row:hover{background:color-mix(in srgb,var(--surface) 80%,transparent)}
129
+ .work-row.selected{background:color-mix(in srgb,var(--accent) 15%,transparent)}
130
+ .work-row.attn{box-shadow:inset 3px 0 0 var(--yellow)}
131
+ .work-title{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--text);font-weight:600;min-width:0}
132
+ .work-title .t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
133
+ .work-title .t.unnamed{color:var(--muted);font-weight:500;font-style:italic}
134
+ .work-sub{display:flex;gap:10px;flex-wrap:wrap;align-items:center;font-size:10.5px;color:var(--dim);margin-top:3px;font-variant-numeric:tabular-nums}
135
+ .session-link{appearance:none;background:transparent;color:var(--accent);border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);border-radius:9px;padding:1px 6px;font:inherit;cursor:pointer;white-space:nowrap}
136
+ .session-link:hover{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--text)}
137
+ .work-scope{display:none;align-items:center;gap:8px;padding:6px 10px;background:color-mix(in srgb,var(--accent) 9%,var(--surface));border-bottom:1px solid color-mix(in srgb,var(--accent) 25%,var(--border));font-size:11px;color:var(--muted)}
138
+ .work-scope code{color:var(--text);font-size:10.5px}
139
+ .work-scope .clear{margin-left:auto;background:transparent;color:var(--muted);border:1px solid var(--border);border-radius:9px;padding:1px 7px;cursor:pointer}
140
+ .chip{font-size:10px;padding:1px 7px;border-radius:9px;background:var(--surface);color:var(--muted);border:1px solid var(--border);white-space:nowrap}
141
+ .chip-ok{background:color-mix(in srgb,var(--green) 12%,transparent);color:var(--green);border-color:color-mix(in srgb,var(--green) 30%,transparent)}
142
+ .chip-warn{background:color-mix(in srgb,var(--yellow) 12%,transparent);color:var(--yellow);border-color:color-mix(in srgb,var(--yellow) 30%,transparent)}
143
+ .chip-bad{background:color-mix(in srgb,var(--red) 12%,transparent);color:var(--red);border-color:color-mix(in srgb,var(--red) 30%,transparent)}
101
144
  /* Graph needs a column flex box so the canvas can claim the leftover height. */
102
145
  .panel#panel-graph.visible{display:flex;flex-direction:column}
103
146
  .activity-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;padding:10px;background:var(--surface);border-bottom:1px solid var(--border)}
@@ -118,7 +161,8 @@ tr.selected td{background:rgba(99,102,241,0.15);border-left:2px solid var(--acce
118
161
  <!-- Left: runs list + tabs -->
119
162
  <div class="left">
120
163
  <div class="tabs">
121
- <button class="tab active" onclick="showTab('activity')">Activity</button>
164
+ <button class="tab active" onclick="showTab('work')">Work</button>
165
+ <button class="tab" onclick="showTab('activity')">Activity</button>
122
166
  <button class="tab" onclick="showTab('runs')">Runs</button>
123
167
  <button class="tab" onclick="showTab('tasks')">Tasks</button>
124
168
  <button class="tab" onclick="showTab('graph')">Graph</button>
@@ -127,7 +171,23 @@ tr.selected td{background:rgba(99,102,241,0.15);border-left:2px solid var(--acce
127
171
  <button class="tab" onclick="showTab('categories')">Categories</button>
128
172
  </div>
129
173
  <div class="table-wrap">
130
- <div class="panel visible" id="panel-activity">
174
+ <!-- Work board: task-first default view. One row = intent title +
175
+ outcome/score, sub-line = boundary provenance chip + folded actions.
176
+ Lifecycle sub-tabs are triage (attention first, then recency) and
177
+ their badge counts come from the same SQL bucket definitions as the
178
+ filter, so tab and rows cannot disagree. -->
179
+ <div class="panel visible" id="panel-work">
180
+ <div class="work-tabs" id="work-tabs"></div>
181
+ <div style="display:flex;gap:6px;padding:6px 10px;background:var(--surface);border-bottom:1px solid var(--border);flex-wrap:wrap">
182
+ <input id="w-q" placeholder="Search prompt / summary" oninput="workSearchDebounced()" 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">
183
+ <select id="w-agent" onchange="loadWork(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>
184
+ <select id="w-project" onchange="loadWork(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>
185
+ </div>
186
+ <div class="work-scope" id="work-scope"></div>
187
+ <div id="work-rows"></div>
188
+ <div id="work-pager" style="display:flex;gap:8px;align-items:center;justify-content:center;padding:10px;font-size:11px;color:var(--dim)"></div>
189
+ </div>
190
+ <div class="panel" id="panel-activity">
131
191
  <div class="activity-cards" id="activity-cards"></div>
132
192
  <table><thead><tr>
133
193
  <th>Agent</th><th>Task</th><th>Project</th><th>When</th><th>Time</th>
@@ -207,6 +267,7 @@ function showTab(n){
207
267
  if(n==='tasks'&&!tasksLoaded){tasksLoaded=true;loadTasks(0)}
208
268
  if(n==='graph'){requestAnimationFrame(()=>loadGraph())}
209
269
  if(n==='activity'){loadActivity()}
270
+ if(n==='work'){loadWork(workOffset)}
210
271
  }
211
272
  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>'}
212
273
  function fo(o){if(!o)return'';return'<span class="badge badge-'+o+'">'+o+'</span>'}
@@ -221,7 +282,8 @@ async function loadActivity(){
221
282
  cards.innerHTML=(d.agents||[]).map(a=>'<div class="activity-card"><div class="agent">'+esc(a.agentType)+'</div><div class="time">'+fd(a.durationMs)+'</div><div class="count">'+a.taskCount+' task'+(a.taskCount===1?'':'s')+'</div></div>').join('')||'<div class="empty">No activity yet</div>';
222
283
  const body=document.getElementById('activity-body');
223
284
  body.innerHTML=activityRows.map((x,i)=>'<tr onclick="selectActivity('+i+')"><td>'+esc(x.agentType)+'</td><td class="task-col" title="'+esc(x.task)+'">'+esc(x.task.slice(0,90))+'</td><td>'+esc(x.projectName||'—')+'</td><td>'+new Date(x.startedAt).toLocaleString([],{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})+'</td><td>'+fd(x.durationMs)+'</td></tr>').join('')||'<tr><td colspan="5" class="empty">No collected tasks yet</td></tr>';
224
- document.getElementById('status').textContent=activityRows.length+' tasks · '+new Date().toLocaleTimeString();
285
+ if(document.getElementById('panel-activity').classList.contains('visible'))
286
+ document.getElementById('status').textContent=activityRows.length+' tasks · '+new Date().toLocaleTimeString();
225
287
  }catch(e){document.getElementById('status').textContent='Error: '+e.message}
226
288
  }
227
289
  function selectActivity(i){
@@ -323,6 +385,7 @@ async function selectRun(id){
323
385
  const startedAt=run.started_at??run.startedAt;
324
386
  const endedAt=run.ended_at??run.endedAt;
325
387
  const agentType=run.agent_type??run.agentType??'';
388
+ const sessionId=run.session_id??run.sessionId??'';
326
389
  const modelId=run.model_id??run.modelId??'—';
327
390
  const projectName=run.project_name??run.projectName??'—';
328
391
  const taskPrompt=run.task_prompt??run.taskPrompt;
@@ -342,6 +405,7 @@ async function selectRun(id){
342
405
  h+='<span>🕐 '+tm+(dur?' · '+fd(dur):'')+'</span>';
343
406
  if(taskCategory)h+='<span><span class="badge-cat">'+taskCategory+'</span></span>';
344
407
  h+='<span style="font-family:monospace;font-size:11px;color:var(--dim);cursor:pointer" onclick="navigator.clipboard.writeText(\\''+run.id+'\\');this.textContent=\\'copied!\\';;setTimeout(()=>this.textContent=\\''+run.id.slice(0,12)+'\\',1000)" title="Click to copy full ID">'+run.id.slice(0,12)+'</span>';
408
+ if(sessionId)h+='<button class="session-link" data-session="'+esc(sessionId)+'" data-project="'+esc(projectName==='—'?'':projectName)+'" data-agent="'+esc(agentType)+'" onclick="openWorkSessionFrom(this,event)" title="Open every task collected from this native session">View session work</button>';
345
409
  h+='</div></div>';
346
410
 
347
411
  // Task prompt
@@ -352,7 +416,7 @@ async function selectRun(id){
352
416
 
353
417
  // Active session notice
354
418
  if(!endedAt){
355
- h+='<div class="section" style="padding:10px 12px;background:rgba(59,130,246,0.15);border:1px solid rgba(59,130,246,0.3);border-radius:6px;color:var(--blue);font-size:12px">⏳ Session active — score, category, and outcome appear after the session ends.</div>';
419
+ h+='<div class="section" style="padding:10px 12px;background:color-mix(in srgb,var(--blue) 15%,transparent);border:1px solid color-mix(in srgb,var(--blue) 30%,transparent);border-radius:6px;color:var(--blue);font-size:12px">⏳ Session active — score, category, and outcome appear after the session ends.</div>';
356
420
  }else{
357
421
  // Completed but un-scored: no composite, no LLM/task/turn judge, no
358
422
  // deterministic, no manual review. Without this, the panel reads as a
@@ -362,7 +426,7 @@ async function selectRun(id){
362
426
  const scored=compScore!=null||evals.length>0;
363
427
  if(!scored){
364
428
  const reason=layer1Skipped?('deterministic checks skipped ('+esc(layer1Skipped)+') and no LLM judge ran'):'no evaluation ran for this task';
365
- h+='<div class="section" style="padding:10px 12px;background:rgba(148,163,184,0.12);border:1px solid var(--border);border-radius:6px;font-size:12px;color:var(--muted);line-height:1.5">🔍 Not evaluated — '+reason+'.<br>Turn on a judge (Anthropic API, OpenClaw, local MLX 8B+, or Apple Intelligence) in <code>apme.judge</code> to score future tasks. Tasks without a judge still record their trajectory, cost, and outcome — they just carry no quality score.</div>';
429
+ h+='<div class="section" style="padding:10px 12px;background:color-mix(in srgb,var(--muted) 12%,transparent);border:1px solid var(--border);border-radius:6px;font-size:12px;color:var(--muted);line-height:1.5">🔍 Not evaluated — '+reason+'.<br>Turn on a judge (Anthropic API, OpenClaw, local MLX 8B+, or Apple Intelligence) in <code>apme.judge</code> to score future tasks. Tasks without a judge still record their trajectory, cost, and outcome — they just carry no quality score.</div>';
366
430
  }
367
431
  }
368
432
 
@@ -389,7 +453,7 @@ async function selectRun(id){
389
453
  h+='<div class="section"><div class="section-head"><span>Manual Reviews <span style="font-size:10px;color:var(--dim);border:1px solid var(--dim);border-radius:8px;padding:1px 6px;margin-left:6px">hand-run</span></span><span style="font-size:11px;color:var(--dim)">'+mEvals.length+'</span></div>';
390
454
  for(const e of mEvals){
391
455
  const risk=e.score>=1?'low':e.score>=0.5?'medium':'high';
392
- const rc=risk==='high'?'var(--red)':risk==='medium'?'var(--amber,#d19a3a)':'var(--green)';
456
+ const rc=risk==='high'?'var(--red)':risk==='medium'?'var(--amber)':'var(--green)';
393
457
  let summary='',findings=0;
394
458
  if(e.raw){try{const r=JSON.parse(e.raw);summary=r.summary||'';findings=(r.findings||[]).length;}catch{}}
395
459
  h+='<div style="padding:6px 0;border-bottom:1px solid var(--border,rgba(255,255,255,.06))">';
@@ -473,7 +537,7 @@ async function selectRun(id){
473
537
  if(cscore!=null)h+=fs(cscore);else h+='<span style="font-size:11px;color:var(--dim)">…</span>';
474
538
  h+='</div>';
475
539
  const ocGlyph=oc==='success'?'✓':oc==='partial'?'△':oc==='fail'?'✗':oc==='abandoned'?'⊘':'';
476
- const ocColor=oc==='success'?'var(--green)':oc==='fail'?'var(--red)':oc==='partial'?'var(--orange,#f59e0b)':oc==='abandoned'?'var(--dim)':'var(--dim)';
540
+ const ocColor=oc==='success'?'var(--green)':oc==='fail'?'var(--red)':oc==='partial'?'var(--orange)':oc==='abandoned'?'var(--dim)':'var(--dim)';
477
541
  if(cat||oc)h+='<div style="margin-top:4px">'+(cat?'<span class="badge-cat">'+cat+'</span>':'')+(oc?' <span style="font-size:11px;color:'+ocColor+';margin-left:6px">'+ocGlyph+' '+oc+'</span>':'')+'</div>';
478
542
  if(summary)h+='<div style="color:var(--muted);font-size:12px;margin:6px 0 0;padding:6px 8px;background:var(--bg);border-radius:4px;border-left:2px solid var(--accent)">'+esc(summary.slice(0,500))+'</div>';
479
543
  h+='</div>';
@@ -558,7 +622,7 @@ async function loadTasks(offset){
558
622
  if(!tasks.length){body.innerHTML='<tr><td colspan="9" class="empty">No tasks match</td></tr>';document.getElementById('tasks-pager').textContent='';return}
559
623
  let h='';
560
624
  for(const t of tasks){
561
- const label=t.summary||t.firstPrompt||('Task '+t.taskIndex);
625
+ const label=t.summary||t.firstPrompt||('Task '+(t.taskIndex+1));
562
626
  // A unit whose replies were never archived can only be partly judged —
563
627
  // say so on the row instead of letting a blank score read as "not run".
564
628
  const gap=t.turnCount>t.answeredTurns
@@ -592,6 +656,160 @@ function fillTaskFacets(f){
592
656
  }
593
657
  function taskSearchDebounced(){clearTimeout(taskSearchTimer);taskSearchTimer=setTimeout(()=>loadTasks(0),250)}
594
658
 
659
+ /* ── Work board ────────────────────────────────────────────────────────────
660
+ Task-first default view. Rows come from /apme/tasks (server-paged), whose
661
+ ordering is attention-first, then recency. The lifecycle sub-tabs map to
662
+ the endpoint's \`view\` buckets; badge counts ride the same response so a
663
+ tab can never disagree with its rows. */
664
+ let workOffset=0,workTotal=0,workView='',workSearchTimer=null,workCounts=null,workSession='',workSessionMeta=null;const WORK_PAGE=50;
665
+ const WORK_VIEWS=[['','All'],['attention','Attention'],['judged','Judged'],['reported','Reported'],['inprogress','In progress'],['orphaned','Orphaned']];
666
+ const SIG_LABEL={todo_complete:'TODO done',clear:'/clear',session_end:'Session end',manual:'Manual',idle_gap:'Idle gap',orphaned:'Orphaned',open:'open'};
667
+ /* Boundary provenance: explicit (user/agent said so) > inferred (idle gap) >
668
+ default (session end) > reaper (orphaned). The chip color says how much to
669
+ trust the segmentation, not how well the task went. */
670
+ function sigChip(sig){
671
+ const cls=(sig==='clear'||sig==='manual'||sig==='todo_complete')?'chip chip-ok'
672
+ :sig==='idle_gap'?'chip chip-warn'
673
+ :sig==='orphaned'?'chip chip-bad':'chip';
674
+ return '<span class="'+cls+'" title="task boundary source">'+esc(SIG_LABEL[sig]||sig||'—')+'</span>';
675
+ }
676
+ function ago(ts){
677
+ if(!ts)return'—';const s=Math.max(0,Math.round((Date.now()-ts)/1000));
678
+ if(s<60)return s+'s ago';if(s<3600)return Math.floor(s/60)+'m ago';
679
+ if(s<86400)return Math.floor(s/3600)+'h ago';return Math.floor(s/86400)+'d ago';
680
+ }
681
+ function renderWorkTabs(){
682
+ const counts=workCounts||{};
683
+ document.getElementById('work-tabs').innerHTML=WORK_VIEWS.map(([v,label])=>{
684
+ const n=v===''?counts.all:counts[v];
685
+ return '<button class="work-tab'+(workView===v?' active':'')+'" onclick="setWorkView(\\''+v+'\\')">'+label+
686
+ (n!=null?'<span class="n">'+n+'</span>':'')+'</button>';
687
+ }).join('')+'<span style="margin-left:auto;font-size:10px;color:var(--dim)">'+(workSession?'session · recent first':'attention first, then recency')+'</span>';
688
+ }
689
+ function setWorkView(v){workView=v;loadWork(0)}
690
+ function shortSession(id){
691
+ if(!id)return'unknown';
692
+ if(id.includes(':'))return id.length>22?'…'+id.slice(-20):id;
693
+ return id.length>10?id.slice(0,8)+'…':id;
694
+ }
695
+ function renderWorkScope(){
696
+ const el=document.getElementById('work-scope');
697
+ if(!workSession){el.style.display='none';el.innerHTML='';return}
698
+ const m=workSessionMeta||{};
699
+ el.style.display='flex';
700
+ el.innerHTML='<strong style="color:var(--accent)">Session</strong>'+
701
+ (m.project?'<span>'+esc(m.project)+'</span>':'')+
702
+ (m.agent?'<span class="chip">'+esc(m.agent)+'</span>':'')+
703
+ '<code title="'+esc(workSession)+'">'+esc(shortSession(workSession))+'</code>'+
704
+ '<span>'+workTotal+' task'+(workTotal===1?'':'s')+'</span>'+
705
+ '<button class="clear" onclick="clearWorkSession()" title="Return to all sessions">All sessions ×</button>';
706
+ }
707
+ function setWorkSession(id,project,agent){
708
+ if(!id)return;
709
+ workSession=id;workSessionMeta={project:project||'',agent:agent||''};workView='';
710
+ document.getElementById('w-q').value='';
711
+ document.getElementById('w-agent').value='';
712
+ document.getElementById('w-project').value='';
713
+ loadWork(0);
714
+ }
715
+ function clearWorkSession(){workSession='';workSessionMeta=null;workView='';loadWork(0)}
716
+ function openWorkSessionFrom(el,ev){
717
+ if(ev)ev.stopPropagation();
718
+ const id=el.dataset.session||'';if(!id)return;
719
+ const visible=document.getElementById('panel-work').classList.contains('visible');
720
+ if(visible){setWorkSession(id,el.dataset.project||'',el.dataset.agent||'');return}
721
+ // Switching from Run/Task detail: prime the scope first, then let showTab's
722
+ // single load render it. Calling setWorkSession here would fetch twice.
723
+ workSession=id;workSessionMeta={project:el.dataset.project||'',agent:el.dataset.agent||''};workView='';
724
+ document.getElementById('w-q').value='';
725
+ document.getElementById('w-agent').value='';
726
+ document.getElementById('w-project').value='';
727
+ showTabById('work');
728
+ }
729
+ async function loadWork(offset){
730
+ workOffset=offset||0;
731
+ const rowsEl=document.getElementById('work-rows');
732
+ const p=new URLSearchParams();
733
+ p.set('limit',String(WORK_PAGE));p.set('offset',String(workOffset));
734
+ if(!workSession)p.set('sort','attention');
735
+ if(workView)p.set('view',workView);
736
+ if(workSession)p.set('session',workSession);
737
+ const q=document.getElementById('w-q').value.trim();if(q)p.set('q',q);
738
+ const ag=document.getElementById('w-agent').value;if(ag)p.set('agent',ag);
739
+ const pr=document.getElementById('w-project').value;if(pr)p.set('project',pr);
740
+ try{
741
+ const r=await fetch(api('/apme/tasks?'+p.toString()));
742
+ if(!r.ok)throw new Error('HTTP '+r.status);
743
+ const d=await r.json();
744
+ const tasks=d.tasks||[];workTotal=d.total||0;workCounts=d.viewCounts||null;
745
+ if(workSession&&tasks[0])workSessionMeta={project:tasks[0].projectName||'',agent:tasks[0].agentType||''};
746
+ renderWorkTabs();
747
+ renderWorkScope();
748
+ // A page that shrank under us (idle-gap closes, filters) leaves a stale
749
+ // offset past the new total — clamp back instead of rendering an empty
750
+ // page with no pager (which the 15s tick would then refetch forever).
751
+ if(!tasks.length&&workTotal>0&&workOffset>0){
752
+ loadWork(Math.max(0,Math.floor((workTotal-1)/WORK_PAGE)*WORK_PAGE));
753
+ return;
754
+ }
755
+ const fill=(id,vals)=>{const s=document.getElementById(id);const cur=s.value;
756
+ while(s.options.length>1)s.remove(1);
757
+ (vals||[]).forEach(v=>{const o=document.createElement('option');o.value=v;o.textContent=v;s.add(o)});
758
+ s.value=cur;};
759
+ fill('w-agent',(d.facets||{}).agents);fill('w-project',(d.facets||{}).projects);
760
+ if(!tasks.length){rowsEl.innerHTML='<div class="empty">No tasks in this view</div>';document.getElementById('work-pager').textContent='';return}
761
+ let h='';
762
+ for(const t of tasks){
763
+ // Summary-first, like the timeline SSOT: the judge's outcome sentence
764
+ // outranks the intent title; the intent covers the unjudged majority.
765
+ const label=t.summary||t.title||('Task '+(t.taskIndex+1));
766
+ const unnamed=!t.title&&!t.summary;
767
+ const gap=t.turnCount>t.answeredTurns
768
+ ?'<span title="'+(t.turnCount-t.answeredTurns)+' turn(s) archived without a reply" style="color:var(--yellow)">◍</span>':'';
769
+ const open=!t.endedAt;
770
+ const score=t.overallScore!=null?fs(t.overallScore)
771
+ :open?'<span class="badge badge-pending">open</span>'
772
+ :'<span style="font-size:10px;color:var(--dim)">unjudged</span>';
773
+ // Cost basis grammar: bare $ only for a KNOWN figure, ≈$ for estimates.
774
+ const cost=t.costUsd!=null?(t.costKnown?'$':'≈$')+t.costUsd.toFixed(2):'';
775
+ const turns=t.turnCount?t.turnCount+' turn'+(t.turnCount===1?'':'s'):'';
776
+ h+='<div class="work-row'+(t.attention?' attn':'')+(selTaskId===t.id?' selected':'')+'" id="wr-'+t.id+'" onclick="selectTask(\\''+t.id+'\\')">'+
777
+ '<div class="work-title"><span class="t'+(unnamed?' unnamed':'')+'" title="'+esc(t.firstPrompt||label)+'">'+esc(label)+'</span>'+gap+
778
+ (t.outcome?fo(t.outcome):'')+score+'</div>'+
779
+ '<div class="work-sub">'+
780
+ sigChip(open?'open':t.boundarySignal)+
781
+ '<span class="chip">'+esc(t.agentType||'—')+'</span>'+
782
+ (t.sessionId?'<button class="session-link" data-session="'+esc(t.sessionId)+'" data-project="'+esc(t.projectName||'')+'" data-agent="'+esc(t.agentType||'')+'" onclick="openWorkSessionFrom(this,event)" title="Show all work in this session">'+esc(t.projectName||'No project')+' · '+esc(shortSession(t.sessionId))+'</button>':'')+
783
+ (t.taskCategory?'<span class="badge-cat">'+esc(t.taskCategory)+'</span>':'')+
784
+ (t.coordination?'<span class="chip" title="subagent dispatches / agent-to-agent messages" style="color:var(--accent);border-color:color-mix(in srgb,var(--accent) 30%,transparent)">'+
785
+ (t.coordination.dispatches?'⑂×'+t.coordination.dispatches:'')+
786
+ (t.coordination.dispatches&&t.coordination.messages?' ':'')+
787
+ (t.coordination.messages?'✉×'+t.coordination.messages:'')+'</span>':'')+
788
+ (t.actionFold?'<span>'+esc(t.actionFold)+'</span>':'')+
789
+ (turns?'<span>'+turns+'</span>':'')+
790
+ (cost?'<span>'+cost+'</span>':'')+
791
+ '<span style="margin-left:auto">'+ago(t.startedAt)+'</span>'+
792
+ '</div></div>';
793
+ }
794
+ rowsEl.innerHTML=h;
795
+ const from=workTotal?workOffset+1:0,to=Math.min(workOffset+tasks.length,workTotal);
796
+ document.getElementById('work-pager').innerHTML=
797
+ '<button onclick="loadWork('+Math.max(0,workOffset-WORK_PAGE)+')" '+(workOffset<=0?'disabled':'')+' style="background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:3px 10px;cursor:pointer">Prev</button>'+
798
+ '<span>'+from+'–'+to+' of '+workTotal+'</span>'+
799
+ '<button onclick="loadWork('+(workOffset+WORK_PAGE)+')" '+(to>=workTotal?'disabled':'')+' style="background:var(--surface);color:var(--muted);border:1px solid var(--border);border-radius:4px;padding:3px 10px;cursor:pointer">Next</button>';
800
+ if(document.getElementById('panel-work').classList.contains('visible'))
801
+ document.getElementById('status').textContent=workTotal+' tasks · '+new Date().toLocaleTimeString();
802
+ }catch(e){
803
+ // Both daemons serve /apme/tasks now (Swift since 2026-08-29), but an
804
+ // OLDER Swift daemon build may not — say which capability is missing
805
+ // instead of a bare error, so a degraded board is a labeled state.
806
+ rowsEl.innerHTML='<div class="empty">Task list unavailable ('+esc(e.message)+').<br>'+
807
+ 'An older daemon build may not serve /apme/tasks; Activity and Runs tabs still work.</div>';
808
+ document.getElementById('work-pager').textContent='';
809
+ }
810
+ }
811
+ function workSearchDebounced(){clearTimeout(workSearchTimer);workSearchTimer=setTimeout(()=>loadWork(0),250)}
812
+
595
813
  async function selectTask(id){
596
814
  selTaskId=id;
597
815
  const el=document.getElementById('detail-panel');
@@ -599,18 +817,30 @@ async function selectTask(id){
599
817
  try{
600
818
  const r=await fetch(api('/apme/tasks/'+id));const d=await r.json();
601
819
  const t=d.task||{},run=d.run||{},turns=d.turns||[],evals=d.evals||[],sample=d.sample;
820
+ // Normalize both casings like selectRun does — an older Swift daemon
821
+ // build serves this detail with snake_case keys.
822
+ const taskIndex=t.taskIndex??t.task_index;
823
+ const startedAt=t.startedAt??t.started_at;
824
+ const endedAt=t.endedAt??t.ended_at;
825
+ const boundarySignal=t.boundarySignal??t.boundary_signal;
826
+ const costUsd=t.costUsd??t.cost_usd;
827
+ const taskSessionId=run.sessionId??run.session_id??'';
828
+ const taskAgent=run.agentType||run.agent_type||'';
829
+ const taskProject=run.projectName||run.project_name||'';
602
830
  let h='';
603
- h+='<div class="detail-header"><h2>'+esc(t.summary||('Task '+t.taskIndex))+'</h2>';
604
- h+='<div class="meta-row"><span>'+esc(run.agentType||'')+'</span><span>'+esc(run.modelId||'—')+'</span>'+
605
- '<span>'+esc(run.projectName||'—')+'</span>'+
606
- '<span>'+(t.startedAt?new Date(t.startedAt).toLocaleString():'—')+'</span>'+
607
- '<span>'+(t.endedAt?fd(t.endedAt-t.startedAt):'<span style="color:var(--yellow)">open</span>')+'</span>'+
608
- '<span>boundary '+esc(t.boundarySignal||'—')+'</span></div></div>';
609
- h+='<div class="section"><div class="metric-grid">'
610
- '<div class="metric-card"><div class="lbl">Score</div><div class="val">'+fs(d.overallScore??t.compositeScore)+'</div></div>'+
831
+ h+='<div class="detail-header"><h2>'+esc(t.summary||('Task '+((taskIndex??0)+1)))+'</h2>';
832
+ h+='<div class="meta-row"><span>'+esc(run.agentType||run.agent_type||'')+'</span><span>'+esc(run.modelId||run.model_id||'—')+'</span>'+
833
+ '<span>'+esc(run.projectName||run.project_name||'—')+'</span>'+
834
+ '<span>'+(startedAt?new Date(startedAt).toLocaleString():'—')+'</span>'+
835
+ '<span>'+(endedAt?fd(endedAt-startedAt):'<span style="color:var(--yellow)">open</span>')+'</span>'+
836
+ '<span>boundary '+esc(boundarySignal||'—')+'</span>'+
837
+ (taskSessionId?'<button class="session-link" data-session="'+esc(taskSessionId)+'" data-project="'+esc(taskProject)+'" data-agent="'+esc(taskAgent)+'" onclick="openWorkSessionFrom(this,event)" title="Open every task collected from this native session">View session work</button>':'')+
838
+ '</div></div>';
839
+ h+='<div class="section"><div class="metric-grid">'+
840
+ '<div class="metric-card"><div class="lbl">Score</div><div class="val">'+fs(d.overallScore??t.compositeScore??t.composite_score)+'</div></div>'+
611
841
  '<div class="metric-card"><div class="lbl">Turns</div><div class="val">'+turns.length+'</div></div>'+
612
842
  '<div class="metric-card"><div class="lbl">Events</div><div class="val">'+((sample&&sample.events?sample.events.length:0))+'</div></div>'+
613
- '<div class="metric-card"><div class="lbl">Cost</div><div class="val">'+(t.costUsd!=null?'$'+t.costUsd.toFixed(4):'—')+'</div></div>'+
843
+ '<div class="metric-card"><div class="lbl">Cost</div><div class="val">'+(costUsd!=null?'$'+costUsd.toFixed(4):'—')+'</div></div>'+
614
844
  '</div></div>';
615
845
  if(evals.length){
616
846
  h+='<div class="section"><div class="section-head"><span>Evals</span></div><table><tbody>';
@@ -634,7 +864,12 @@ async function selectTask(id){
634
864
  h+='</div>';
635
865
  el.innerHTML=h;
636
866
  }catch(e){el.innerHTML='<div class="detail-empty">Error: '+esc(e.message)+'</div>'}
637
- loadTasks(taskOffset);
867
+ // Move the selection highlight in place — a full list refetch just to move
868
+ // a CSS class was the dashboard's most expensive click.
869
+ if(document.getElementById('panel-work').classList.contains('visible')){
870
+ document.querySelectorAll('.work-row.selected').forEach(r=>r.classList.remove('selected'));
871
+ const row=document.getElementById('wr-'+id);if(row)row.classList.add('selected');
872
+ } else loadTasks(taskOffset);
638
873
  }
639
874
 
640
875
  async function loadScorecard(){
@@ -681,7 +916,7 @@ async function loadRecommend(){
681
916
  h+='<table style="margin-top:4px"><thead><tr><th>Agent</th><th>Model</th><th>Quality</th><th>$/Quality</th><th>Cost</th><th>Samples</th></tr></thead><tbody>';
682
917
  for(const s of list){
683
918
  const isBest=s===best;
684
- h+='<tr'+(isBest?' style="background:rgba(34,197,94,0.08)"':'')+'><td>'+(isBest?'★ ':'')+esc(s.agentType||'—')+'</td><td>'+esc((s.modelId||'—').slice(0,22))+'</td><td>'+fs(s.avgQuality)+'</td><td>'+(s.costPerQuality!=null?'$'+s.costPerQuality.toFixed(3):'—')+'</td><td>'+(s.totalCost!=null?'$'+s.totalCost.toFixed(2):'—')+'</td><td>'+s.samples+'</td></tr>';
919
+ h+='<tr'+(isBest?' style="background:color-mix(in srgb,var(--green) 8%,transparent)"':'')+'><td>'+(isBest?'★ ':'')+esc(s.agentType||'—')+'</td><td>'+esc((s.modelId||'—').slice(0,22))+'</td><td>'+fs(s.avgQuality)+'</td><td>'+(s.costPerQuality!=null?'$'+s.costPerQuality.toFixed(3):'—')+'</td><td>'+(s.totalCost!=null?'$'+s.totalCost.toFixed(2):'—')+'</td><td>'+s.samples+'</td></tr>';
685
920
  }
686
921
  h+='</tbody></table></div>';
687
922
  }
@@ -697,7 +932,11 @@ async function loadRecommend(){
697
932
 
698
933
  Colour is by node kind; radius by degree, so the derived hubs (a file many
699
934
  tasks touched, a tool used everywhere) are the ones that read as structural. */
700
- const G_COLORS={run:'#6166E0',task:'#22c55e',turn:'#64748b',session:'#eab308',project:'#06b6d4',model:'#a855f7',agent:'#C07058',tool:'#f97316',file:'#94a3b8'};
935
+ // Canvas 2D can't resolve var() at draw time, so node colors are read off
936
+ // :root once — every value is a design token, keeping the file hex-free
937
+ // outside the :root mirror (design/verify-tokens-sync.py).
938
+ const cssTok=n=>getComputedStyle(document.documentElement).getPropertyValue(n).trim();
939
+ const G_COLORS={run:cssTok('--ui-cyan'),task:cssTok('--ui-ok'),turn:cssTok('--ui-idle-dark'),session:cssTok('--amber-500'),project:cssTok('--kelp-300'),model:cssTok('--kelp-500'),agent:cssTok('--coral-500'),tool:cssTok('--ui-attn'),file:cssTok('--ink-300')};
701
940
  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)'};
702
941
 
703
942
  async function loadGraph(){
@@ -810,12 +1049,12 @@ function startGraphSim(nodes,edges){
810
1049
  ctx.beginPath();ctx.moveTo(l.s.x,l.s.y);ctx.lineTo(l.t.x,l.t.y);ctx.stroke();
811
1050
  }
812
1051
  for(const n of nodes){
813
- ctx.fillStyle=G_COLORS[n.kind]||'#94a3b8';
1052
+ ctx.fillStyle=G_COLORS[n.kind]||G_COLORS.file;
814
1053
  ctx.globalAlpha=hover&&hover!==n?0.55:1;
815
1054
  ctx.beginPath();ctx.arc(n.x,n.y,n.r,0,6.2832);ctx.fill();
816
1055
  // Label only what carries the structure, else the canvas is unreadable.
817
1056
  if(n.r>7||n===hover){
818
- ctx.globalAlpha=1;ctx.fillStyle='#cbd5e1';ctx.font='10px ui-monospace,monospace';
1057
+ ctx.globalAlpha=1;ctx.fillStyle=cssTok('--ui-tty-text');ctx.font='10px ui-monospace,monospace';
819
1058
  ctx.fillText(String(n.label||'').slice(0,26),n.x+n.r+3,n.y+3);
820
1059
  }
821
1060
  ctx.globalAlpha=1;
@@ -832,12 +1071,13 @@ function showTabById(n){
832
1071
  if(btn)btn.click();
833
1072
  }
834
1073
 
835
- loadActivity();loadRuns();loadRecommend();loadScorecard();loadCategories();
1074
+ loadWork(0);loadActivity();loadRuns();loadRecommend();loadScorecard();loadCategories();
836
1075
  setInterval(loadRuns,15000);setInterval(loadRecommend,30000);setInterval(loadScorecard,30000);setInterval(loadCategories,30000);
837
- // Tasks refresh only while its tab is up — the query is server-paged and there
838
- // is no reason to run it against the store every 15s in the background.
1076
+ // Tasks/Work refresh only while their tab is up — the query is server-paged and
1077
+ // there is no reason to run it against the store every 15s in the background.
839
1078
  setInterval(()=>{if(document.getElementById('panel-tasks').classList.contains('visible'))loadTasks(taskOffset)},15000);
840
1079
  setInterval(()=>{if(document.getElementById('panel-activity').classList.contains('visible'))loadActivity()},15000);
1080
+ setInterval(()=>{if(document.getElementById('panel-work').classList.contains('visible'))loadWork(workOffset)},15000);
841
1081
  </script>
842
1082
  </body>
843
1083
  </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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAk0BD,CAAC;AACT,CAAC"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAkjCD,CAAC;AACT,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/apme/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM7C,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,UAAU,GAAG,IAAI,GACtB,OAAO,CAAC,OAAO,CAAC,CAwQlB;AAyCD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,iBAAiB,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAMzG"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/apme/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAS7C,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,UAAU,GAAG,IAAI,GACtB,OAAO,CAAC,OAAO,CAAC,CAmSlB;AAyCD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,iBAAiB,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAMzG"}
package/dist/apme/http.js CHANGED
@@ -15,8 +15,9 @@
15
15
  */
16
16
  import { loadApmeConfig } from './settings.js';
17
17
  import { apmeDashboardHtml } from './dashboard-html.js';
18
- import { EVAL_SCHEMA_VERSION } from '@agentdeck/shared';
18
+ import { agentCoordinationSummary, deriveTaskTitle, foldActionCounts, EVAL_SCHEMA_VERSION } from '@agentdeck/shared';
19
19
  import { activitySnapshotForStore } from './activity-history.js';
20
+ const TASK_VIEWS = ['attention', 'inprogress', 'judged', 'reported', 'orphaned'];
20
21
  export async function handleApmeRequest(req, res, apme) {
21
22
  const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
22
23
  if (!url.pathname.startsWith('/apme'))
@@ -113,19 +114,46 @@ export async function handleApmeRequest(req, res, apme) {
113
114
  const limit = clampInt(url.searchParams.get('limit'), 1, 500, 50);
114
115
  const offset = clampInt(url.searchParams.get('offset'), 0, 1_000_000, 0);
115
116
  const stateParam = url.searchParams.get('state');
116
- const { total, tasks } = apme.store.listTaskPage({
117
- limit, offset,
117
+ const viewParam = url.searchParams.get('view');
118
+ const view = TASK_VIEWS.find((v) => v === viewParam);
119
+ const filters = {
118
120
  ...(url.searchParams.get('agent') ? { agentType: url.searchParams.get('agent') } : {}),
121
+ ...(url.searchParams.get('session') ? { sessionId: url.searchParams.get('session') } : {}),
119
122
  ...(url.searchParams.get('project') ? { projectName: url.searchParams.get('project') } : {}),
120
123
  ...(url.searchParams.get('category') ? { category: url.searchParams.get('category') } : {}),
121
124
  ...(url.searchParams.get('outcome') ? { outcome: url.searchParams.get('outcome') } : {}),
122
- ...(stateParam === 'closed' || stateParam === 'open' ? { state: stateParam } : {}),
123
125
  ...(url.searchParams.get('q') ? { q: url.searchParams.get('q') } : {}),
126
+ };
127
+ const { total, tasks } = apme.store.listTaskPage({
128
+ limit, offset,
129
+ ...filters,
130
+ ...(stateParam === 'closed' || stateParam === 'open' ? { state: stateParam } : {}),
131
+ ...(view ? { view } : {}),
132
+ // Attention-first is the Work board's ordering and ONLY the Work
133
+ // board's — every other listTaskPage consumer keeps pure recency.
134
+ ...(url.searchParams.get('sort') === 'attention' ? { order: 'attention' } : {}),
135
+ });
136
+ // Work-board display projections, enriched here so the store keeps
137
+ // returning raw facts: intent title (deriveTaskTitle SSOT — the same
138
+ // rules that name the timeline's task_start header, over the task's OWN
139
+ // first prompt, never the run fallback) and the folded action line
140
+ // (foldActionCounts SSOT) over one grouped tool query.
141
+ const toolCounts = apme.store.toolCountsForTasks(tasks.map((t) => t.id));
142
+ const enriched = tasks.map((t) => {
143
+ const tools = toolCounts.get(t.id) ?? [];
144
+ return {
145
+ ...t,
146
+ title: deriveTaskTitle(t.ownFirstPrompt),
147
+ actionFold: foldActionCounts({ tools, filesTouched: t.filesTouched }),
148
+ coordination: agentCoordinationSummary(tools),
149
+ };
124
150
  });
125
151
  sendJson(res, 200, {
126
152
  schema: EVAL_SCHEMA_VERSION,
127
- total, limit, offset, tasks,
153
+ total, limit, offset, tasks: enriched,
128
154
  facets: apme.store.taskFacets(),
155
+ // Badges honor the same narrowing filters as the rows.
156
+ viewCounts: apme.store.taskViewCounts(filters),
129
157
  });
130
158
  return true;
131
159
  }