@codemieai/code 0.4.1 → 0.5.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 (167) hide show
  1. package/README.md +7 -1
  2. package/bin/codemie-kimi-acp.js +18 -0
  3. package/bin/codemie-kimi.js +18 -0
  4. package/dist/agents/core/AgentCLI.d.ts.map +1 -1
  5. package/dist/agents/core/AgentCLI.js +4 -0
  6. package/dist/agents/core/AgentCLI.js.map +1 -1
  7. package/dist/agents/core/BaseAgentAdapter.d.ts.map +1 -1
  8. package/dist/agents/core/BaseAgentAdapter.js +26 -6
  9. package/dist/agents/core/BaseAgentAdapter.js.map +1 -1
  10. package/dist/agents/core/metrics/types.d.ts +4 -1
  11. package/dist/agents/core/metrics/types.d.ts.map +1 -1
  12. package/dist/agents/core/session/BaseProcessor.d.ts +2 -0
  13. package/dist/agents/core/session/BaseProcessor.d.ts.map +1 -1
  14. package/dist/agents/core/session/BaseSessionAdapter.d.ts +10 -0
  15. package/dist/agents/core/session/BaseSessionAdapter.d.ts.map +1 -1
  16. package/dist/agents/core/types.d.ts +30 -0
  17. package/dist/agents/core/types.d.ts.map +1 -1
  18. package/dist/agents/plugins/claude/claude.plugin.js +3 -3
  19. package/dist/agents/plugins/claude/claude.session.d.ts.map +1 -1
  20. package/dist/agents/plugins/claude/claude.session.js +29 -7
  21. package/dist/agents/plugins/claude/claude.session.js.map +1 -1
  22. package/dist/agents/plugins/claude/session/claude-named-invocations.d.ts +28 -0
  23. package/dist/agents/plugins/claude/session/claude-named-invocations.d.ts.map +1 -0
  24. package/dist/agents/plugins/claude/session/claude-named-invocations.js +78 -0
  25. package/dist/agents/plugins/claude/session/claude-named-invocations.js.map +1 -0
  26. package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.d.ts.map +1 -1
  27. package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.js +10 -1
  28. package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.js.map +1 -1
  29. package/dist/agents/plugins/gemini/gemini.plugin.js.map +1 -1
  30. package/dist/agents/plugins/gemini/session/processors/gemini.metrics-processor.d.ts.map +1 -1
  31. package/dist/agents/plugins/gemini/session/processors/gemini.metrics-processor.js +3 -0
  32. package/dist/agents/plugins/gemini/session/processors/gemini.metrics-processor.js.map +1 -1
  33. package/dist/agents/plugins/kimi/extension/SKILL.md +6 -0
  34. package/dist/agents/plugins/kimi/extension/manifest.json +8 -0
  35. package/dist/agents/plugins/kimi/kimi-acp.plugin.d.ts +7 -0
  36. package/dist/agents/plugins/kimi/kimi-acp.plugin.d.ts.map +1 -0
  37. package/dist/agents/plugins/kimi/kimi-acp.plugin.js +28 -0
  38. package/dist/agents/plugins/kimi/kimi-acp.plugin.js.map +1 -0
  39. package/dist/agents/plugins/kimi/kimi.extension-installer.d.ts +56 -0
  40. package/dist/agents/plugins/kimi/kimi.extension-installer.d.ts.map +1 -0
  41. package/dist/agents/plugins/kimi/kimi.extension-installer.js +79 -0
  42. package/dist/agents/plugins/kimi/kimi.extension-installer.js.map +1 -0
  43. package/dist/agents/plugins/kimi/kimi.hook-config-injector.d.ts +34 -0
  44. package/dist/agents/plugins/kimi/kimi.hook-config-injector.d.ts.map +1 -0
  45. package/dist/agents/plugins/kimi/kimi.hook-config-injector.js +158 -0
  46. package/dist/agents/plugins/kimi/kimi.hook-config-injector.js.map +1 -0
  47. package/dist/agents/plugins/kimi/kimi.hook-transformer.d.ts +23 -0
  48. package/dist/agents/plugins/kimi/kimi.hook-transformer.d.ts.map +1 -0
  49. package/dist/agents/plugins/kimi/kimi.hook-transformer.js +46 -0
  50. package/dist/agents/plugins/kimi/kimi.hook-transformer.js.map +1 -0
  51. package/dist/agents/plugins/kimi/kimi.models.d.ts +8 -0
  52. package/dist/agents/plugins/kimi/kimi.models.d.ts.map +1 -0
  53. package/dist/agents/plugins/kimi/kimi.models.js +174 -0
  54. package/dist/agents/plugins/kimi/kimi.models.js.map +1 -0
  55. package/dist/agents/plugins/kimi/kimi.paths.d.ts +65 -0
  56. package/dist/agents/plugins/kimi/kimi.paths.d.ts.map +1 -0
  57. package/dist/agents/plugins/kimi/kimi.paths.js +95 -0
  58. package/dist/agents/plugins/kimi/kimi.paths.js.map +1 -0
  59. package/dist/agents/plugins/kimi/kimi.plugin.d.ts +37 -0
  60. package/dist/agents/plugins/kimi/kimi.plugin.d.ts.map +1 -0
  61. package/dist/agents/plugins/kimi/kimi.plugin.js +318 -0
  62. package/dist/agents/plugins/kimi/kimi.plugin.js.map +1 -0
  63. package/dist/agents/plugins/kimi/kimi.session.d.ts +40 -0
  64. package/dist/agents/plugins/kimi/kimi.session.d.ts.map +1 -0
  65. package/dist/agents/plugins/kimi/kimi.session.js +191 -0
  66. package/dist/agents/plugins/kimi/kimi.session.js.map +1 -0
  67. package/dist/agents/plugins/kimi/session/processors/kimi.metrics-processor.d.ts +43 -0
  68. package/dist/agents/plugins/kimi/session/processors/kimi.metrics-processor.d.ts.map +1 -0
  69. package/dist/agents/plugins/kimi/session/processors/kimi.metrics-processor.js +421 -0
  70. package/dist/agents/plugins/kimi/session/processors/kimi.metrics-processor.js.map +1 -0
  71. package/dist/agents/plugins/kimi/session/types.d.ts +81 -0
  72. package/dist/agents/plugins/kimi/session/types.d.ts.map +1 -0
  73. package/dist/agents/plugins/kimi/session/types.js +5 -0
  74. package/dist/agents/plugins/kimi/session/types.js.map +1 -0
  75. package/dist/agents/registry.d.ts.map +1 -1
  76. package/dist/agents/registry.js +4 -0
  77. package/dist/agents/registry.js.map +1 -1
  78. package/dist/cli/commands/analytics/aggregator.d.ts +14 -0
  79. package/dist/cli/commands/analytics/aggregator.d.ts.map +1 -1
  80. package/dist/cli/commands/analytics/aggregator.js +81 -1
  81. package/dist/cli/commands/analytics/aggregator.js.map +1 -1
  82. package/dist/cli/commands/analytics/cost/cost-calculator.d.ts +10 -0
  83. package/dist/cli/commands/analytics/cost/cost-calculator.d.ts.map +1 -1
  84. package/dist/cli/commands/analytics/cost/cost-calculator.js +9 -5
  85. package/dist/cli/commands/analytics/cost/cost-calculator.js.map +1 -1
  86. package/dist/cli/commands/analytics/cost/cost-enricher.d.ts +9 -1
  87. package/dist/cli/commands/analytics/cost/cost-enricher.d.ts.map +1 -1
  88. package/dist/cli/commands/analytics/cost/cost-enricher.js +141 -7
  89. package/dist/cli/commands/analytics/cost/cost-enricher.js.map +1 -1
  90. package/dist/cli/commands/analytics/cost/dispatch-extractor.d.ts +17 -0
  91. package/dist/cli/commands/analytics/cost/dispatch-extractor.d.ts.map +1 -0
  92. package/dist/cli/commands/analytics/cost/dispatch-extractor.js +78 -0
  93. package/dist/cli/commands/analytics/cost/dispatch-extractor.js.map +1 -0
  94. package/dist/cli/commands/analytics/cost/pricing.json +9 -2
  95. package/dist/cli/commands/analytics/cost/types.d.ts +33 -0
  96. package/dist/cli/commands/analytics/cost/types.d.ts.map +1 -1
  97. package/dist/cli/commands/analytics/cost/types.js +4 -1
  98. package/dist/cli/commands/analytics/cost/types.js.map +1 -1
  99. package/dist/cli/commands/analytics/cost/usage-readers.d.ts +22 -1
  100. package/dist/cli/commands/analytics/cost/usage-readers.d.ts.map +1 -1
  101. package/dist/cli/commands/analytics/cost/usage-readers.js +147 -21
  102. package/dist/cli/commands/analytics/cost/usage-readers.js.map +1 -1
  103. package/dist/cli/commands/analytics/model-normalizer.d.ts +2 -0
  104. package/dist/cli/commands/analytics/model-normalizer.d.ts.map +1 -1
  105. package/dist/cli/commands/analytics/model-normalizer.js +8 -0
  106. package/dist/cli/commands/analytics/model-normalizer.js.map +1 -1
  107. package/dist/cli/commands/analytics/native-loader.d.ts.map +1 -1
  108. package/dist/cli/commands/analytics/native-loader.js +35 -0
  109. package/dist/cli/commands/analytics/native-loader.js.map +1 -1
  110. package/dist/cli/commands/analytics/report/client/app.js +685 -53
  111. package/dist/cli/commands/analytics/report/payload-builder.d.ts.map +1 -1
  112. package/dist/cli/commands/analytics/report/payload-builder.js +13 -0
  113. package/dist/cli/commands/analytics/report/payload-builder.js.map +1 -1
  114. package/dist/cli/commands/analytics/report/template.html +108 -8
  115. package/dist/cli/commands/analytics/report/types.d.ts +13 -2
  116. package/dist/cli/commands/analytics/report/types.d.ts.map +1 -1
  117. package/dist/cli/commands/analytics/types.d.ts +18 -0
  118. package/dist/cli/commands/analytics/types.d.ts.map +1 -1
  119. package/dist/cli/commands/hook.d.ts.map +1 -1
  120. package/dist/cli/commands/hook.js +24 -7
  121. package/dist/cli/commands/hook.js.map +1 -1
  122. package/dist/migrations/004-skills-assistants-top-level.migration.d.ts.map +1 -1
  123. package/dist/migrations/004-skills-assistants-top-level.migration.js +8 -5
  124. package/dist/migrations/004-skills-assistants-top-level.migration.js.map +1 -1
  125. package/dist/migrations/005-skill-slug-format.migration.d.ts.map +1 -1
  126. package/dist/migrations/005-skill-slug-format.migration.js +14 -11
  127. package/dist/migrations/005-skill-slug-format.migration.js.map +1 -1
  128. package/dist/providers/index.d.ts +2 -0
  129. package/dist/providers/index.d.ts.map +1 -1
  130. package/dist/providers/index.js +2 -0
  131. package/dist/providers/index.js.map +1 -1
  132. package/dist/providers/plugins/moonshot-subscription/index.d.ts +9 -0
  133. package/dist/providers/plugins/moonshot-subscription/index.d.ts.map +1 -0
  134. package/dist/providers/plugins/moonshot-subscription/index.js +9 -0
  135. package/dist/providers/plugins/moonshot-subscription/index.js.map +1 -0
  136. package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.setup-steps.d.ts +8 -0
  137. package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.setup-steps.d.ts.map +1 -0
  138. package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.setup-steps.js +80 -0
  139. package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.setup-steps.js.map +1 -0
  140. package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.template.d.ts +11 -0
  141. package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.template.d.ts.map +1 -0
  142. package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.template.js +111 -0
  143. package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.template.js.map +1 -0
  144. package/dist/providers/plugins/sso/proxy/plugins/index.d.ts +2 -1
  145. package/dist/providers/plugins/sso/proxy/plugins/index.d.ts.map +1 -1
  146. package/dist/providers/plugins/sso/proxy/plugins/index.js +3 -1
  147. package/dist/providers/plugins/sso/proxy/plugins/index.js.map +1 -1
  148. package/dist/providers/plugins/sso/proxy/plugins/kimi-request-normalizer.plugin.d.ts +18 -0
  149. package/dist/providers/plugins/sso/proxy/plugins/kimi-request-normalizer.plugin.d.ts.map +1 -0
  150. package/dist/providers/plugins/sso/proxy/plugins/kimi-request-normalizer.plugin.js +56 -0
  151. package/dist/providers/plugins/sso/proxy/plugins/kimi-request-normalizer.plugin.js.map +1 -0
  152. package/dist/providers/plugins/sso/session/processors/conversations/constants.d.ts +1 -1
  153. package/dist/providers/plugins/sso/session/processors/conversations/constants.js +1 -1
  154. package/dist/providers/plugins/sso/sso.auth.d.ts +5 -0
  155. package/dist/providers/plugins/sso/sso.auth.d.ts.map +1 -1
  156. package/dist/providers/plugins/sso/sso.auth.js +23 -1
  157. package/dist/providers/plugins/sso/sso.auth.js.map +1 -1
  158. package/dist/utils/config.d.ts +24 -0
  159. package/dist/utils/config.d.ts.map +1 -1
  160. package/dist/utils/config.js +83 -8
  161. package/dist/utils/config.js.map +1 -1
  162. package/dist/utils/native-installer.d.ts.map +1 -1
  163. package/dist/utils/native-installer.js +6 -3
  164. package/dist/utils/native-installer.js.map +1 -1
  165. package/package.json +5 -1
  166. package/scripts/copy-plugins.js +5 -0
  167. package/scripts/test-desktop-telemetry.js +1 -1
