@agentdeck/bridge 1.1.0 → 1.2.1

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 (199) 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/classifier.d.ts.map +1 -1
  15. package/dist/apme/classifier.js +18 -10
  16. package/dist/apme/classifier.js.map +1 -1
  17. package/dist/apme/claude-transcript-reader.d.ts +36 -0
  18. package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
  19. package/dist/apme/claude-transcript-reader.js +78 -2
  20. package/dist/apme/claude-transcript-reader.js.map +1 -1
  21. package/dist/apme/collector.d.ts +126 -3
  22. package/dist/apme/collector.d.ts.map +1 -1
  23. package/dist/apme/collector.js +450 -41
  24. package/dist/apme/collector.js.map +1 -1
  25. package/dist/apme/dashboard-html.d.ts.map +1 -1
  26. package/dist/apme/dashboard-html.js +278 -36
  27. package/dist/apme/dashboard-html.js.map +1 -1
  28. package/dist/apme/graph.d.ts +1 -1
  29. package/dist/apme/graph.d.ts.map +1 -1
  30. package/dist/apme/graph.js +51 -1
  31. package/dist/apme/graph.js.map +1 -1
  32. package/dist/apme/http.d.ts.map +1 -1
  33. package/dist/apme/http.js +37 -5
  34. package/dist/apme/http.js.map +1 -1
  35. package/dist/apme/index.d.ts +4 -0
  36. package/dist/apme/index.d.ts.map +1 -1
  37. package/dist/apme/index.js +19 -3
  38. package/dist/apme/index.js.map +1 -1
  39. package/dist/apme/runner.d.ts +90 -0
  40. package/dist/apme/runner.d.ts.map +1 -1
  41. package/dist/apme/runner.js +524 -48
  42. package/dist/apme/runner.js.map +1 -1
  43. package/dist/apme/sample-to-timeline.d.ts.map +1 -1
  44. package/dist/apme/sample-to-timeline.js +5 -0
  45. package/dist/apme/sample-to-timeline.js.map +1 -1
  46. package/dist/apme/settings.d.ts +8 -0
  47. package/dist/apme/settings.d.ts.map +1 -1
  48. package/dist/apme/settings.js +4 -0
  49. package/dist/apme/settings.js.map +1 -1
  50. package/dist/apme/store.d.ts +118 -9
  51. package/dist/apme/store.d.ts.map +1 -1
  52. package/dist/apme/store.js +342 -44
  53. package/dist/apme/store.js.map +1 -1
  54. package/dist/apme/task-gradeability.d.ts +82 -0
  55. package/dist/apme/task-gradeability.d.ts.map +1 -0
  56. package/dist/apme/task-gradeability.js +126 -0
  57. package/dist/apme/task-gradeability.js.map +1 -0
  58. package/dist/awaiting-overlay.d.ts +13 -26
  59. package/dist/awaiting-overlay.d.ts.map +1 -1
  60. package/dist/awaiting-overlay.js +7 -40
  61. package/dist/awaiting-overlay.js.map +1 -1
  62. package/dist/bridge-core.d.ts +15 -0
  63. package/dist/bridge-core.d.ts.map +1 -1
  64. package/dist/bridge-core.js +30 -4
  65. package/dist/bridge-core.js.map +1 -1
  66. package/dist/claude-permission-rules.d.ts +20 -32
  67. package/dist/claude-permission-rules.d.ts.map +1 -1
  68. package/dist/claude-permission-rules.js +19 -102
  69. package/dist/claude-permission-rules.js.map +1 -1
  70. package/dist/claude-usage-recovery.d.ts +54 -0
  71. package/dist/claude-usage-recovery.d.ts.map +1 -0
  72. package/dist/claude-usage-recovery.js +200 -0
  73. package/dist/claude-usage-recovery.js.map +1 -0
  74. package/dist/cli.d.ts +11 -0
  75. package/dist/cli.d.ts.map +1 -1
  76. package/dist/cli.js +81 -14
  77. package/dist/cli.js.map +1 -1
  78. package/dist/codex-rate-limits-live.d.ts +180 -10
  79. package/dist/codex-rate-limits-live.d.ts.map +1 -1
  80. package/dist/codex-rate-limits-live.js +475 -22
  81. package/dist/codex-rate-limits-live.js.map +1 -1
  82. package/dist/codex-rollout-response.d.ts +43 -0
  83. package/dist/codex-rollout-response.d.ts.map +1 -1
  84. package/dist/codex-rollout-response.js +67 -0
  85. package/dist/codex-rollout-response.js.map +1 -1
  86. package/dist/coordination-evidence.d.ts +150 -0
  87. package/dist/coordination-evidence.d.ts.map +1 -0
  88. package/dist/coordination-evidence.js +455 -0
  89. package/dist/coordination-evidence.js.map +1 -0
  90. package/dist/daemon-server.d.ts.map +1 -1
  91. package/dist/daemon-server.js +592 -35
  92. package/dist/daemon-server.js.map +1 -1
  93. package/dist/daemon-ws-client.d.ts +1 -0
  94. package/dist/daemon-ws-client.d.ts.map +1 -1
  95. package/dist/daemon-ws-client.js +25 -16
  96. package/dist/daemon-ws-client.js.map +1 -1
  97. package/dist/daemon.js +1 -1
  98. package/dist/deferred-reply-read.d.ts +24 -0
  99. package/dist/deferred-reply-read.d.ts.map +1 -0
  100. package/dist/deferred-reply-read.js +42 -0
  101. package/dist/deferred-reply-read.js.map +1 -0
  102. package/dist/esp32-serial.d.ts +15 -0
  103. package/dist/esp32-serial.d.ts.map +1 -1
  104. package/dist/esp32-serial.js +61 -2
  105. package/dist/esp32-serial.js.map +1 -1
  106. package/dist/font-pack.d.ts +15 -0
  107. package/dist/font-pack.d.ts.map +1 -0
  108. package/dist/font-pack.js +75 -0
  109. package/dist/font-pack.js.map +1 -0
  110. package/dist/glance-frame.d.ts.map +1 -1
  111. package/dist/glance-frame.js +2 -0
  112. package/dist/glance-frame.js.map +1 -1
  113. package/dist/hook-codex-sessions.d.ts +9 -0
  114. package/dist/hook-codex-sessions.d.ts.map +1 -1
  115. package/dist/hook-codex-sessions.js +35 -2
  116. package/dist/hook-codex-sessions.js.map +1 -1
  117. package/dist/hook-opencode-sessions.d.ts +62 -0
  118. package/dist/hook-opencode-sessions.d.ts.map +1 -0
  119. package/dist/hook-opencode-sessions.js +180 -0
  120. package/dist/hook-opencode-sessions.js.map +1 -0
  121. package/dist/hook-response-source.d.ts +40 -0
  122. package/dist/hook-response-source.d.ts.map +1 -0
  123. package/dist/hook-response-source.js +31 -0
  124. package/dist/hook-response-source.js.map +1 -0
  125. package/dist/http-auth-gate.d.ts.map +1 -1
  126. package/dist/http-auth-gate.js +9 -1
  127. package/dist/http-auth-gate.js.map +1 -1
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +6 -3
  130. package/dist/index.js.map +1 -1
  131. package/dist/learning-pack.d.ts +24 -0
  132. package/dist/learning-pack.d.ts.map +1 -0
  133. package/dist/learning-pack.js +125 -0
  134. package/dist/learning-pack.js.map +1 -0
  135. package/dist/mdns.js +1 -1
  136. package/dist/mdns.js.map +1 -1
  137. package/dist/native-dependency-diagnostics.d.ts +29 -0
  138. package/dist/native-dependency-diagnostics.d.ts.map +1 -0
  139. package/dist/native-dependency-diagnostics.js +95 -0
  140. package/dist/native-dependency-diagnostics.js.map +1 -0
  141. package/dist/observed-steering.d.ts +4 -4
  142. package/dist/observed-steering.d.ts.map +1 -1
  143. package/dist/observed-steering.js +28 -32
  144. package/dist/observed-steering.js.map +1 -1
  145. package/dist/openclaw-gateway-stability.d.ts +94 -0
  146. package/dist/openclaw-gateway-stability.d.ts.map +1 -0
  147. package/dist/openclaw-gateway-stability.js +176 -0
  148. package/dist/openclaw-gateway-stability.js.map +1 -0
  149. package/dist/pairing-knocks.d.ts +80 -0
  150. package/dist/pairing-knocks.d.ts.map +1 -0
  151. package/dist/pairing-knocks.js +312 -0
  152. package/dist/pairing-knocks.js.map +1 -0
  153. package/dist/passive-observer.d.ts +5 -0
  154. package/dist/passive-observer.d.ts.map +1 -1
  155. package/dist/passive-observer.js +6 -0
  156. package/dist/passive-observer.js.map +1 -1
  157. package/dist/pixoo/micro-glyphs.d.ts +4 -0
  158. package/dist/pixoo/micro-glyphs.d.ts.map +1 -1
  159. package/dist/pixoo/micro-glyphs.js +29 -0
  160. package/dist/pixoo/micro-glyphs.js.map +1 -1
  161. package/dist/pixoo/pixoo-font.d.ts.map +1 -1
  162. package/dist/pixoo/pixoo-font.js +3 -1
  163. package/dist/pixoo/pixoo-font.js.map +1 -1
  164. package/dist/pixoo/pixoo-renderer.d.ts +1 -1
  165. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  166. package/dist/pixoo/pixoo-renderer.js +17 -2
  167. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  168. package/dist/pty-manager.d.ts +9 -0
  169. package/dist/pty-manager.d.ts.map +1 -1
  170. package/dist/pty-manager.js +63 -4
  171. package/dist/pty-manager.js.map +1 -1
  172. package/dist/relayed-usage.d.ts +30 -0
  173. package/dist/relayed-usage.d.ts.map +1 -1
  174. package/dist/relayed-usage.js +25 -2
  175. package/dist/relayed-usage.js.map +1 -1
  176. package/dist/review-runner.js +1 -1
  177. package/dist/subagent-timeline.d.ts +27 -0
  178. package/dist/subagent-timeline.d.ts.map +1 -1
  179. package/dist/subagent-timeline.js +52 -5
  180. package/dist/subagent-timeline.js.map +1 -1
  181. package/dist/surface-protocol.d.ts.map +1 -1
  182. package/dist/surface-protocol.js +2 -0
  183. package/dist/surface-protocol.js.map +1 -1
  184. package/dist/tui/dashboard.d.ts.map +1 -1
  185. package/dist/tui/dashboard.js +8 -1
  186. package/dist/tui/dashboard.js.map +1 -1
  187. package/dist/usage-api.d.ts +3 -0
  188. package/dist/usage-api.d.ts.map +1 -1
  189. package/dist/usage-api.js +101 -36
  190. package/dist/usage-api.js.map +1 -1
  191. package/dist/usage-event.d.ts.map +1 -1
  192. package/dist/usage-event.js +2 -1
  193. package/dist/usage-event.js.map +1 -1
  194. package/dist/ws-server.d.ts +18 -0
  195. package/dist/ws-server.d.ts.map +1 -1
  196. package/dist/ws-server.js +40 -2
  197. package/dist/ws-server.js.map +1 -1
  198. package/package.json +6 -5
  199. 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,162 @@ 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 NG={no_reply:'no reply captured',aborted_only:'ended by the client',trivial:'trivial exchange'};
