codedash-app 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/data.js +86 -3
- package/src/frontend/app.js +48 -12
- package/src/frontend/styles.css +6 -0
- package/src/server.js +9 -1
package/package.json
CHANGED
package/src/data.js
CHANGED
|
@@ -589,6 +589,86 @@ function getSessionReplay(sessionId, project) {
|
|
|
589
589
|
};
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
// ── Pricing per model (per token, April 2026) ─────────────
|
|
593
|
+
|
|
594
|
+
const MODEL_PRICING = {
|
|
595
|
+
'claude-opus-4-6': { input: 5.00 / 1e6, output: 25.00 / 1e6, cache_read: 0.50 / 1e6, cache_create: 6.25 / 1e6 },
|
|
596
|
+
'claude-opus-4-5': { input: 5.00 / 1e6, output: 25.00 / 1e6, cache_read: 0.50 / 1e6, cache_create: 6.25 / 1e6 },
|
|
597
|
+
'claude-sonnet-4-6': { input: 3.00 / 1e6, output: 15.00 / 1e6, cache_read: 0.30 / 1e6, cache_create: 3.75 / 1e6 },
|
|
598
|
+
'claude-sonnet-4-5': { input: 3.00 / 1e6, output: 15.00 / 1e6, cache_read: 0.30 / 1e6, cache_create: 3.75 / 1e6 },
|
|
599
|
+
'claude-haiku-4-5': { input: 1.00 / 1e6, output: 5.00 / 1e6, cache_read: 0.10 / 1e6, cache_create: 1.25 / 1e6 },
|
|
600
|
+
'codex-mini-latest': { input: 1.50 / 1e6, output: 6.00 / 1e6, cache_read: 0.375 / 1e6, cache_create: 1.875 / 1e6 },
|
|
601
|
+
'gpt-5': { input: 1.25 / 1e6, output: 10.00 / 1e6, cache_read: 0.625 / 1e6, cache_create: 1.25 / 1e6 },
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
function getModelPricing(model) {
|
|
605
|
+
if (!model) return MODEL_PRICING['claude-sonnet-4-6']; // default
|
|
606
|
+
for (const key in MODEL_PRICING) {
|
|
607
|
+
if (model.includes(key) || model.startsWith(key)) return MODEL_PRICING[key];
|
|
608
|
+
}
|
|
609
|
+
// Fallback: try partial match
|
|
610
|
+
if (model.includes('opus')) return MODEL_PRICING['claude-opus-4-6'];
|
|
611
|
+
if (model.includes('haiku')) return MODEL_PRICING['claude-haiku-4-5'];
|
|
612
|
+
if (model.includes('sonnet')) return MODEL_PRICING['claude-sonnet-4-6'];
|
|
613
|
+
if (model.includes('codex')) return MODEL_PRICING['codex-mini-latest'];
|
|
614
|
+
return MODEL_PRICING['claude-sonnet-4-6'];
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ── Compute real cost from session file token usage ────────
|
|
618
|
+
|
|
619
|
+
function computeSessionCost(sessionId, project) {
|
|
620
|
+
const found = findSessionFile(sessionId, project);
|
|
621
|
+
if (!found) return { cost: 0, inputTokens: 0, outputTokens: 0, model: '' };
|
|
622
|
+
|
|
623
|
+
let totalCost = 0;
|
|
624
|
+
let totalInput = 0;
|
|
625
|
+
let totalOutput = 0;
|
|
626
|
+
let model = '';
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const lines = fs.readFileSync(found.file, 'utf8').split('\n').filter(Boolean);
|
|
630
|
+
for (const line of lines) {
|
|
631
|
+
try {
|
|
632
|
+
const entry = JSON.parse(line);
|
|
633
|
+
if (found.format === 'claude' && entry.type === 'assistant') {
|
|
634
|
+
const msg = entry.message || {};
|
|
635
|
+
if (!model && msg.model) model = msg.model;
|
|
636
|
+
const u = msg.usage;
|
|
637
|
+
if (!u) continue;
|
|
638
|
+
|
|
639
|
+
const pricing = getModelPricing(msg.model || model);
|
|
640
|
+
const inp = u.input_tokens || 0;
|
|
641
|
+
const cacheCreate = u.cache_creation_input_tokens || 0;
|
|
642
|
+
const cacheRead = u.cache_read_input_tokens || 0;
|
|
643
|
+
const out = u.output_tokens || 0;
|
|
644
|
+
|
|
645
|
+
totalInput += inp + cacheCreate + cacheRead;
|
|
646
|
+
totalOutput += out;
|
|
647
|
+
totalCost += inp * pricing.input
|
|
648
|
+
+ cacheCreate * pricing.cache_create
|
|
649
|
+
+ cacheRead * pricing.cache_read
|
|
650
|
+
+ out * pricing.output;
|
|
651
|
+
}
|
|
652
|
+
// Codex: estimate from file size (no token usage in session files)
|
|
653
|
+
} catch {}
|
|
654
|
+
}
|
|
655
|
+
} catch {}
|
|
656
|
+
|
|
657
|
+
// Fallback for Codex or sessions without usage data
|
|
658
|
+
if (totalCost === 0 && found.format === 'codex') {
|
|
659
|
+
try {
|
|
660
|
+
const size = fs.statSync(found.file).size;
|
|
661
|
+
const tokens = size / 4;
|
|
662
|
+
const pricing = MODEL_PRICING['codex-mini-latest'];
|
|
663
|
+
totalInput = Math.round(tokens * 0.3);
|
|
664
|
+
totalOutput = Math.round(tokens * 0.7);
|
|
665
|
+
totalCost = totalInput * pricing.input + totalOutput * pricing.output;
|
|
666
|
+
} catch {}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return { cost: totalCost, inputTokens: totalInput, outputTokens: totalOutput, model };
|
|
670
|
+
}
|
|
671
|
+
|
|
592
672
|
// ── Cost analytics ────────────────────────────────────────
|
|
593
673
|
|
|
594
674
|
function getCostAnalytics(sessions) {
|
|
@@ -600,9 +680,10 @@ function getCostAnalytics(sessions) {
|
|
|
600
680
|
const sessionCosts = [];
|
|
601
681
|
|
|
602
682
|
for (const s of sessions) {
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
const
|
|
683
|
+
const costData = computeSessionCost(s.id, s.project);
|
|
684
|
+
const cost = costData.cost;
|
|
685
|
+
const tokens = costData.inputTokens + costData.outputTokens;
|
|
686
|
+
if (cost === 0 && tokens === 0) continue;
|
|
606
687
|
totalCost += cost;
|
|
607
688
|
totalTokens += tokens;
|
|
608
689
|
|
|
@@ -746,6 +827,8 @@ module.exports = {
|
|
|
746
827
|
getActiveSessions,
|
|
747
828
|
getSessionReplay,
|
|
748
829
|
getCostAnalytics,
|
|
830
|
+
computeSessionCost,
|
|
831
|
+
MODEL_PRICING,
|
|
749
832
|
CLAUDE_DIR,
|
|
750
833
|
CODEX_DIR,
|
|
751
834
|
HISTORY_FILE,
|
package/src/frontend/app.js
CHANGED
|
@@ -91,9 +91,16 @@ function formatBytes(bytes) {
|
|
|
91
91
|
|
|
92
92
|
function estimateCost(fileSize) {
|
|
93
93
|
if (!fileSize) return 0;
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
return tokens * 0.
|
|
94
|
+
var tokens = fileSize / 4;
|
|
95
|
+
// Quick card badge estimate (Sonnet 4.6: $3/M in, $15/M out)
|
|
96
|
+
return tokens * 0.3 * (3.0 / 1e6) + tokens * 0.7 * (15.0 / 1e6);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function loadRealCost(sessionId, project) {
|
|
100
|
+
try {
|
|
101
|
+
var resp = await fetch('/api/cost/' + sessionId + '?project=' + encodeURIComponent(project));
|
|
102
|
+
return await resp.json();
|
|
103
|
+
} catch (e) { return null; }
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
// ── Tag system ─────────────────────────────────────────────────
|
|
@@ -726,7 +733,7 @@ function render() {
|
|
|
726
733
|
}
|
|
727
734
|
|
|
728
735
|
if (currentView === 'running') {
|
|
729
|
-
renderRunning(content);
|
|
736
|
+
renderRunning(content, sessions);
|
|
730
737
|
return;
|
|
731
738
|
}
|
|
732
739
|
|
|
@@ -1050,8 +1057,9 @@ async function openDetail(s) {
|
|
|
1050
1057
|
infoHtml += '<div class="detail-row"><span class="detail-label">Messages</span><span>' + (s.detail_messages || s.messages || 0) + '</span></div>';
|
|
1051
1058
|
infoHtml += '<div class="detail-row"><span class="detail-label">File size</span><span>' + formatBytes(s.file_size) + '</span></div>';
|
|
1052
1059
|
if (costStr) {
|
|
1053
|
-
infoHtml += '<div class="detail-row"><span class="detail-label">Est. cost</span><span class="cost-badge">' + costStr + '</span></div>';
|
|
1060
|
+
infoHtml += '<div class="detail-row"><span class="detail-label">Est. cost</span><span class="cost-badge" id="detail-cost">' + costStr + '</span></div>';
|
|
1054
1061
|
}
|
|
1062
|
+
infoHtml += '<div class="detail-row" id="detail-real-cost" style="display:none"><span class="detail-label">Real cost</span><span></span></div>';
|
|
1055
1063
|
// Tags
|
|
1056
1064
|
infoHtml += '<div class="detail-row"><span class="detail-label">Tags</span><span class="card-tags">';
|
|
1057
1065
|
sessionTags.forEach(function(t) {
|
|
@@ -1110,6 +1118,23 @@ async function openDetail(s) {
|
|
|
1110
1118
|
body.querySelector('.detail-messages').innerHTML = '<div class="empty-state">No detail file available for this session.</div>';
|
|
1111
1119
|
}
|
|
1112
1120
|
|
|
1121
|
+
// Load real cost
|
|
1122
|
+
loadRealCost(s.id, s.project || '').then(function(costData) {
|
|
1123
|
+
if (!costData || !costData.cost) return;
|
|
1124
|
+
var row = document.getElementById('detail-real-cost');
|
|
1125
|
+
if (row) {
|
|
1126
|
+
row.style.display = '';
|
|
1127
|
+
row.querySelector('span:last-child').innerHTML =
|
|
1128
|
+
'<span class="cost-badge" style="background:rgba(74,222,128,0.2);color:var(--accent-green)">$' + costData.cost.toFixed(2) + '</span>' +
|
|
1129
|
+
' <span style="font-size:11px;color:var(--text-muted)">' +
|
|
1130
|
+
formatTokens(costData.inputTokens) + ' in / ' + formatTokens(costData.outputTokens) + ' out' +
|
|
1131
|
+
(costData.model ? ' (' + costData.model + ')' : '') + '</span>';
|
|
1132
|
+
}
|
|
1133
|
+
// Update estimated badge to show it was estimated
|
|
1134
|
+
var estBadge = document.getElementById('detail-cost');
|
|
1135
|
+
if (estBadge) estBadge.style.opacity = '0.5';
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1113
1138
|
// Load git commits
|
|
1114
1139
|
if (s.project) {
|
|
1115
1140
|
var commits = await loadGitCommits(s.project, s.first_ts, s.last_ts);
|
|
@@ -1440,7 +1465,7 @@ document.addEventListener('keydown', function(e) {
|
|
|
1440
1465
|
|
|
1441
1466
|
// ── Running Sessions View ──────────────────────────────────────
|
|
1442
1467
|
|
|
1443
|
-
function renderRunning(container) {
|
|
1468
|
+
function renderRunning(container, sessions) {
|
|
1444
1469
|
var activeIds = Object.keys(activeSessions);
|
|
1445
1470
|
|
|
1446
1471
|
if (activeIds.length === 0) {
|
|
@@ -1448,8 +1473,10 @@ function renderRunning(container) {
|
|
|
1448
1473
|
return;
|
|
1449
1474
|
}
|
|
1450
1475
|
|
|
1476
|
+
// Running cards at top
|
|
1451
1477
|
var html = '<div class="running-container">';
|
|
1452
|
-
html += '<h2 class="heatmap-title">Running Sessions</h2>';
|
|
1478
|
+
html += '<h2 class="heatmap-title">Running Sessions (' + activeIds.length + ')</h2>';
|
|
1479
|
+
html += '<div class="running-grid">';
|
|
1453
1480
|
|
|
1454
1481
|
activeIds.forEach(function(sid) {
|
|
1455
1482
|
var a = activeSessions[sid];
|
|
@@ -1466,7 +1493,6 @@ function renderRunning(container) {
|
|
|
1466
1493
|
html += '<span class="running-tool">' + escHtml(a.entrypoint || a.kind || 'claude') + '</span>';
|
|
1467
1494
|
html += '</div>';
|
|
1468
1495
|
|
|
1469
|
-
// Stats row
|
|
1470
1496
|
html += '<div class="running-stats">';
|
|
1471
1497
|
html += '<div class="running-stat"><span class="running-stat-val">' + a.cpu.toFixed(1) + '%</span><span class="running-stat-label">CPU</span></div>';
|
|
1472
1498
|
html += '<div class="running-stat"><span class="running-stat-val">' + a.memoryMB + 'MB</span><span class="running-stat-label">Memory</span></div>';
|
|
@@ -1476,22 +1502,32 @@ function renderRunning(container) {
|
|
|
1476
1502
|
}
|
|
1477
1503
|
html += '</div>';
|
|
1478
1504
|
|
|
1479
|
-
// Message preview
|
|
1480
1505
|
if (s && s.first_message) {
|
|
1481
1506
|
html += '<div class="running-msg">' + escHtml(s.first_message.slice(0, 150)) + '</div>';
|
|
1482
1507
|
}
|
|
1483
1508
|
|
|
1484
|
-
// Action buttons
|
|
1485
1509
|
html += '<div class="running-actions">';
|
|
1486
|
-
html += '<button class="launch-btn" style="background:var(--accent-green);color:#000" onclick="focusSession(\'' + sid + '\')">Focus
|
|
1510
|
+
html += '<button class="launch-btn" style="background:var(--accent-green);color:#000" onclick="focusSession(\'' + sid + '\')">Focus</button>';
|
|
1487
1511
|
if (s) {
|
|
1488
1512
|
html += '<button class="launch-btn btn-secondary" onclick="var ss=allSessions.find(function(x){return x.id===\'' + sid + '\'});if(ss)openDetail(ss);">Details</button>';
|
|
1513
|
+
html += '<button class="launch-btn btn-secondary" onclick="closeDetail();openReplay(\'' + sid + '\',\'' + escHtml((s.project || '').replace(/'/g, "\\'")) + '\')">Replay</button>';
|
|
1489
1514
|
}
|
|
1490
1515
|
html += '</div>';
|
|
1491
|
-
|
|
1492
1516
|
html += '</div>';
|
|
1493
1517
|
});
|
|
1494
1518
|
|
|
1519
|
+
html += '</div>';
|
|
1520
|
+
|
|
1521
|
+
// Also show recent non-active sessions below
|
|
1522
|
+
var recentInactive = sessions.filter(function(s) { return !activeSessions[s.id]; }).slice(0, 6);
|
|
1523
|
+
if (recentInactive.length > 0) {
|
|
1524
|
+
html += '<h3 style="margin:24px 0 12px;font-size:14px;color:var(--text-secondary)">Recently Inactive</h3>';
|
|
1525
|
+
html += '<div class="grid-view">';
|
|
1526
|
+
var idx = 0;
|
|
1527
|
+
recentInactive.forEach(function(s) { html += renderCard(s, idx++); });
|
|
1528
|
+
html += '</div>';
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1495
1531
|
html += '</div>';
|
|
1496
1532
|
container.innerHTML = html;
|
|
1497
1533
|
}
|
package/src/frontend/styles.css
CHANGED
|
@@ -1586,6 +1586,12 @@ body {
|
|
|
1586
1586
|
|
|
1587
1587
|
.running-container { padding: 20px; }
|
|
1588
1588
|
|
|
1589
|
+
.running-grid {
|
|
1590
|
+
display: grid;
|
|
1591
|
+
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
1592
|
+
gap: 12px;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1589
1595
|
.running-card {
|
|
1590
1596
|
background: var(--bg-card);
|
|
1591
1597
|
border: 1px solid var(--border);
|
package/src/server.js
CHANGED
|
@@ -3,7 +3,7 @@ const http = require('http');
|
|
|
3
3
|
const https = require('https');
|
|
4
4
|
const { URL } = require('url');
|
|
5
5
|
const { exec } = require('child_process');
|
|
6
|
-
const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText, getActiveSessions, getSessionReplay, getCostAnalytics } = require('./data');
|
|
6
|
+
const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText, getActiveSessions, getSessionReplay, getCostAnalytics, computeSessionCost } = require('./data');
|
|
7
7
|
const { detectTerminals, openInTerminal, focusTerminalByPid } = require('./terminals');
|
|
8
8
|
const { getHTML } = require('./html');
|
|
9
9
|
|
|
@@ -140,6 +140,14 @@ function startServer(port, openBrowser = true) {
|
|
|
140
140
|
json(res, results);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// ── Session cost ──────────────────────
|
|
144
|
+
else if (req.method === 'GET' && pathname.startsWith('/api/cost/')) {
|
|
145
|
+
const sessionId = pathname.split('/').pop();
|
|
146
|
+
const project = parsed.searchParams.get('project') || '';
|
|
147
|
+
const data = computeSessionCost(sessionId, project);
|
|
148
|
+
json(res, data);
|
|
149
|
+
}
|
|
150
|
+
|
|
143
151
|
// ── Session replay ─────────────────────
|
|
144
152
|
else if (req.method === 'GET' && pathname.startsWith('/api/replay/')) {
|
|
145
153
|
const sessionId = pathname.split('/').pop();
|