@@ -14,6 +14,9 @@
14
14
  return;
15
15
  }
16
16
 
17
+ // sessionId -> record, for the session-detail modal (rows carry data-session).
18
+ var SESSION_BY_ID = (function () { var m = {}; (DATA.sessions || []).forEach(function (s) { m[s.sessionId] = s; }); return m; })();
19
+
17
20
  // ---- palette ------------------------------------------------------------
18
21
  var PALETTE = ['#7C5CFC', '#2297F6', '#F5A534', '#06B6D4', '#259F4C', '#F9303C', '#C084FC', '#E879A6'];
19
22
  var AGENT_COLORS = { claude: '#7C5CFC', 'claude-acp': '#9D7BFF', 'claude-desktop': '#B79DFF', gemini: '#F5A534', codex: '#06B6D4', opencode: '#259F4C', 'codemie-code': '#2297F6' };
@@ -48,6 +51,11 @@
48
51
  }
49
52
  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
50
53
  function shortPath(p) { var parts = String(p || '').split('/'); return parts[parts.length - 1] || p; }
54
+ // Human-readable session label: the cleaned first-prompt title, falling back to a short id.
55
+ function sessTitle(s) { return (s && s.title && s.title.trim()) ? s.title.trim() : ('#' + String((s && s.sessionId) || '').slice(0, 8)); }
56
+ function truncStr(s, n) { s = String(s == null ? '' : s); return s.length > n ? s.slice(0, n - 1) + '…' : s; }
57
+ // First n whitespace-delimited words (the "starting message" preview), '…' when truncated.
58
+ function firstWords(s, n) { s = String(s == null ? '' : s).trim(); var w = s.split(/\s+/); return w.length > n ? w.slice(0, n).join(' ') + '…' : s; }
51
59
 
52
60
  // ---- aggregation helpers ------------------------------------------------
53
61
  function sum(arr, f) { var t = 0; for (var i = 0; i < arr.length; i++) t += f(arr[i]) || 0; return t; }
@@ -147,6 +155,93 @@
147
155
  return cv;
148
156
  }
149
157
 
158
+ var INVOCATION_CHARTS = [
159
+ { kind: 'skill', title: 'Skills invoked', color: '#7c4fff' },
160
+ { kind: 'agent', title: 'Agent subtypes', color: '#ff6b6b' },
161
+ { kind: 'command', title: 'Slash commands', color: '#4a9eff' },
162
+ ];
163
+
164
+ function appendToolsAndModels(host, fs, chartFn) {
165
+ var toolAgg = new Map();
166
+ fs.forEach(function (s) {
167
+ (s.tools || []).forEach(function (t) {
168
+ var cur = toolAgg.get(t.toolName) || { total: 0, success: 0, failure: 0 };
169
+ cur.total += t.totalCalls; cur.success += t.successCount; cur.failure += t.failureCount;
170
+ toolAgg.set(t.toolName, cur);
171
+ });
172
+ });
173
+ var tools = Array.from(toolAgg.entries()).map(function (e) { return { name: e[0], total: e[1].total, success: e[1].success, rate: e[1].total ? Math.round((e[1].success / e[1].total) * 100) : 0 }; })
174
+ .sort(function (a, b) { return b.total - a.total; });
175
+ var maxTool = tools.length ? tools[0].total : 1;
176
+
177
+ var row = el('div', 'grid-2 mb16');
178
+ var toolCard = card('Tool usage & success rate');
179
+ tools.slice(0, 12).forEach(function (t) {
180
+ var bar = el('div', 'bar-row');
181
+ var color = t.rate >= 90 ? '#259F4C' : (t.rate >= 70 ? '#F5A534' : '#F9303C');
182
+ bar.innerHTML = '<span class="nm">' + esc(t.name) + '</span><div class="bar-track"><div class="bar-fill" style="width:' + Math.max(3, (t.total / maxTool) * 100) + '%;background:' + color + '"></div></div><span class="vl">' + fmtNum(t.total) + ' · ' + t.rate + '%</span>';
183
+ toolCard._body.appendChild(bar);
184
+ });
185
+ if (!tools.length) toolCard._body.appendChild(el('div', 'empty', 'No per-tool data.'));
186
+ row.appendChild(toolCard);
187
+
188
+ var modelAgg = new Map();
189
+ fs.forEach(function (s) { (s.perModelCost || []).forEach(function (m) { modelAgg.set(m.model, (modelAgg.get(m.model) || 0) + (m.tokens ? m.tokens.total : 0)); }); });
190
+ var modelCard = card('Tokens by model');
191
+ var mEntries = Array.from(modelAgg.entries()).sort(function (a, b) { return b[1] - a[1]; }).slice(0, 8);
192
+ if (mEntries.length) {
193
+ chartFn(canvasIn(modelCard._body), {
194
+ type: 'bar',
195
+ data: { labels: mEntries.map(function (e) { return e[0]; }), datasets: [{ data: mEntries.map(function (e) { return e[1]; }), backgroundColor: mEntries.map(function (_, i) { return PALETTE[i % PALETTE.length]; }), borderRadius: 5 }] },
196
+ options: { indexAxis: 'y', plugins: { legend: { display: false }, tooltip: { callbacks: { label: function (c) { return fmtTokens(c.parsed.x) + ' tokens'; } } } }, scales: { x: { grid: { color: GRID }, ticks: { callback: function (v) { return fmtTokens(v); } } }, y: { grid: { display: false } } } }
197
+ });
198
+ } else {
199
+ modelCard._body.appendChild(el('div', 'empty', 'No token data (sessions unpriced or no native logs).'));
200
+ }
201
+ row.appendChild(modelCard);
202
+ host.appendChild(row);
203
+ }
204
+
205
+ function appendInvocationCharts(host, fs, chartFn) {
206
+ INVOCATION_CHARTS.forEach(function (def) {
207
+ var agg = {};
208
+ fs.forEach(function (s) {
209
+ dispatchCounts(s, def.kind).forEach(function (x) {
210
+ agg[x.name] = (agg[x.name] || 0) + x.totalCalls;
211
+ });
212
+ });
213
+ var entries = Object.entries(agg)
214
+ .sort(function (a, b) { return b[1] - a[1]; })
215
+ .slice(0, 10);
216
+
217
+ var invCard = card(def.title);
218
+ if (entries.length) {
219
+ chartFn(canvasIn(invCard._body), {
220
+ type: 'bar',
221
+ data: {
222
+ labels: entries.map(function (e) { return e[0]; }),
223
+ datasets: [{
224
+ data: entries.map(function (e) { return e[1]; }),
225
+ backgroundColor: def.color,
226
+ borderRadius: 4,
227
+ }],
228
+ },
229
+ options: {
230
+ indexAxis: 'y',
231
+ plugins: { legend: { display: false } },
232
+ scales: {
233
+ x: { grid: { color: GRID }, ticks: { precision: 0 } },
234
+ y: { grid: { display: false } },
235
+ },
236
+ },
237
+ });
238
+ } else {
239
+ invCard._body.appendChild(el('div', 'empty', 'No data.'));
240
+ }
241
+ host.appendChild(invCard);
242
+ });
243
+ }
244
+
150
245
  // ---- small DOM builders -------------------------------------------------