771
+ const score=t.overallScore!=null?fs(t.overallScore)
772
+ :open?'<span class="badge badge-pending">open</span>'
773
+ :t.notGradeable?'<span class="chip" title="the judge declined this task: '+esc(NG[t.notGradeable]||t.notGradeable)+'" style="color:var(--dim)">not graded · '+esc(NG[t.notGradeable]||t.notGradeable)+'</span>'
774
+ :'<span style="font-size:10px;color:var(--dim)">unjudged</span>';
775
+ // Cost basis grammar: bare $ only for a KNOWN figure, ≈$ for estimates.
776
+ const cost=t.costUsd!=null?(t.costKnown?'$':'≈$')+t.costUsd.toFixed(2):'';
777
+ const turns=t.turnCount?t.turnCount+' turn'+(t.turnCount===1?'':'s'):'';
778
+ h+='<div class="work-row'+(t.attention?' attn':'')+(selTaskId===t.id?' selected':'')+'" id="wr-'+t.id+'" onclick="selectTask(\\''+t.id+'\\')">'+
779
+ '<div class="work-title"><span class="t'+(unnamed?' unnamed':'')+'" title="'+esc(t.firstPrompt||label)+'">'+esc(label)+'</span>'+gap+
780
+ (t.outcome?fo(t.outcome):'')+score+'</div>'+
781
+ '<div class="work-sub">'+
782
+ sigChip(open?'open':t.boundarySignal)+
783
+ '<span class="chip">'+esc(t.agentType||'—')+'</span>'+
784
+ (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>':'')+
785
+ (t.taskCategory?'<span class="badge-cat">'+esc(t.taskCategory)+'</span>':'')+
786
+ (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)">'+
787
+ (t.coordination.dispatches?'⑂×'+t.coordination.dispatches:'')+
788
+ (t.coordination.dispatches&&t.coordination.messages?' ':'')+
789
+ (t.coordination.messages?'✉×'+t.coordination.messages:'')+'</span>':'')+
790
+ (t.actionFold?'<span>'+esc(t.actionFold)+'</span>':'')+
791
+ (turns?'<span>'+turns+'</span>':'')+
792
+ (cost?'<span>'+cost+'</span>':'')+
793
+ '<span style="margin-left:auto">'+ago(t.startedAt)+'</span>'+
794
+ '</div></div>';
795
+ }
796
+ rowsEl.innerHTML=h;
797
+ const from=workTotal?workOffset+1:0,to=Math.min(workOffset+tasks.length,workTotal);
798
+ document.getElementById('work-pager').innerHTML=
799
+ '<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>'+
800
+ '<span>'+from+'–'+to+' of '+workTotal+'</span>'+
801
+ '<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>';
802
+ if(document.getElementById('panel-work').classList.contains('visible'))
803
+ document.getElementById('status').textContent=workTotal+' tasks · '+new Date().toLocaleTimeString();
804
+ }catch(e){
805
+ // Both daemons serve /apme/tasks now (Swift since 2026-08-29), but an
806
+ // OLDER Swift daemon build may not — say which capability is missing
807
+ // instead of a bare error, so a degraded board is a labeled state.
808
+ rowsEl.innerHTML='<div class="empty">Task list unavailable ('+esc(e.message)+').<br>'+
809
+ 'An older daemon build may not serve /apme/tasks; Activity and Runs tabs still work.</div>';
810
+ document.getElementById('work-pager').textContent='';
811
+ }
812
+ }
813
+ function workSearchDebounced(){clearTimeout(workSearchTimer);workSearchTimer=setTimeout(()=>loadWork(0),250)}
814
+
595
815
  async function selectTask(id){
596
816
  selTaskId=id;
597
817
  const el=document.getElementById('detail-panel');
@@ -599,18 +819,30 @@ async function selectTask(id){
599
819
  try{
600
820
  const r=await fetch(api('/apme/tasks/'+id));const d=await r.json();
601
821
  const t=d.task||{},run=d.run||{},turns=d.turns||[],evals=d.evals||[],sample=d.sample;
822
+ // Normalize both casings like selectRun does — an older Swift daemon
823
+ // build serves this detail with snake_case keys.
824
+ const taskIndex=t.taskIndex??t.task_index;
825
+ const startedAt=t.startedAt??t.started_at;
826
+ const endedAt=t.endedAt??t.ended_at;
827
+ const boundarySignal=t.boundarySignal??t.boundary_signal;
828
+ const costUsd=t.costUsd??t.cost_usd;
829
+ const taskSessionId=run.sessionId??run.session_id??'';
830
+ const taskAgent=run.agentType||run.agent_type||'';
831
+ const taskProject=run.projectName||run.project_name||'';
602
832
  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>'+
833
+ h+='<div class="detail-header"><h2>'+esc(t.summary||('Task '+((taskIndex??0)+1)))+'</h2>';
834
+ h+='<div class="meta-row"><span>'+esc(run.agentType||run.agent_type||'')+'</span><span>'+esc(run.modelId||run.model_id||'—')+'</span>'+
835
+ '<span>'+esc(run.projectName||run.project_name||'—')+'</span>'+
836
+ '<span>'+(startedAt?new Date(startedAt).toLocaleString():'—')+'</span>'+
837
+ '<span>'+(endedAt?fd(endedAt-startedAt):'<span style="color:var(--yellow)">open</span>')+'</span>'+
838
+ '<span>boundary '+esc(boundarySignal||'—')+'</span>'+
839
+ (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>':'')+
840
+ '</div></div>';
841
+ h+='<div class="section"><div class="metric-grid">'+
842
+ '<div class="metric-card"><div class="lbl">Score</div><div class="val">'+fs(d.overallScore??t.compositeScore??t.composite_score)+'</div></div>'+
611
843
  '<div class="metric-card"><div class="lbl">Turns</div><div class="val">'+turns.length+'</div></div>'+
612
844
  '<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>'+
845
+ '<div class="metric-card"><div class="lbl">Cost</div><div class="val">'+(costUsd!=null?'$'+costUsd.toFixed(4):'—')+'</div></div>'+
614
846
  '</div></div>';
615
847
  if(evals.length){
616
848
  h+='<div class="section"><div class="section-head"><span>Evals</span></div><table><tbody>';
@@ -634,7 +866,12 @@ async function selectTask(id){
634
866
  h+='</div>';
635
867
  el.innerHTML=h;
636
868
  }catch(e){el.innerHTML='<div class="detail-empty">Error: '+esc(e.message)+'</div>'}
637
- loadTasks(taskOffset);
869
+ // Move the selection highlight in place — a full list refetch just to move
870
+ // a CSS class was the dashboard's most expensive click.
871
+ if(document.getElementById('panel-work').classList.contains('visible')){
872
+ document.querySelectorAll('.work-row.selected').forEach(r=>r.classList.remove('selected'));
873
+ const row=document.getElementById('wr-'+id);if(row)row.classList.add('selected');
874
+ } else loadTasks(taskOffset);
638
875
  }
639
876
 
640
877
  async function loadScorecard(){
@@ -681,7 +918,7 @@ async function loadRecommend(){
681
918
  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
919
  for(const s of list){
683
920
  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>';
921
+ 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
922
  }
686
923
  h+='</tbody></table></div>';
687
924
  }
@@ -697,8 +934,12 @@ async function loadRecommend(){
697
934
 
698
935
  Colour is by node kind; radius by degree, so the derived hubs (a file many
699
936
  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'};
701
- 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)'};
937
+ // Canvas 2D can't resolve var() at draw time, so node colors are read off
938
+ // :root once — every value is a design token, keeping the file hex-free
939
+ // outside the :root mirror (design/verify-tokens-sync.py).
940
+ const cssTok=n=>getComputedStyle(document.documentElement).getPropertyValue(n).trim();
941
+ 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'),subagent:cssTok('--ui-cyan'),tool:cssTok('--ui-attn'),file:cssTok('--ink-300')};
942
+ const G_EDGE={contains:'rgba(148,163,184,0.35)',continues:'rgba(97,102,224,0.75)',produced:'rgba(234,179,8,0.30)',delegated:'rgba(34,211,238,0.55)',used:'rgba(249,115,22,0.30)',touched:'rgba(148,163,184,0.25)'};
702
943
 
703
944
  async function loadGraph(){
704
945
  const stats=document.getElementById('g-stats');
@@ -810,12 +1051,12 @@ function startGraphSim(nodes,edges){
810
1051
  ctx.beginPath();ctx.moveTo(l.s.x,l.s.y);ctx.lineTo(l.t.x,l.t.y);ctx.stroke();
811
1052
  }
812
1053
  for(const n of nodes){
813
- ctx.fillStyle=G_COLORS[n.kind]||'#94a3b8';
1054
+ ctx.fillStyle=G_COLORS[n.kind]||G_COLORS.file;
814
1055
  ctx.globalAlpha=hover&&hover!==n?0.55:1;
815
1056
  ctx.beginPath();ctx.arc(n.x,n.y,n.r,0,6.2832);ctx.fill();
816
1057
  // Label only what carries the structure, else the canvas is unreadable.
817
1058
  if(n.r>7||n===hover){
818
- ctx.globalAlpha=1;ctx.fillStyle='#cbd5e1';ctx.font='10px ui-monospace,monospace';
1059
+ ctx.globalAlpha=1;ctx.fillStyle=cssTok('--ui-tty-text');ctx.font='10px ui-monospace,monospace';
819
1060
  ctx.fillText(String(n.label||'').slice(0,26),n.x+n.r+3,n.y+3);
820
1061
  }
821
1062
  ctx.globalAlpha=1;
@@ -832,12 +1073,13 @@ function showTabById(n){
832
1073
  if(btn)btn.click();
833
1074
  }
834
1075
 
835
- loadActivity();loadRuns();loadRecommend();loadScorecard();loadCategories();
1076
+ loadWork(0);loadActivity();loadRuns();loadRecommend();loadScorecard();loadCategories();
836
1077
  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.
1078
+ // Tasks/Work refresh only while their tab is up — the query is server-paged and
1079
+ // there is no reason to run it against the store every 15s in the background.
839
1080
  setInterval(()=>{if(document.getElementById('panel-tasks').classList.contains('visible'))loadTasks(taskOffset)},15000);
840
1081
  setInterval(()=>{if(document.getElementById('panel-activity').classList.contains('visible'))loadActivity()},15000);
1082
+ setInterval(()=>{if(document.getElementById('panel-work').classList.contains('visible'))loadWork(workOffset)},15000);
841
1083
  </script>
842
1084
  </body>
843
1085
  </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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAojCD,CAAC;AACT,CAAC"}
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Derived on demand, never persisted: the containment spine comes straight from
6
6
  * foreign keys, while session / project / model / agent / tool / file hubs are
7
- * materialized from denormalized columns and tool payloads. Those hubs are the
7
+ * materialized from denormalized columns and trajectory payloads. Those hubs are the
8
8
  * reason this exists — they are the only edges that connect work units sharing
9
9
  * no ancestor.
10
10
  */
@@ -1 +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"}
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;AA+ED,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,GAAE,gBAAqB,GAAG,cAAc,CAwI5F"}
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Derived on demand, never persisted: the containment spine comes straight from
6
6
  * foreign keys, while session / project / model / agent / tool / file hubs are
7
- * materialized from denormalized columns and tool payloads. Those hubs are the
7
+ * materialized from denormalized columns and trajectory payloads. Those hubs are the
8
8
  * reason this exists — they are the only edges that connect work units sharing
9
9
  * no ancestor.
10
10
  */
@@ -68,6 +68,8 @@ class GraphBuilder {
68
68
  existing.ts = n.ts;
69
69
  if (existing.score == null && n.score != null)
70
70
  existing.score = n.score;
71
+ if (n.meta)
72
+ existing.meta = { ...(existing.meta ?? {}), ...n.meta };
71
73
  return n.id;
72
74
  }
73
75
  this.nodes.set(n.id, n);
@@ -125,6 +127,17 @@ function num(v) {
125
127
  function str(v) {
126
128
  return typeof v === 'string' && v.length > 0 ? v : null;
127
129
  }
130
+ function payloadObject(payload) {
131
+ if (!payload)
132
+ return {};
133
+ try {
134
+ const parsed = JSON.parse(payload);
135
+ return parsed && typeof parsed === 'object' ? parsed : {};
136
+ }
137
+ catch {
138
+ return {};
139
+ }
140
+ }
128
141
  /** Build a graph slice anchored on the most recent task units. */
129
142
  export function buildApmeGraph(store, opts = {}) {
130
143
  const limit = Math.min(Math.max(opts.limit ?? 60, 1), 400);
@@ -179,7 +192,44 @@ export function buildApmeGraph(store, opts = {}) {
179
192
  }
180
193
  // ── Trajectory: tools and files ──
181
194
  const events = store.listSampleEventRows(t.id);
195
+ const delegated = new Set();
182
196
  for (const ev of events) {
197
+ if (ev.kind === 'subagent') {
198
+ const p = payloadObject(ev.payload);
199
+ const childId = str(p.id);
200
+ const name = str(p.name);
201
+ if (!childId || !name)
202
+ continue;
203
+ let anchor = taskNode;
204
+ const turnId = str(ev.turnId);
205
+ if (includeTurns && turnId) {
206
+ anchor = g.node({
207
+ id: nid.turn(turnId), kind: 'turn',
208
+ label: `turn ${ev.turnIndex ?? '?'}`,
209
+ ts: ev.ts,
210
+ meta: { index: num(ev.turnIndex) },
211
+ });
212
+ g.edge(taskNode, anchor, 'contains');
213
+ }
214
+ const childKey = `${t.sessionId}:${childId}`;
215
+ const childNode = g.node({
216
+ id: nid.subagent(childKey), kind: 'subagent', label: name, ts: ev.ts,
217
+ meta: {
218
+ phase: p.phase === 'completed' ? 'completed' : 'started',
219
+ durationMs: num(p.durationMs),
220
+ summary: str(p.summary),
221
+ },
222
+ });
223
+ // Start + completion are two observations of ONE delegation edge, not
224
+ // a weighted edge of two. A completion without a captured start still
225
+ // gets the edge rather than disappearing.
226
+ const edgeKey = `${anchor}|${childNode}`;
227
+ if (!delegated.has(edgeKey)) {
228
+ delegated.add(edgeKey);
229
+ g.edge(anchor, childNode, 'delegated');
230
+ }
231
+ continue;
232
+ }
183
233
  if (ev.kind !== 'tool')
184
234
  continue;
185
235
  toolEvents++;