@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.
- package/README.md +7 -1
- package/bin/codemie-kimi-acp.js +18 -0
- package/bin/codemie-kimi.js +18 -0
- package/dist/agents/core/AgentCLI.d.ts.map +1 -1
- package/dist/agents/core/AgentCLI.js +4 -0
- package/dist/agents/core/AgentCLI.js.map +1 -1
- package/dist/agents/core/BaseAgentAdapter.d.ts.map +1 -1
- package/dist/agents/core/BaseAgentAdapter.js +26 -6
- package/dist/agents/core/BaseAgentAdapter.js.map +1 -1
- package/dist/agents/core/metrics/types.d.ts +4 -1
- package/dist/agents/core/metrics/types.d.ts.map +1 -1
- package/dist/agents/core/session/BaseProcessor.d.ts +2 -0
- package/dist/agents/core/session/BaseProcessor.d.ts.map +1 -1
- package/dist/agents/core/session/BaseSessionAdapter.d.ts +10 -0
- package/dist/agents/core/session/BaseSessionAdapter.d.ts.map +1 -1
- package/dist/agents/core/types.d.ts +30 -0
- package/dist/agents/core/types.d.ts.map +1 -1
- package/dist/agents/plugins/claude/claude.plugin.js +3 -3
- package/dist/agents/plugins/claude/claude.session.d.ts.map +1 -1
- package/dist/agents/plugins/claude/claude.session.js +29 -7
- package/dist/agents/plugins/claude/claude.session.js.map +1 -1
- package/dist/agents/plugins/claude/session/claude-named-invocations.d.ts +28 -0
- package/dist/agents/plugins/claude/session/claude-named-invocations.d.ts.map +1 -0
- package/dist/agents/plugins/claude/session/claude-named-invocations.js +78 -0
- package/dist/agents/plugins/claude/session/claude-named-invocations.js.map +1 -0
- package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.d.ts.map +1 -1
- package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.js +10 -1
- package/dist/agents/plugins/claude/session/processors/claude.metrics-processor.js.map +1 -1
- package/dist/agents/plugins/gemini/gemini.plugin.js.map +1 -1
- package/dist/agents/plugins/gemini/session/processors/gemini.metrics-processor.d.ts.map +1 -1
- package/dist/agents/plugins/gemini/session/processors/gemini.metrics-processor.js +3 -0
- package/dist/agents/plugins/gemini/session/processors/gemini.metrics-processor.js.map +1 -1
- package/dist/agents/plugins/kimi/extension/SKILL.md +6 -0
- package/dist/agents/plugins/kimi/extension/manifest.json +8 -0
- package/dist/agents/plugins/kimi/kimi-acp.plugin.d.ts +7 -0
- package/dist/agents/plugins/kimi/kimi-acp.plugin.d.ts.map +1 -0
- package/dist/agents/plugins/kimi/kimi-acp.plugin.js +28 -0
- package/dist/agents/plugins/kimi/kimi-acp.plugin.js.map +1 -0
- package/dist/agents/plugins/kimi/kimi.extension-installer.d.ts +56 -0
- package/dist/agents/plugins/kimi/kimi.extension-installer.d.ts.map +1 -0
- package/dist/agents/plugins/kimi/kimi.extension-installer.js +79 -0
- package/dist/agents/plugins/kimi/kimi.extension-installer.js.map +1 -0
- package/dist/agents/plugins/kimi/kimi.hook-config-injector.d.ts +34 -0
- package/dist/agents/plugins/kimi/kimi.hook-config-injector.d.ts.map +1 -0
- package/dist/agents/plugins/kimi/kimi.hook-config-injector.js +158 -0
- package/dist/agents/plugins/kimi/kimi.hook-config-injector.js.map +1 -0
- package/dist/agents/plugins/kimi/kimi.hook-transformer.d.ts +23 -0
- package/dist/agents/plugins/kimi/kimi.hook-transformer.d.ts.map +1 -0
- package/dist/agents/plugins/kimi/kimi.hook-transformer.js +46 -0
- package/dist/agents/plugins/kimi/kimi.hook-transformer.js.map +1 -0
- package/dist/agents/plugins/kimi/kimi.models.d.ts +8 -0
- package/dist/agents/plugins/kimi/kimi.models.d.ts.map +1 -0
- package/dist/agents/plugins/kimi/kimi.models.js +174 -0
- package/dist/agents/plugins/kimi/kimi.models.js.map +1 -0
- package/dist/agents/plugins/kimi/kimi.paths.d.ts +65 -0
- package/dist/agents/plugins/kimi/kimi.paths.d.ts.map +1 -0
- package/dist/agents/plugins/kimi/kimi.paths.js +95 -0
- package/dist/agents/plugins/kimi/kimi.paths.js.map +1 -0
- package/dist/agents/plugins/kimi/kimi.plugin.d.ts +37 -0
- package/dist/agents/plugins/kimi/kimi.plugin.d.ts.map +1 -0
- package/dist/agents/plugins/kimi/kimi.plugin.js +318 -0
- package/dist/agents/plugins/kimi/kimi.plugin.js.map +1 -0
- package/dist/agents/plugins/kimi/kimi.session.d.ts +40 -0
- package/dist/agents/plugins/kimi/kimi.session.d.ts.map +1 -0
- package/dist/agents/plugins/kimi/kimi.session.js +191 -0
- package/dist/agents/plugins/kimi/kimi.session.js.map +1 -0
- package/dist/agents/plugins/kimi/session/processors/kimi.metrics-processor.d.ts +43 -0
- package/dist/agents/plugins/kimi/session/processors/kimi.metrics-processor.d.ts.map +1 -0
- package/dist/agents/plugins/kimi/session/processors/kimi.metrics-processor.js +421 -0
- package/dist/agents/plugins/kimi/session/processors/kimi.metrics-processor.js.map +1 -0
- package/dist/agents/plugins/kimi/session/types.d.ts +81 -0
- package/dist/agents/plugins/kimi/session/types.d.ts.map +1 -0
- package/dist/agents/plugins/kimi/session/types.js +5 -0
- package/dist/agents/plugins/kimi/session/types.js.map +1 -0
- package/dist/agents/registry.d.ts.map +1 -1
- package/dist/agents/registry.js +4 -0
- package/dist/agents/registry.js.map +1 -1
- package/dist/cli/commands/analytics/aggregator.d.ts +14 -0
- package/dist/cli/commands/analytics/aggregator.d.ts.map +1 -1
- package/dist/cli/commands/analytics/aggregator.js +81 -1
- package/dist/cli/commands/analytics/aggregator.js.map +1 -1
- package/dist/cli/commands/analytics/cost/cost-calculator.d.ts +10 -0
- package/dist/cli/commands/analytics/cost/cost-calculator.d.ts.map +1 -1
- package/dist/cli/commands/analytics/cost/cost-calculator.js +9 -5
- package/dist/cli/commands/analytics/cost/cost-calculator.js.map +1 -1
- package/dist/cli/commands/analytics/cost/cost-enricher.d.ts +9 -1
- package/dist/cli/commands/analytics/cost/cost-enricher.d.ts.map +1 -1
- package/dist/cli/commands/analytics/cost/cost-enricher.js +141 -7
- package/dist/cli/commands/analytics/cost/cost-enricher.js.map +1 -1
- package/dist/cli/commands/analytics/cost/dispatch-extractor.d.ts +17 -0
- package/dist/cli/commands/analytics/cost/dispatch-extractor.d.ts.map +1 -0
- package/dist/cli/commands/analytics/cost/dispatch-extractor.js +78 -0
- package/dist/cli/commands/analytics/cost/dispatch-extractor.js.map +1 -0
- package/dist/cli/commands/analytics/cost/pricing.json +9 -2
- package/dist/cli/commands/analytics/cost/types.d.ts +33 -0
- package/dist/cli/commands/analytics/cost/types.d.ts.map +1 -1
- package/dist/cli/commands/analytics/cost/types.js +4 -1
- package/dist/cli/commands/analytics/cost/types.js.map +1 -1
- package/dist/cli/commands/analytics/cost/usage-readers.d.ts +22 -1
- package/dist/cli/commands/analytics/cost/usage-readers.d.ts.map +1 -1
- package/dist/cli/commands/analytics/cost/usage-readers.js +147 -21
- package/dist/cli/commands/analytics/cost/usage-readers.js.map +1 -1
- package/dist/cli/commands/analytics/model-normalizer.d.ts +2 -0
- package/dist/cli/commands/analytics/model-normalizer.d.ts.map +1 -1
- package/dist/cli/commands/analytics/model-normalizer.js +8 -0
- package/dist/cli/commands/analytics/model-normalizer.js.map +1 -1
- package/dist/cli/commands/analytics/native-loader.d.ts.map +1 -1
- package/dist/cli/commands/analytics/native-loader.js +35 -0
- package/dist/cli/commands/analytics/native-loader.js.map +1 -1
- package/dist/cli/commands/analytics/report/client/app.js +685 -53
- package/dist/cli/commands/analytics/report/payload-builder.d.ts.map +1 -1
- package/dist/cli/commands/analytics/report/payload-builder.js +13 -0
- package/dist/cli/commands/analytics/report/payload-builder.js.map +1 -1
- package/dist/cli/commands/analytics/report/template.html +108 -8
- package/dist/cli/commands/analytics/report/types.d.ts +13 -2
- package/dist/cli/commands/analytics/report/types.d.ts.map +1 -1
- package/dist/cli/commands/analytics/types.d.ts +18 -0
- package/dist/cli/commands/analytics/types.d.ts.map +1 -1
- package/dist/cli/commands/hook.d.ts.map +1 -1
- package/dist/cli/commands/hook.js +24 -7
- package/dist/cli/commands/hook.js.map +1 -1
- package/dist/migrations/004-skills-assistants-top-level.migration.d.ts.map +1 -1
- package/dist/migrations/004-skills-assistants-top-level.migration.js +8 -5
- package/dist/migrations/004-skills-assistants-top-level.migration.js.map +1 -1
- package/dist/migrations/005-skill-slug-format.migration.d.ts.map +1 -1
- package/dist/migrations/005-skill-slug-format.migration.js +14 -11
- package/dist/migrations/005-skill-slug-format.migration.js.map +1 -1
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +2 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/plugins/moonshot-subscription/index.d.ts +9 -0
- package/dist/providers/plugins/moonshot-subscription/index.d.ts.map +1 -0
- package/dist/providers/plugins/moonshot-subscription/index.js +9 -0
- package/dist/providers/plugins/moonshot-subscription/index.js.map +1 -0
- package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.setup-steps.d.ts +8 -0
- package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.setup-steps.d.ts.map +1 -0
- package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.setup-steps.js +80 -0
- package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.setup-steps.js.map +1 -0
- package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.template.d.ts +11 -0
- package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.template.d.ts.map +1 -0
- package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.template.js +111 -0
- package/dist/providers/plugins/moonshot-subscription/moonshot-subscription.template.js.map +1 -0
- package/dist/providers/plugins/sso/proxy/plugins/index.d.ts +2 -1
- package/dist/providers/plugins/sso/proxy/plugins/index.d.ts.map +1 -1
- package/dist/providers/plugins/sso/proxy/plugins/index.js +3 -1
- package/dist/providers/plugins/sso/proxy/plugins/index.js.map +1 -1
- package/dist/providers/plugins/sso/proxy/plugins/kimi-request-normalizer.plugin.d.ts +18 -0
- package/dist/providers/plugins/sso/proxy/plugins/kimi-request-normalizer.plugin.d.ts.map +1 -0
- package/dist/providers/plugins/sso/proxy/plugins/kimi-request-normalizer.plugin.js +56 -0
- package/dist/providers/plugins/sso/proxy/plugins/kimi-request-normalizer.plugin.js.map +1 -0
- package/dist/providers/plugins/sso/session/processors/conversations/constants.d.ts +1 -1
- package/dist/providers/plugins/sso/session/processors/conversations/constants.js +1 -1
- package/dist/providers/plugins/sso/sso.auth.d.ts +5 -0
- package/dist/providers/plugins/sso/sso.auth.d.ts.map +1 -1
- package/dist/providers/plugins/sso/sso.auth.js +23 -1
- package/dist/providers/plugins/sso/sso.auth.js.map +1 -1
- package/dist/utils/config.d.ts +24 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +83 -8
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/native-installer.d.ts.map +1 -1
- package/dist/utils/native-installer.js +6 -3
- package/dist/utils/native-installer.js.map +1 -1
- package/package.json +5 -1
- package/scripts/copy-plugins.js +5 -0
- 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 { '&': '&', '<': '<', '>': '>', '"': '"' }[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) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 = '';
|