151
246
  function el(tag, cls, html) { var e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; }
152
247
  function card(title, sub) {
@@ -222,6 +317,25 @@
222
317
  });
223
318
  host.appendChild(tgrid);
224
319
 
320
+ // efficiency headline KPIs (full detail on the Efficiency tab)
321
+ var effCacheReadCost = sum(fs, function (s) { return s.cacheReadCostUSD || 0; });
322
+ var effTotalCost = sum(fs, function (s) { return s.costUSD; });
323
+ var effCacheRead = sum(fs, function (s) { return s.tokens ? s.tokens.cacheRead : 0; });
324
+ var effTurns = sum(fs, function (s) { return s.turns; });
325
+ var effDead = fs.filter(function (s) { return s.costUSD > 0 && (s.filesChanged || 0) === 0 && s.netLines === 0; });
326
+ var effBloat = effTotalCost > 0 ? Math.round((effCacheReadCost / effTotalCost) * 1000) / 10 : 0;
327
+ var effAvgCtx = effTurns ? effCacheRead / effTurns : 0;
328
+ host.appendChild(el('div', 'kpi-section-label', 'Efficiency'));
329
+ var egrid = el('div', 'kpi-grid');
330
+ [['Cache-read cost', effTotalCost ? fmtUSD(effCacheReadCost) : '—', 'spend on context re-reads'],
331
+ ['Bloat %', effTotalCost ? (effBloat + '%') : '—', 'cache reads / total cost'],
332
+ ['Dead sessions', fmtNum(effDead.length), fs.length ? (Math.round((effDead.length / fs.length) * 100) + '% wasted') : ''],
333
+ ['Avg context / call', fmtTokens(Math.round(effAvgCtx)), 'see Efficiency tab']
334
+ ].forEach(function (k) {
335
+ var c = el('div', 'kpi'); c.appendChild(el('div', 'kpi-label', k[0])); c.appendChild(el('div', 'kpi-value', k[1])); if (k[2]) c.appendChild(el('div', 'kpi-sub', k[2])); egrid.appendChild(c);
336
+ });
337
+ host.appendChild(egrid);
338
+
225
339
  var row = el('div', 'grid-32 mb16');
226
340
  var trend = card('Net lines over time');
227
341
  row.appendChild(trend);
@@ -246,11 +360,24 @@
246
360
 
247
361
  // top projects
248
362
  var proj = groupBy(fs, function (s) { return s.project; });
249
- var pc = card('Top projects');
250
- var rows = Array.from(proj.entries()).map(function (e) { return { p: e[0], sessions: e[1].length, net: sum(e[1], function (s) { return s.netLines; }) }; })
251
- .sort(function (a, b) { return b.sessions - a.sessions; }).slice(0, 8);
252
- pc._body.innerHTML = tableHTML(['Project', 'Sessions', 'Net lines'],
253
- rows.map(function (r) { return ['<span title="' + esc(r.p) + '">' + esc(shortPath(r.p)) + '</span>', fmtNum(r.sessions), tdNum(r.net)]; }));
363
+ var pc = card('Top projects', 'files changed · lines added / removed · net');
364
+ var rows = Array.from(proj.entries()).map(function (e) {
365
+ return {
366
+ p: e[0], sessions: e[1].length,
367
+ files: sum(e[1], function (s) { return s.filesChanged || 0; }),
368
+ added: sum(e[1], function (s) { return s.linesAdded; }),
369
+ removed: sum(e[1], function (s) { return s.linesRemoved; }),
370
+ net: sum(e[1], function (s) { return s.netLines; })
371
+ };
372
+ }).sort(function (a, b) { return b.sessions - a.sessions; }).slice(0, 8);
373
+ pc._body.innerHTML = tableHTML(['Project', 'Sessions', 'Files', 'Lines +', 'Lines −', 'Net lines'],
374
+ rows.map(function (r) { return ['<span class="proj-link" data-proj-open="' + esc(r.p) + '" title="' + esc(r.p) + '">' + esc(shortPath(r.p)) + '</span>', fmtNum(r.sessions), tdNum(r.files), tdNum('+' + fmtNum(r.added)), tdNum('−' + fmtNum(r.removed)), tdNum(r.net)]; }));
375
+ pc._body.addEventListener('click', function (ev) {
376
+ var span = ev.target.closest('[data-proj-open]');
377
+ if (!span) return;
378
+ var path = span.getAttribute('data-proj-open');
379
+ openProjectModal(path, proj.get(path) || []);
380
+ });
254
381
  host.appendChild(pc);
255
382
  };
256
383
 
@@ -328,18 +455,29 @@
328
455
  var wrap = el('div', 'table-wrapper');
329
456
  var rows = Array.from(byProj.entries()).map(function (e) { return { p: e[0], ss: e[1] }; })
330
457
  .sort(function (a, b) { return b.ss.length - a.ss.length; });
331
- var html = '<table class="table"><thead><tr><th>Project</th><th class="td-number">Sessions</th><th class="td-number">Turns</th><th class="td-number">Net lines</th><th class="td-number">Tool success</th><th class="td-number">Cost</th></tr></thead><tbody>';
458
+ var html = '<table class="table"><thead><tr><th>Project</th><th class="td-number">Sessions</th><th class="td-number">Turns</th><th class="td-number">Files</th><th class="td-number">Lines +</th><th class="td-number">Lines −</th><th class="td-number">Net lines</th><th class="td-number">Tool success</th><th class="td-number">Cost</th></tr></thead><tbody>';
459
+ var crudCells = function (ss) {
460
+ return '<td class="td-number">' + fmtNum(sum(ss, function (s) { return s.filesChanged || 0; })) + '</td>'
461
+ + '<td class="td-number">+' + fmtNum(sum(ss, function (s) { return s.linesAdded; })) + '</td>'
462
+ + '<td class="td-number">−' + fmtNum(sum(ss, function (s) { return s.linesRemoved; })) + '</td>';
463
+ };
332
464
  rows.forEach(function (r, i) {
333
- html += '<tr class="clickable" data-proj="' + i + '"><td>▸ ' + esc(shortPath(r.p)) + '</td><td class="td-number">' + fmtNum(r.ss.length) + '</td><td class="td-number">' + fmtNum(sum(r.ss, function (s) { return s.turns; })) + '</td><td class="td-number">' + fmtNum(sum(r.ss, function (s) { return s.netLines; })) + '</td><td class="td-number">' + successRate(r.ss) + '%</td><td class="td-number">' + fmtUSD(sum(r.ss, function (s) { return s.costUSD; })) + '</td></tr>';
465
+ html += '<tr class="clickable" data-proj="' + i + '" data-proj-path="' + esc(r.p) + '"><td>▸ <span class="proj-link" data-proj-open="' + esc(r.p) + '">' + esc(shortPath(r.p)) + '</span></td><td class="td-number">' + fmtNum(r.ss.length) + '</td><td class="td-number">' + fmtNum(sum(r.ss, function (s) { return s.turns; })) + '</td>' + crudCells(r.ss) + '<td class="td-number">' + fmtNum(sum(r.ss, function (s) { return s.netLines; })) + '</td><td class="td-number">' + successRate(r.ss) + '%</td><td class="td-number">' + fmtUSD(sum(r.ss, function (s) { return s.costUSD; })) + '</td></tr>';
334
466
  // branch sub-rows (hidden)
335
467
  var byBranch = groupBy(r.ss, function (s) { return s.branch || '(none)'; });
336
468
  byBranch.forEach(function (bss, b) {
337
- html += '<tr class="drill" data-parent="' + i + '" style="display:none"><td style="padding-left:28px">⎇ ' + esc(b) + '</td><td class="td-number">' + bss.length + '</td><td class="td-number">' + fmtNum(sum(bss, function (s) { return s.turns; })) + '</td><td class="td-number">' + fmtNum(sum(bss, function (s) { return s.netLines; })) + '</td><td class="td-number">' + successRate(bss) + '%</td><td class="td-number">' + fmtUSD(sum(bss, function (s) { return s.costUSD; })) + '</td></tr>';
469
+ html += '<tr class="drill" data-parent="' + i + '" style="display:none"><td style="padding-left:28px">⎇ ' + esc(b) + '</td><td class="td-number">' + bss.length + '</td><td class="td-number">' + fmtNum(sum(bss, function (s) { return s.turns; })) + '</td>' + crudCells(bss) + '<td class="td-number">' + fmtNum(sum(bss, function (s) { return s.netLines; })) + '</td><td class="td-number">' + successRate(bss) + '%</td><td class="td-number">' + fmtUSD(sum(bss, function (s) { return s.costUSD; })) + '</td></tr>';
338
470
  });
339
471
  });
340
472
  html += '</tbody></table>';
341
473
  wrap.innerHTML = html;
342
474
  wrap.addEventListener('click', function (ev) {
475
+ var open = ev.target.closest('[data-proj-open]');
476
+ if (open) {
477
+ var path = open.getAttribute('data-proj-open');
478
+ openProjectModal(path, byProj.get(path) || []);
479
+ return;
480
+ }
343
481
  var tr = ev.target.closest('tr[data-proj]');
344
482
  if (!tr) return;
345
483
  var id = tr.getAttribute('data-proj');
@@ -354,47 +492,8 @@
354
492
  host.appendChild(el('h2', 'view-title', 'Tools & Models'));
355
493
  host.appendChild(el('p', 'view-sub', 'Tool usage across sessions and model token/cost distribution.'));
356
494
  if (!fs.length) { host.appendChild(el('div', 'empty', 'No sessions in view.')); return; }
357
-
358
- // aggregate tools across sessions
359
- var toolAgg = new Map();
360
- fs.forEach(function (s) {
361
- (s.tools || []).forEach(function (t) {
362
- var cur = toolAgg.get(t.toolName) || { total: 0, success: 0, failure: 0 };
363
- cur.total += t.totalCalls; cur.success += t.successCount; cur.failure += t.failureCount;
364
- toolAgg.set(t.toolName, cur);
365
- });
366
- });
367
- var tools = Array.from(toolAgg.entries()).map(function (e) { return { name: e[0], total: e[1].total, success: e[1].success, rate: e[1].total ? Math.round((e[1].success / e[1].total) * 100) : 0 }; })
368
- .sort(function (a, b) { return b.total - a.total; });
369
- var maxTool = tools.length ? tools[0].total : 1;
370
-
371
- var row = el('div', 'grid-2 mb16');
372
- var toolCard = card('Tool usage & success rate');
373
- tools.slice(0, 12).forEach(function (t) {
374
- var bar = el('div', 'bar-row');
375
- var color = t.rate >= 90 ? '#259F4C' : (t.rate >= 70 ? '#F5A534' : '#F9303C');
376
- bar.innerHTML = '<span class="nm">' + esc(t.name) + '</span><div class="bar-track"><div class="bar-fill" style="width:' + Math.max(3, (t.total / maxTool) * 100) + '%;background:' + color + '"></div></div><span class="vl">' + fmtNum(t.total) + ' · ' + t.rate + '%</span>';
377
- toolCard._body.appendChild(bar);
378
- });
379
- if (!tools.length) toolCard._body.appendChild(el('div', 'empty', 'No per-tool data.'));
380
- row.appendChild(toolCard);
381
-
382
- // models by tokens (from perModelCost)
383
- var modelAgg = new Map();
384
- fs.forEach(function (s) { (s.perModelCost || []).forEach(function (m) { modelAgg.set(m.model, (modelAgg.get(m.model) || 0) + (m.tokens ? m.tokens.total : 0)); }); });
385
- var modelCard = card('Tokens by model');
386
- var mEntries = Array.from(modelAgg.entries()).sort(function (a, b) { return b[1] - a[1]; }).slice(0, 8);
387
- if (mEntries.length) {
388
- makeChart(canvasIn(modelCard._body), {
389
- type: 'bar',
390
- data: { labels: mEntries.map(function (e) { return e[0]; }), datasets: [{ data: mEntries.map(function (e) { return e[1]; }), backgroundColor: mEntries.map(function (_, i) { return PALETTE[i % PALETTE.length]; }), borderRadius: 5 }] },
391
- options: { indexAxis: 'y', plugins: { legend: { display: false }, tooltip: { callbacks: { label: function (c) { return fmtTokens(c.parsed.x) + ' tokens'; } } } }, scales: { x: { grid: { color: GRID }, ticks: { callback: function (v) { return fmtTokens(v); } } }, y: { grid: { display: false } } } }
392
- });
393
- } else {
394
- modelCard._body.appendChild(el('div', 'empty', 'No token data (sessions unpriced or no native logs).'));
395
- }
396
- row.appendChild(modelCard);
397
- host.appendChild(row);
495
+ appendToolsAndModels(host, fs, makeChart);
496
+ appendInvocationCharts(host, fs, makeChart);
398
497
  };
399
498
 
400
499
  VIEWS.activity = function (host, fs) {
@@ -449,6 +548,162 @@
449
548
  host.appendChild(row);
450
549
  };
451
550
 
551
+ VIEWS.efficiency = function (host, fs) {
552
+ host.appendChild(el('h2', 'view-title', 'Efficiency'));
553
+ host.appendChild(el('p', 'view-sub', 'Context usage, waste, and how efficiently sessions convert spend into work. Estimates are labelled.'));
554
+ if (!fs.length) { host.appendChild(el('div', 'empty', 'No sessions in view.')); return; }
555
+
556
+ // ---- context & waste KPIs ----
557
+ var totalCacheRead = sum(fs, function (s) { return s.tokens ? s.tokens.cacheRead : 0; });
558
+ var totalTurns = sum(fs, function (s) { return s.turns; });
559
+ var totalCost = sum(fs, function (s) { return s.costUSD; });
560
+ var totalCacheReadCost = sum(fs, function (s) { return s.cacheReadCostUSD || 0; });
561
+ var avgCtx = totalTurns ? totalCacheRead / totalTurns : 0;
562
+ var worst = 0, worstS = null;
563
+ fs.forEach(function (s) {
564
+ var t = s.turns || 0, cr = s.tokens ? s.tokens.cacheRead : 0, v = t ? cr / t : 0;
565
+ if (v > worst) { worst = v; worstS = s; }
566
+ });
567
+ var bloatPct = totalCost > 0 ? Math.round((totalCacheReadCost / totalCost) * 1000) / 10 : 0;
568
+
569
+ var grid = el('div', 'kpi-grid');
570
+ [['Avg context / call', fmtTokens(Math.round(avgCtx)), 'cache tokens re-read each call'],
571
+ ['Worst session ctx / call', fmtTokens(Math.round(worst)), worstS ? esc(truncStr(sessTitle(worstS), 28)) : ''],
572
+ ['Cache-read cost', totalCacheReadCost ? fmtUSD(totalCacheReadCost) : '—', totalCost ? (bloatPct + '% of spend') : 'tokens × pricing'],
573
+ ['Bloat %', totalCost ? (bloatPct + '%') : '—', 'spend on context re-reads']
574
+ ].forEach(function (k) {
575
+ var c = el('div', 'kpi'); c.appendChild(el('div', 'kpi-label', k[0])); c.appendChild(el('div', 'kpi-value', k[1])); if (k[2]) c.appendChild(el('div', 'kpi-sub', k[2])); grid.appendChild(c);
576
+ });
577
+ host.appendChild(grid);
578
+
579
+ // ---- avg context/call per session (bar) + most bloated sessions (table) ----
580
+ var row1 = el('div', 'grid-2 mb16');
581
+ var ctxCard = card('Avg context / call per session', 'top 20 by context re-read');
582
+ var withCtx = fs.map(function (s) {
583
+ var t = s.turns || 0, cr = s.tokens ? s.tokens.cacheRead : 0;
584
+ return { s: s, v: t ? cr / t : 0 };
585
+ }).filter(function (x) { return x.v > 0; }).sort(function (a, b) { return b.v - a.v; }).slice(0, 20);
586
+ if (withCtx.length) {
587
+ var ctxTitles = withCtx.map(function (x) { return sessTitle(x.s); });
588
+ makeChart(canvasIn(ctxCard._body), {
589
+ type: 'bar',
590
+ data: { labels: withCtx.map(function (x) { return truncStr(sessTitle(x.s), 18); }), datasets: [{ data: withCtx.map(function (x) { return Math.round(x.v); }), backgroundColor: '#F5A534', borderRadius: 4 }] },
591
+ options: { plugins: { legend: { display: false }, tooltip: { callbacks: { title: function (items) { return ctxTitles[items[0].dataIndex]; }, label: function (c) { return fmtTokens(c.parsed.y) + ' / call'; } } } }, scales: { x: { grid: { display: false }, ticks: { maxTicksLimit: 12 } }, y: { grid: { color: GRID }, ticks: { callback: function (v) { return fmtTokens(v); } } } } }
592
+ });
593
+ } else { ctxCard._body.appendChild(el('div', 'empty', 'No cache-read data.')); }
594
+ row1.appendChild(ctxCard);
595
+
596
+ var bloatCard = card('Most bloated sessions', 'highest context re-read per call');
597
+ bloatCard._body.style.paddingTop = '0';
598
+ var bloated = fs.map(function (s) {
599
+ var t = s.turns || 0, cr = s.tokens ? s.tokens.cacheRead : 0;
600
+ return { s: s, ctx: t ? cr / t : 0, bloat: s.costUSD > 0 ? ((s.cacheReadCostUSD || 0) / s.costUSD) * 100 : 0 };
601
+ }).filter(function (x) { return x.ctx > 0; }).sort(function (a, b) { return b.ctx - a.ctx; }).slice(0, 10);
602
+ bloatCard._body.innerHTML = '<div class="table-wrapper">' + tableHTML(
603
+ ['Session', 'Agent', 'Model', 'Ctx/call', 'Cache read', 'Cost', 'Bloat%'],
604
+ bloated.map(function (x) {
605
+ var s = x.s;
606
+ return ['<span title="' + esc(sessTitle(s)) + '">' + esc(truncStr(sessTitle(s), 44)) + '</span>',
607
+ '<span class="tag tag-sm" style="text-transform:capitalize">' + esc(s.agentName) + '</span>',
608
+ '<span class="tag tag-sm">' + esc((s.models && s.models[0]) || '—') + '</span>',
609
+ fmtTokens(Math.round(x.ctx)), fmtTokens(s.tokens ? s.tokens.cacheRead : 0), fmtUSD(s.costUSD), (Math.round(x.bloat * 10) / 10) + '%'];
610
+ }),
611
+ [false, false, false, true, true, true, true],
612
+ bloated.map(function (x) { return 'class="clickable" data-session="' + esc(x.s.sessionId) + '"'; })) + '</div>';
613
+ bloatCard._body.addEventListener('click', function (ev) {
614
+ var tr = ev.target.closest('tr[data-session]'); if (!tr) return;
615
+ openSessionModal(SESSION_BY_ID[tr.getAttribute('data-session')]);
616
+ });
617
+ row1.appendChild(bloatCard);
618
+ host.appendChild(row1);
619
+
620
+ // ---- dead sessions ----
621
+ var dead = fs.filter(function (s) { return s.costUSD > 0 && (s.filesChanged || 0) === 0 && s.netLines === 0; });
622
+ var deadCost = sum(dead, function (s) { return s.costUSD; });
623
+ var deadCard = card('Dead sessions', 'cost spent, zero files changed and zero net lines — pure inference waste');
624
+ var dkv = el('div', 'kpi-grid'); dkv.style.gridTemplateColumns = 'repeat(3,1fr)';
625
+ [['Dead sessions', fmtNum(dead.length), fs.length ? (Math.round((dead.length / fs.length) * 100) + '% of sessions') : ''],
626
+ ['Wasted cost', fmtUSD(deadCost), totalCost ? (Math.round((deadCost / totalCost) * 100) + '% of spend') : ''],
627
+ ['Avg cost / dead', dead.length ? fmtUSD(deadCost / dead.length) : '—', 'per unproductive session']
628
+ ].forEach(function (k) {
629
+ var c = el('div', 'kpi'); c.appendChild(el('div', 'kpi-label', k[0])); c.appendChild(el('div', 'kpi-value', k[1])); if (k[2]) c.appendChild(el('div', 'kpi-sub', k[2])); dkv.appendChild(c);
630
+ });
631
+ deadCard._body.appendChild(dkv);
632
+ if (dead.length) {
633
+ var topDead = dead.slice().sort(function (a, b) { return b.costUSD - a.costUSD; }).slice(0, 10);
634
+ var dw = el('div', 'table-wrapper');
635
+ dw.innerHTML = tableHTML(['Session', 'Agent', 'Model', 'Turns', 'Cost'],
636
+ topDead.map(function (s) {
637
+ return ['<span title="' + esc(sessTitle(s)) + '">' + esc(truncStr(sessTitle(s), 44)) + '</span>',
638
+ '<span class="tag tag-sm" style="text-transform:capitalize">' + esc(s.agentName) + '</span>',
639
+ '<span class="tag tag-sm">' + esc((s.models && s.models[0]) || '—') + '</span>',
640
+ fmtNum(s.turns), fmtUSD(s.costUSD)];
641
+ }), [false, false, false, true, true]);
642
+ deadCard._body.appendChild(dw);
643
+ }
644
+ host.appendChild(deadCard);
645
+
646
+ // ---- session depth + command effectiveness ----
647
+ var row2 = el('div', 'grid-2 mb16');
648
+ var depthCard = card('Session depth', 'turns per session — long runs that never compact/restart');
649
+ var buckets = [['1', function (t) { return t <= 1; }], ['2–5', function (t) { return t >= 2 && t <= 5; }], ['6–10', function (t) { return t >= 6 && t <= 10; }], ['11–25', function (t) { return t >= 11 && t <= 25; }], ['26–50', function (t) { return t >= 26 && t <= 50; }], ['50+', function (t) { return t > 50; }]];
650
+ var counts = buckets.map(function (b) { return fs.filter(function (s) { return b[1](s.turns || 0); }).length; });
651
+ makeChart(canvasIn(depthCard._body), {
652
+ type: 'bar', data: { labels: buckets.map(function (b) { return b[0]; }), datasets: [{ data: counts, backgroundColor: '#7C5CFC', borderRadius: 4 }] },
653
+ options: { plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } }, y: { grid: { color: GRID }, ticks: { precision: 0 } } } }
654
+ });
655
+ var turnsArr = fs.map(function (s) { return s.turns || 0; }).sort(function (a, b) { return a - b; });
656
+ var median = 0;
657
+ if (turnsArr.length) {
658
+ var mid = Math.floor(turnsArr.length / 2);
659
+ median = turnsArr.length % 2 ? turnsArr[mid] : Math.round((turnsArr[mid - 1] + turnsArr[mid]) / 2);
660
+ }
661
+ var avgTurns = fs.length ? Math.round(totalTurns / fs.length) : 0;
662
+ depthCard._body.appendChild(el('p', 'text-muted', '<span style="font-size:12px">median ' + median + ' · avg ' + avgTurns + ' turns / session</span>'));
663
+ row2.appendChild(depthCard);
664
+
665
+ var cmdCard = card('Command effectiveness', 'est. cost per file changed, by command (session cost → its dominant command)');
666
+ // null-proto map: command names are data-derived, so a name like "__proto__" must not alias Object.prototype.
667
+ var cmdAgg = Object.create(null);
668
+ fs.forEach(function (s) {
669
+ var cmds = s.commandInvocations || [];
670
+ if (!cmds.length) return;
671
+ var dom = cmds.slice().sort(function (a, b) { return b.totalCalls - a.totalCalls; })[0];
672
+ var cur = cmdAgg[dom.name] || { cost: 0, artifacts: 0 };
673
+ cur.cost += s.costUSD; cur.artifacts += (s.filesChanged || 0);
674
+ cmdAgg[dom.name] = cur;
675
+ });
676
+ var cmdEntries = Object.keys(cmdAgg).map(function (name) {
677
+ var a = cmdAgg[name];
678
+ return { name: name, perArtifact: a.artifacts > 0 ? a.cost / a.artifacts : 0 };
679
+ }).filter(function (x) { return x.perArtifact > 0; }).sort(function (a, b) { return a.perArtifact - b.perArtifact; }).slice(0, 10);
680
+ if (cmdEntries.length) {
681
+ makeChart(canvasIn(cmdCard._body), {
682
+ type: 'bar',
683
+ data: { labels: cmdEntries.map(function (e) { return e.name; }), datasets: [{ data: cmdEntries.map(function (e) { return Math.round(e.perArtifact * 100) / 100; }), backgroundColor: '#06B6D4', borderRadius: 4 }] },
684
+ options: { indexAxis: 'y', plugins: { legend: { display: false }, tooltip: { callbacks: { label: function (c) { return fmtUSD(c.parsed.x) + ' / file'; } } } }, scales: { x: { grid: { color: GRID }, ticks: { callback: function (v) { return fmtUSD(v); } } }, y: { grid: { display: false } } } }
685
+ });
686
+ } else { cmdCard._body.appendChild(el('div', 'empty', 'No command + artifact data.')); }
687
+ row2.appendChild(cmdCard);
688
+ host.appendChild(row2);
689
+
690
+ // ---- code changes ----
691
+ var changeCard = card('Code changes', 'files & lines changed across sessions in view');
692
+ var cg = el('div', 'kpi-grid');
693
+ [['Files changed', fmtNum(sum(fs, function (s) { return s.filesChanged || 0; })), 'written or edited'],
694
+ ['Files written', fmtNum(sum(fs, function (s) { return s.filesWritten || 0; })), 'Write tool'],
695
+ ['Files edited', fmtNum(sum(fs, function (s) { return s.filesEdited || 0; })), 'Edit tool'],
696
+ ['Lines added', '+' + fmtNum(sum(fs, function (s) { return s.linesAdded; })), ''],
697
+ ['Lines removed', '−' + fmtNum(sum(fs, function (s) { return s.linesRemoved; })), ''],
698
+ ['Net lines', (sum(fs, function (s) { return s.netLines; }) >= 0 ? '+' : '') + fmtNum(sum(fs, function (s) { return s.netLines; })), '']
699
+ ].forEach(function (k) {
700
+ var c = el('div', 'kpi'); c.appendChild(el('div', 'kpi-label', k[0])); c.appendChild(el('div', 'kpi-value', k[1])); if (k[2]) c.appendChild(el('div', 'kpi-sub', k[2])); cg.appendChild(c);
701
+ });
702
+ changeCard._body.appendChild(cg);
703
+ changeCard._body.appendChild(el('p', 'text-muted', '<span style="font-size:12px">File deletions and line-level "modified" counts aren\'t tracked; Write counts the whole file as added.</span>'));
704
+ host.appendChild(changeCard);
705
+ };
706
+
452
707
  VIEWS.cost = function (host, fs) {
453
708
  host.appendChild(el('h2', 'view-title', 'Cost'));
454
709
  host.appendChild(el('p', 'view-sub', 'Estimated cost (API-equivalent) — token usage × model pricing. On a subscription you don’t pay per token; this is the equivalent metered API value.'));
@@ -536,6 +791,12 @@
536
791
  bar.appendChild(input); host.appendChild(bar);
537
792
  var c = card('All sessions'); c._body.style.paddingTop = '0';
538
793
  var holder = el('div', 'table-wrapper'); c._body.appendChild(holder); host.appendChild(c);
794
+ // Delegated row → modal. Attached ONCE on the persistent holder (draw() re-sets innerHTML
795
+ // on every keystroke, so a listener inside draw would stack and fire N times).
796
+ holder.addEventListener('click', function (ev) {
797
+ var tr = ev.target.closest('tr[data-session]'); if (!tr) return;
798
+ openSessionModal(SESSION_BY_ID[tr.getAttribute('data-session')]);
799
+ });
539
800
 
540
801
  function draw(q) {
541
802
  var list = fs.slice().sort(function (a, b) { return b.startTime - a.startTime; });
@@ -543,33 +804,403 @@
543
804
  var ql = q.toLowerCase();
544
805
  list = list.filter(function (s) { return (s.sessionId + ' ' + s.agentName + ' ' + s.project + ' ' + s.branch).toLowerCase().indexOf(ql) >= 0; });
545
806
  }
807
+ var shown = list.slice(0, 300);
546
808
  holder.innerHTML = tableHTML(
547
809
  ['Date', 'Agent', 'Project', 'Branch', 'Turns', 'Net lines', 'Input', 'Output', 'Cached', 'Cost'],
548
- list.slice(0, 300).map(function (s) {
810
+ shown.map(function (s) {
549
811
  return [new Date(s.startTime).toISOString().slice(0, 16).replace('T', ' '),
550
812
  '<span class="tag tag-sm" style="text-transform:capitalize">' + esc(s.agentName) + '</span>',
551
813
  '<span title="' + esc(s.project) + '">' + esc(shortPath(s.project)) + '</span>', esc(s.branch || '—'),
552
814
  fmtNum(s.turns), fmtNum(s.netLines), fmtTokens(tkIn(s)), fmtTokens(tkOut(s)), fmtTokens(tkCached(s)), fmtUSD(s.costUSD)];
553
815
  }),
554
- [false, false, false, false, true, true, true, true, true, true]);
816
+ [false, false, false, false, true, true, true, true, true, true],
817
+ shown.map(function (s) { return 'class="clickable" data-session="' + esc(s.sessionId) + '"'; }));
555
818
  if (list.length > 300) holder.appendChild(el('p', 'text-muted', '<span style="font-size:12px">Showing first 300 of ' + list.length + '.</span>'));
556
819
  }
557
820
  input.addEventListener('input', function () { draw(input.value.trim()); });
558
821
  draw('');
559
822
  };
560
823
 
824
+ // ---- session-detail modal ----------------------------------------------
825
+ var modalCharts = []; // owned by the modal; NOT in the global charts[] (decoupled from destroyCharts()).
826
+ var modalEsc = null;
827
+ function makeModalChart(canvas, config) {
828
+ if (!window.Chart) return null;
829
+ var c = new Chart(canvas, config);
830
+ modalCharts.push(c);
831
+ return c;
832
+ }
833
+ function closeSessionModal() {
834
+ modalCharts.forEach(function (c) { try { c.destroy(); } catch (e) {} });
835
+ modalCharts = [];
836
+ if (modalEsc) { document.removeEventListener('keydown', modalEsc); modalEsc = null; }
837
+ var ov = document.getElementById('session-modal'); if (ov && ov.parentNode) ov.parentNode.removeChild(ov);
838
+ }
839
+ var MODAL_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
840
+ function fmtWhen(ms) { var d = new Date(ms); return MODAL_MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); }
841
+ function hashStr(s) { var h = 0; s = String(s); for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; return Math.abs(h); }
842
+ // Lighter, borderless stat grid (label + value), replacing the heavy box-in-box KPI cards.
843
+ function statsEl(items) { // items: [label, value(html-safe), sub]
844
+ var g = el('div', 'modal-stats');
845
+ items.forEach(function (k) {
846
+ var c = el('div', 'mstat');
847
+ c.appendChild(el('div', 'mlabel', esc(k[0])));
848
+ c.appendChild(el('div', 'mval', k[1])); // value is formatter output (fmt*), not user text
849
+ if (k[2]) c.appendChild(el('div', 'msub', esc(k[2])));
850
+ g.appendChild(c);
851
+ });
852
+ return g;
853
+ }
854
+ function sumCalls(list) { return (list || []).reduce(function (a, n) { return a + (n.totalCalls || 0); }, 0); }
855
+ // Name→count list for a dispatch kind, derived from the timed native-log dispatches (the timeline's
856
+ // source) so counts/chips stay consistent with it; falls back to the tracked named-invocation
857
+ // counts when this session has no dispatch data (e.g. older/tracked-only sessions).
858
+ function dispatchCounts(s, kind) {
859
+ var disp = (s.dispatches || []).filter(function (d) { return d.kind === kind; });
860
+ if (disp.length) {
861
+ var m = {};
862
+ disp.forEach(function (d) { m[d.name] = (m[d.name] || 0) + 1; });
863
+ return Object.keys(m).map(function (n) { return { name: n, totalCalls: m[n] }; });
864
+ }
865
+ return kind === 'agent' ? (s.agentInvocations || []) : kind === 'skill' ? (s.skillInvocations || []) : (s.commandInvocations || []);
866
+ }
867
+ function chipsEl(title, list) { // list: NamedInvocationStats[]
868
+ var sec = el('div', 'dispatch-col');
869
+ sec.appendChild(el('h4', null, esc(title) + ' (' + (list ? list.length : 0) + ')'));
870
+ if (!list || !list.length) { sec.appendChild(el('div', 'text-muted', '<span style="font-size:12px">none</span>')); return sec; }
871
+ var wrap = el('div', 'modal-chips');
872
+ list.slice().sort(function (a, b) { return b.totalCalls - a.totalCalls; }).forEach(function (n) {
873
+ wrap.appendChild(el('span', 'chip', esc(n.name) + '<b>×' + fmtNum(n.totalCalls) + '</b>'));
874
+ });
875
+ sec.appendChild(wrap); return sec;
876
+ }
877
+ // One timeline row: label + track with a positioned bar + a right-aligned duration. All text esc()'d.
878
+ function tlRow(label, color, leftPct, wPct, durText, barText, strong) {
879
+ var row = el('div', 'tl-row');
880
+ row.appendChild(el('div', 'tl-label' + (strong ? ' tl-label-strong' : ''), esc(label)));
881
+ var track = el('div', 'tl-track');
882
+ var bar = el('div', 'tl-bar');
883
+ bar.style.left = 'calc(min(' + leftPct + '%, 100% - 28px))'; bar.style.width = wPct + '%'; bar.style.background = color;
884
+ if (barText) {
885
+ bar.classList.add('tl-bar-has-text');
886
+ bar.innerHTML = '<span class="tl-bar-text">' + esc(barText) + '</span>';
887
+ }
888
+ bar.title = label + ' · ' + durText; // .title property = safe (not parsed as HTML)
889
+ track.appendChild(bar); row.appendChild(track);
890
+ row.appendChild(el('div', 'tl-dur', esc(durText)));
891
+ return row;
892
+ }
893
+ // Precise wall-clock timing for short timeline steps. The general report formatter rounds
894
+ // sub-minute values up to one minute, which is useful for sessions but misleading here.
895
+ function fmtTimelineDuration(ms) {
896
+ ms = Math.max(0, ms || 0);
897
+ if (ms < 1000) return Math.round(ms) + 'ms';
898
+ if (ms < 60000) return (Math.round(ms / 100) / 10) + 's';
899
+ if (ms < 3600000) return Math.floor(ms / 60000) + 'm ' + Math.round((ms % 60000) / 1000) + 's';
900
+ return Math.floor(ms / 3600000) + 'h ' + Math.round((ms % 3600000) / 60000) + 'm';
901
+ }
902
+ // Side panel content for a selected timeline step.
903
+ function tlDetailEl(d) {
904
+ var panel = el('div', '');
905
+ if (!d) {
906
+ panel.appendChild(el('div', 'tl-side-empty', 'Click a timeline step to see details.'));
907
+ return panel;
908
+ }
909
+ var color = PALETTE[hashStr(d.kind + ':' + d.name) % PALETTE.length];
910
+ var nameEl = el('div', 'tl-side-name');
911
+ var dot = el('span', 'tl-side-dot'); dot.style.background = color;
912
+ nameEl.appendChild(dot);
913
+ nameEl.appendChild(document.createTextNode(d.kind + ' · ' + d.name));
914
+ panel.appendChild(nameEl);
915
+ var hasCost = d.costUSD != null;
916
+ var hasTok = d.tokens != null;
917
+ panel.appendChild(statsEl([
918
+ ['Cost', hasCost ? fmtUSD(d.costUSD) : '—', ''],
919
+ ['Tokens', hasTok ? fmtTokens(d.tokens.total) : '—', ''],
920
+ ['Duration', fmtTimelineDuration(d.durationMs), ''],
921
+ ]));
922
+ if (hasTok) {
923
+ var tok = d.tokens;
924
+ var total = Math.max(tok.total, 1);
925
+ var barEl = el('div', 'tl-tok-bar');
926
+ var segs = [
927
+ { val: tok.input, color: '#7C5CFC' },
928
+ { val: tok.output, color: '#F5A534' },
929
+ { val: tok.cacheRead, color: '#259F4C' },
930
+ { val: tok.cacheCreation, color: '#3B82F6' },
931
+ ].filter(function(s) { return s.val > 0; });
932
+ segs.forEach(function(s) {
933
+ var seg = el('div', 'tl-tok-seg');
934
+ seg.style.flex = String(s.val / total * 100);
935
+ seg.style.background = s.color;
936
+ barEl.appendChild(seg);
937
+ });
938
+ var tokSec = el('div', 'tl-side-section');
939
+ tokSec.appendChild(el('div', 'tl-side-label', 'Token breakdown'));
940
+ tokSec.appendChild(barEl);
941
+ tokSec.appendChild(statsEl([
942
+ ['Input', fmtTokens(tok.input), ''],
943
+ ['Output', fmtTokens(tok.output), ''],
944
+ ['Cache read', fmtTokens(tok.cacheRead), ''],
945
+ ['Cache write', fmtTokens(tok.cacheCreation), ''],
946
+ ]));
947
+ panel.appendChild(tokSec);
948
+ }
949
+ if (d.tools && d.tools.length) {
950
+ var toolSec = el('div', 'tl-side-section');
951
+ toolSec.appendChild(el('div', 'tl-side-label', 'Tools called'));
952
+ var chips = el('div', 'modal-chips');
953
+ d.tools.forEach(function(t) {
954
+ chips.appendChild(el('span', 'chip', esc(t.name) + '<b>\xd7' + fmtNum(t.calls) + '</b>'));
955
+ });
956
+ toolSec.appendChild(chips);
957
+ panel.appendChild(toolSec);
958
+ }
959
+ return panel;
960
+ }
961
+ // Gantt of top-level agent dispatches: session span on top, each agent bar positioned
962
+ // by wall-clock start time relative to the session start (proportional scaling).
963
+ // Skills and commands are excluded — they have no separate transcript or cost to show.
964
+ // min-width 28px (CSS) keeps short agents visible and clickable without distorting the scale.
965
+ function timelineEl(s) {
966
+ var dispatches = (s.dispatches || []).filter(function (d) { return d.kind === 'agent'; }).sort(function (a, b) { return a.start - b.start; });
967
+ // Scale bars to the agent activity window, not the full session duration.
968
+ // Agent bars fill the track proportionally relative to each other; absolute
969
+ // start offset (vs. session start) is shown in the side panel detail view.
970
+ var actStart = dispatches.length ? dispatches.reduce(function (m, d) { return Math.min(m, d.start); }, Infinity) : s.startTime;
971
+ var actEnd = dispatches.length ? dispatches.reduce(function (m, d) { return Math.max(m, d.start + (d.durationMs || 0)); }, -Infinity) : s.startTime + s.durationMs;
972
+ var ganttSpan = Math.max(1, actEnd - actStart);
973
+
974
+ var container = el('div', 'tl-container');
975
+ var gantt = el('div', 'tl-gantt timeline');
976
+ var sidePane = el('div', 'tl-side');
977
+
978
+ var selected = dispatches[0] || null;
979
+ sidePane.appendChild(tlDetailEl(selected));
980
+
981
+ function selectDispatch(d, barEl) {
982
+ gantt.querySelectorAll('.tl-bar[data-dispatch]').forEach(function(b) {
983
+ b.style.outline = b === barEl ? '2px solid rgba(255,255,255,0.55)' : 'none';
984
+ });
985
+ sidePane.innerHTML = '';
986
+ sidePane.appendChild(tlDetailEl(d));
987
+ }
988
+
989
+ gantt.appendChild(tlRow('session', '#259F4C', 0, 100, fmtTimelineDuration(s.durationMs), fmtUSD(s.costUSD), true));
990
+
991
+ var occ = {};
992
+ dispatches.forEach(function (d) {
993
+ var key = d.kind + ':' + d.name;
994
+ var total = dispatches.filter(function (x) { return x.kind === d.kind && x.name === d.name; }).length;
995
+ occ[key] = (occ[key] || 0) + 1;
996
+ var label = d.kind + ' · ' + d.name + (total > 1 ? ' #' + occ[key] : '');
997
+ var leftPct = Math.max(0, (d.start - actStart) / ganttSpan * 100);
998
+ var wPct = (d.durationMs || 0) / ganttSpan * 100;
999
+ if (leftPct + wPct > 100) wPct = Math.max(0, 100 - leftPct);
1000
+ var barText = d.costUSD != null && wPct >= 8 ? fmtUSD(d.costUSD) : '';
1001
+ var color = PALETTE[hashStr(key) % PALETTE.length];
1002
+ var row = tlRow(label, color, leftPct, wPct, fmtTimelineDuration(d.durationMs), barText, false);
1003
+ var bar = row.querySelector('.tl-bar');
1004
+ if (bar) {
1005
+ bar.setAttribute('data-dispatch', key);
1006
+ if (d === selected) bar.style.outline = '2px solid rgba(255,255,255,0.55)';
1007
+ }
1008
+ row.style.cursor = 'pointer';
1009
+ row.addEventListener('click', function () { selectDispatch(d, bar); });
1010
+ gantt.appendChild(row);
1011
+ });
1012
+
1013
+ container.appendChild(gantt);
1014
+ container.appendChild(sidePane);
1015
+ return container;
1016
+ }
1017
+ function openSessionModal(s, onBack) {
1018
+ if (!s) return;
1019
+ closeSessionModal();
1020
+ var ov = el('div', 'modal-overlay'); ov.id = 'session-modal';
1021
+ var modal = el('div', 'modal');
1022
+
1023
+ // header — every interpolation through esc(); title is arbitrary user text (XSS vector).
1024
+ var head = el('div', 'modal-head');
1025
+ var htxt = el('div');
1026
+ // onBack (optional): when this modal was reached from another modal (e.g. the project
1027
+ // sessions list), render a Back affordance that returns there instead of just closing.
1028
+ if (onBack) {
1029
+ var back = el('button', 'modal-back', '← Back');
1030
+ back.setAttribute('aria-label', 'Back to project');
1031
+ back.addEventListener('click', function () { closeSessionModal(); onBack(); });
1032
+ htxt.appendChild(back);
1033
+ }
1034
+ htxt.appendChild(el('div', 'modal-title', esc(truncStr(firstWords(sessTitle(s), 10), 120))));
1035
+ var metaBits = [s.agentName, (s.models && s.models[0]) || null, shortPath(s.project), s.branch].filter(Boolean);
1036
+ htxt.appendChild(el('div', 'modal-meta', metaBits.map(function (b) { return esc(b); }).join(' · ')));
1037
+ head.appendChild(htxt);
1038
+ var close = el('button', 'modal-close', '✕'); close.setAttribute('aria-label', 'Close'); close.addEventListener('click', closeSessionModal);
1039
+ head.appendChild(close);
1040
+ modal.appendChild(head);
1041
+
1042
+ var body = el('div', 'modal-body');
1043
+
1044
+ // Cost & Time / Token Usage / Activity — light borderless stats, equal-height cards.
1045
+ var t = s.tokens || { input: 0, output: 0, cacheRead: 0, cacheCreation: 0, total: 0 };
1046
+ var grid3 = el('div', 'grid-3');
1047
+ var costCard = card('Cost & Time'); costCard._body.appendChild(statsEl([
1048
+ ['Cost', fmtUSD(s.costUSD), 'API-equivalent'],
1049
+ ['Cache-read', s.cacheReadCostUSD ? fmtUSD(s.cacheReadCostUSD) : '—', ''],
1050
+ ['Duration', fmtDuration(s.durationMs || 0), ''],
1051
+ ['Started', '<span class="mval-sm">' + esc(fmtWhen(s.startTime)) + '</span>', '']
1052
+ ]));
1053
+ var tokCard = card('Token usage'); tokCard._body.appendChild(statsEl([
1054
+ ['Input', fmtTokens(t.input), ''], ['Output', fmtTokens(t.output), ''],
1055
+ ['Cache read', fmtTokens(t.cacheRead), ''], ['Cache create', fmtTokens(t.cacheCreation), ''],
1056
+ ['Total', fmtTokens(t.total), '']
1057
+ ]));
1058
+ var actCard = card('Activity'); actCard._body.appendChild(statsEl([
1059
+ ['Turns / API', fmtNum(s.turns), ''],
1060
+ ['Tool calls', fmtNum(s.toolCallsTotal), (s.toolCallsTotal ? Math.round((s.toolCallsSuccess / s.toolCallsTotal) * 100) + '% ok' : '')],
1061
+ ['Agents', fmtNum(sumCalls(dispatchCounts(s, 'agent'))), ''],
1062
+ ['Skills', fmtNum(sumCalls(dispatchCounts(s, 'skill'))), ''],
1063
+ ['Commands', fmtNum(sumCalls(dispatchCounts(s, 'command'))), '']
1064
+ ]));
1065
+ grid3.appendChild(costCard); grid3.appendChild(tokCard); grid3.appendChild(actCard);
1066
+ body.appendChild(grid3);
1067
+
1068
+ // Code changes
1069
+ var ccCard = card('Code changes'); ccCard._body.appendChild(statsEl([
1070
+ ['Files changed', fmtNum(s.filesChanged || 0), 'written or edited'],
1071
+ ['Lines added', '+' + fmtNum(s.linesAdded || 0), ''],
1072
+ ['Lines removed', '−' + fmtNum(s.linesRemoved || 0), ''],
1073
+ ['Net lines', ((s.netLines || 0) >= 0 ? '+' : '') + fmtNum(s.netLines || 0), '']
1074
+ ]));
1075
+ body.appendChild(ccCard);
1076
+
1077
+ // Token & cost growth chart (backfilled per-turn series; honest fallback when absent)
1078
+ var growth = card('Token & cost growth', 'cumulative per turn — from the native log');
1079
+ var series = s.costSeries || [];
1080
+ if (series.length >= 2 && window.Chart) {
1081
+ var useTs = series[0].t > 1e12; // epoch ms vs turn ordinal
1082
+ var t0 = series[0].t;
1083
+ var labels = series.map(function (p) { return useTs ? fmtDuration(Math.max(0, p.t - t0)) : ('turn ' + p.t); });
1084
+ var cv = canvasIn(growth._body, 220);
1085
+ makeModalChart(cv, {
1086
+ type: 'line',
1087
+ data: { labels: labels, datasets: [
1088
+ { label: 'Cost ($)', data: series.map(function (p) { return Math.round(p.cost * 10000) / 10000; }), borderColor: '#7C5CFC', backgroundColor: 'rgba(124,92,252,0.12)', fill: true, yAxisID: 'y', tension: 0.25, pointRadius: 0 },
1089
+ { label: 'Tokens', data: series.map(function (p) { return p.tokens; }), borderColor: '#F5A534', backgroundColor: 'transparent', fill: false, yAxisID: 'y1', tension: 0.25, pointRadius: 0 }
1090
+ ] },
1091
+ options: { interaction: { mode: 'index', intersect: false }, plugins: { legend: { display: true } },
1092
+ scales: { x: { grid: { display: false }, ticks: { maxTicksLimit: 8 } },
1093
+ y: { position: 'left', grid: { color: GRID }, ticks: { callback: function (v) { return fmtUSD(v); } } },
1094
+ y1: { position: 'right', grid: { display: false }, ticks: { callback: function (v) { return fmtTokens(v); } } } } }
1095
+ });
1096
+ } else {
1097
+ growth._body.appendChild(el('div', 'empty', 'Per-turn data not available for this session.'));
1098
+ }
1099
+ body.appendChild(growth);
1100
+
1101
+ // Timeline — Gantt of all top-level agent, skill, and command dispatches.
1102
+ var hasDispatches = (s.dispatches || []).length > 0;
1103
+ var hasCostData = (s.dispatches || []).some(function (d) { return d.kind === 'agent' && d.costUSD != null; });
1104
+ var tlSubtitle = hasDispatches ? (hasCostData ? 'click a step for cost, token & timing details · long idle gaps compressed' : 'click a step for timing details · long idle gaps compressed') : '';
1105
+ var tlCard = card('Timeline', tlSubtitle);
1106
+ if (hasDispatches) {
1107
+ tlCard._body.appendChild(timelineEl(s));
1108
+ } else {
1109
+ tlCard._body.appendChild(el('div', 'empty', 'No agent, skill, or command dispatches recorded for this session.'));
1110
+ }
1111
+ body.appendChild(tlCard);
1112
+
1113
+ appendToolsAndModels(body, [s], makeModalChart);
1114
+ appendInvocationCharts(body, [s], makeModalChart);
1115
+
1116
+ modal.appendChild(body);
1117
+ ov.appendChild(modal);
1118
+ ov.addEventListener('click', function (ev) { if (ev.target === ov) closeSessionModal(); });
1119
+ modalEsc = function (ev) { if (ev.key === 'Escape') closeSessionModal(); };
1120
+ document.addEventListener('keydown', modalEsc);
1121
+ document.body.appendChild(ov);
1122
+ }
1123
+
1124
+ // ---- project-sessions modal --------------------------------------------
1125
+ var projModalEsc = null;
1126
+ function closeProjectModal() {
1127
+ if (projModalEsc) { document.removeEventListener('keydown', projModalEsc); projModalEsc = null; }
1128
+ var ov = document.getElementById('project-modal'); if (ov && ov.parentNode) ov.parentNode.removeChild(ov);
1129
+ }
1130
+ function openProjectModal(projectPath, sessions) {
1131
+ closeProjectModal();
1132
+ var sorted = sessions.slice().sort(function (a, b) { return b.startTime - a.startTime; });
1133
+ var ov = el('div', 'modal-overlay'); ov.id = 'project-modal';
1134
+ var modal = el('div', 'modal');
1135
+
1136
+ var head = el('div', 'modal-head');
1137
+ var htxt = el('div');
1138
+ htxt.appendChild(el('div', 'modal-title', esc(shortPath(projectPath))));
1139
+ htxt.appendChild(el('div', 'modal-meta', esc(projectPath)));
1140
+ head.appendChild(htxt);
1141
+ var close = el('button', 'modal-close', '✕'); close.setAttribute('aria-label', 'Close'); close.addEventListener('click', closeProjectModal);
1142
+ head.appendChild(close);
1143
+ modal.appendChild(head);
1144
+
1145
+ var body = el('div', 'modal-body');
1146
+ var totalCost = sum(sessions, function (s) { return s.costUSD || 0; });
1147
+ var totalTurns = sum(sessions, function (s) { return s.turns || 0; });
1148
+ var netLines = sum(sessions, function (s) { return s.netLines || 0; });
1149
+ body.appendChild(statsEl([
1150
+ ['Sessions', fmtNum(sessions.length)],
1151
+ ['Total cost', fmtUSD(totalCost)],
1152
+ ['Turns', fmtNum(totalTurns)],
1153
+ ['Net lines', (netLines >= 0 ? '+' : '') + fmtNum(netLines)]
1154
+ ]));
1155
+
1156
+ var tbl = el('div', 'table-wrapper proj-sessions');
1157
+ tbl.innerHTML = tableHTML(
1158
+ ['Session', 'Date', 'Turns', 'Cost', ''],
1159
+ sorted.map(function (s) {
1160
+ return [
1161
+ // Truncate to a short preview (~12 words / 100 chars): keeps the table compact and
1162
+ // avoids exposing the full first-prompt text in the project list.
1163
+ esc(truncStr(firstWords(sessTitle(s), 12), 100)),
1164
+ esc(fmtWhen(s.startTime)),
1165
+ fmtNum(s.turns || 0),
1166
+ fmtUSD(s.costUSD || 0),
1167
+ '<button class="btn-open" data-open-session="' + esc(s.sessionId) + '">Open ↗</button>'
1168
+ ];
1169
+ }),
1170
+ [false, false, true, true, false]
1171
+ );
1172
+ tbl.addEventListener('click', function (ev) {
1173
+ var btn = ev.target.closest('[data-open-session]');
1174
+ if (!btn) return;
1175
+ var sid = btn.getAttribute('data-open-session');
1176
+ closeProjectModal();
1177
+ // Pass an onBack closure so the session modal can return to this project list.
1178
+ openSessionModal(SESSION_BY_ID[sid], function () { openProjectModal(projectPath, sessions); });
1179
+ });
1180
+ body.appendChild(tbl);
1181
+ modal.appendChild(body);
1182
+ ov.appendChild(modal);
1183
+ ov.addEventListener('click', function (ev) { if (ev.target === ov) closeProjectModal(); });
1184
+ projModalEsc = function (ev) { if (ev.key === 'Escape') closeProjectModal(); };
1185
+ document.addEventListener('keydown', projModalEsc);
1186
+ document.body.appendChild(ov);
1187
+ }
1188
+
561
1189
  // ---- table + misc helpers ----------------------------------------------
562
1190
  function tdNum(v) { return '<span class="td-number">' + (typeof v === 'number' ? fmtNum(v) : v) + '</span>'; }
563
1191
  // numericCols: optional boolean[] marking right-aligned numeric columns. Default = every
564
1192
  // column except the first (back-compat). Text columns (agent, project, branch, status) must
565
1193
  // be left-aligned, so callers with interleaved/leading text pass an explicit mask.
566
- function tableHTML(headers, rows, numericCols) {
1194
+ function tableHTML(headers, rows, numericCols, rowAttrs) {
567
1195
  var isNum = numericCols || headers.map(function (_, i) { return i > 0; });
568
1196
  var h = '<table class="table"><thead><tr>';
569
1197
  headers.forEach(function (x, i) { h += '<th' + (isNum[i] ? ' class="td-number"' : '') + '>' + esc(x) + '</th>'; });
570
1198
  h += '</tr></thead><tbody>';
571
1199
  if (!rows.length) h += '<tr><td colspan="' + headers.length + '" class="text-muted">No data</td></tr>';
572
- rows.forEach(function (r) { h += '<tr>' + r.map(function (cell, i) { return '<td' + (isNum[i] ? ' class="td-number"' : '') + '>' + cell + '</td>'; }).join('') + '</tr>'; });
1200
+ rows.forEach(function (r, ri) {
1201
+ var attrs = rowAttrs && rowAttrs[ri] ? ' ' + rowAttrs[ri] : '';
1202
+ h += '<tr' + attrs + '>' + r.map(function (cell, i) { return '<td' + (isNum[i] ? ' class="td-number"' : '') + '>' + cell + '</td>'; }).join('') + '</tr>';
1203
+ });
573
1204
  return h + '</tbody></table>';
574
1205
  }
575
1206
  // tokens shorthand: cached = cacheRead + cacheCreation (the prompt-cache reuse + writes)
@@ -586,6 +1217,7 @@
586
1217
  }
587
1218
 
588
1219
  function render() {
1220
+ closeSessionModal();
589
1221
  applyChartTheme();
590
1222
  destroyCharts();
591
1223
  root.innerHTML = '';