@beastmode-develeap/beastmode 0.1.111 → 0.1.113
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/dist/web/board.html +257 -1
- package/dist/web/build-stamp.txt +1 -1
- package/package.json +1 -1
package/dist/web/board.html
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
}
|
|
16
16
|
</script>
|
|
17
17
|
<!--BOARD_DATA-->
|
|
18
|
-
<script>window.__BUILD_STAMP__ = "20260418-
|
|
18
|
+
<script>window.__BUILD_STAMP__ = "20260418-125925-5c4a0ed";</script>
|
|
19
19
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
20
20
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
21
21
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -1632,6 +1632,91 @@ input[type="range"]::-webkit-slider-thumb {
|
|
|
1632
1632
|
white-space: nowrap;
|
|
1633
1633
|
}
|
|
1634
1634
|
|
|
1635
|
+
/* Cost badge on card */
|
|
1636
|
+
.card-badge.badge-cost {
|
|
1637
|
+
background: var(--success-subtle);
|
|
1638
|
+
color: var(--success);
|
|
1639
|
+
font-family: var(--font-mono);
|
|
1640
|
+
font-size: 10px;
|
|
1641
|
+
font-weight: 600;
|
|
1642
|
+
letter-spacing: 0.01em;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/* Detail sidebar cost section */
|
|
1646
|
+
.detail-cost-section {
|
|
1647
|
+
padding: 12px 24px;
|
|
1648
|
+
border-bottom: 1px solid var(--border);
|
|
1649
|
+
}
|
|
1650
|
+
.detail-cost-title {
|
|
1651
|
+
margin: 0 0 10px 0;
|
|
1652
|
+
font-size: 13px;
|
|
1653
|
+
font-weight: 600;
|
|
1654
|
+
display: flex;
|
|
1655
|
+
align-items: center;
|
|
1656
|
+
gap: 6px;
|
|
1657
|
+
color: var(--text);
|
|
1658
|
+
}
|
|
1659
|
+
.cost-totals {
|
|
1660
|
+
display: grid;
|
|
1661
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1662
|
+
gap: 8px;
|
|
1663
|
+
margin-bottom: 12px;
|
|
1664
|
+
}
|
|
1665
|
+
.cost-total-item {
|
|
1666
|
+
display: flex;
|
|
1667
|
+
flex-direction: column;
|
|
1668
|
+
gap: 2px;
|
|
1669
|
+
padding: 8px 10px;
|
|
1670
|
+
background: var(--bg-input);
|
|
1671
|
+
border-radius: var(--radius-xs);
|
|
1672
|
+
border: 1px solid var(--border-subtle);
|
|
1673
|
+
}
|
|
1674
|
+
.cost-total-label {
|
|
1675
|
+
font-size: 10px;
|
|
1676
|
+
font-weight: 500;
|
|
1677
|
+
color: var(--text-muted);
|
|
1678
|
+
text-transform: uppercase;
|
|
1679
|
+
letter-spacing: 0.04em;
|
|
1680
|
+
}
|
|
1681
|
+
.cost-total-value {
|
|
1682
|
+
font-size: 16px;
|
|
1683
|
+
font-weight: 600;
|
|
1684
|
+
font-family: var(--font-mono);
|
|
1685
|
+
color: var(--text);
|
|
1686
|
+
}
|
|
1687
|
+
.cost-value-usd {
|
|
1688
|
+
color: var(--success);
|
|
1689
|
+
}
|
|
1690
|
+
.cost-phase-table {
|
|
1691
|
+
width: 100%;
|
|
1692
|
+
border-collapse: collapse;
|
|
1693
|
+
font-size: 12px;
|
|
1694
|
+
}
|
|
1695
|
+
.cost-phase-table th {
|
|
1696
|
+
text-align: left;
|
|
1697
|
+
padding: 6px 8px;
|
|
1698
|
+
font-size: 10px;
|
|
1699
|
+
font-weight: 600;
|
|
1700
|
+
color: var(--text-muted);
|
|
1701
|
+
text-transform: uppercase;
|
|
1702
|
+
letter-spacing: 0.04em;
|
|
1703
|
+
border-bottom: 1px solid var(--border);
|
|
1704
|
+
}
|
|
1705
|
+
.cost-phase-table td {
|
|
1706
|
+
padding: 6px 8px;
|
|
1707
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
1708
|
+
}
|
|
1709
|
+
.cost-phase-name {
|
|
1710
|
+
font-weight: 500;
|
|
1711
|
+
color: var(--text);
|
|
1712
|
+
text-transform: capitalize;
|
|
1713
|
+
}
|
|
1714
|
+
.cost-phase-value {
|
|
1715
|
+
font-family: var(--font-mono);
|
|
1716
|
+
color: var(--text-secondary);
|
|
1717
|
+
font-size: 11px;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1635
1720
|
/* ================================================================
|
|
1636
1721
|
EXTENSIONS / ITEMS LIST
|
|
1637
1722
|
================================================================ */
|
|
@@ -2562,6 +2647,36 @@ async function api(method, path, body) {
|
|
|
2562
2647
|
return data;
|
|
2563
2648
|
}
|
|
2564
2649
|
|
|
2650
|
+
// ================================================================
|
|
2651
|
+
// Cost / Token / Duration Formatters
|
|
2652
|
+
// ================================================================
|
|
2653
|
+
|
|
2654
|
+
function formatCost(usd) {
|
|
2655
|
+
if (usd === null || usd === undefined) return null;
|
|
2656
|
+
if (usd >= 100) return '$' + Math.round(usd);
|
|
2657
|
+
if (usd >= 10) return '$' + usd.toFixed(1);
|
|
2658
|
+
if (usd >= 0.01) return '$' + usd.toFixed(2);
|
|
2659
|
+
return '<$0.01';
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
function formatTokens(n) {
|
|
2663
|
+
if (n === null || n === undefined || n === 0) return '0';
|
|
2664
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
|
|
2665
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\.0$/, '') + 'K';
|
|
2666
|
+
return String(n);
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
function formatDuration(s) {
|
|
2670
|
+
if (!s || s <= 0) return '0s';
|
|
2671
|
+
if (s < 60) return Math.round(s) + 's';
|
|
2672
|
+
const m = Math.floor(s / 60);
|
|
2673
|
+
const sec = Math.round(s % 60);
|
|
2674
|
+
if (m < 60) return sec > 0 ? m + 'm ' + sec + 's' : m + 'm';
|
|
2675
|
+
const h = Math.floor(m / 60);
|
|
2676
|
+
const rm = m % 60;
|
|
2677
|
+
return rm > 0 ? h + 'h ' + rm + 'm' : h + 'h';
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2565
2680
|
// ================================================================
|
|
2566
2681
|
// Router
|
|
2567
2682
|
// ================================================================
|
|
@@ -3561,9 +3676,25 @@ function ItemDetailSidebar({ item, onClose, onStatusChange }) {
|
|
|
3561
3676
|
// that open in a new tab when clicked.
|
|
3562
3677
|
const [attachments, setAttachments] = useState([]);
|
|
3563
3678
|
const [loadingAttachments, setLoadingAttachments] = useState(true);
|
|
3679
|
+
const [costSummary, setCostSummary] = useState(null);
|
|
3680
|
+
const [loadingCost, setLoadingCost] = useState(true);
|
|
3564
3681
|
const sidebarRef = useRef(null);
|
|
3565
3682
|
const topCommentRef = useRef(null);
|
|
3566
3683
|
|
|
3684
|
+
// Cost summary fetch — keyed on item.id, refreshed alongside the
|
|
3685
|
+
// 10-second updates/attachments poll below. The api() helper only
|
|
3686
|
+
// auto-scopes /api/board/*, so append ?board=<proj> manually.
|
|
3687
|
+
// Treat record_count=0 as "no cost data" so the sidebar stays clean.
|
|
3688
|
+
const refreshCostSummary = useCallback(() => {
|
|
3689
|
+
if (!item) return;
|
|
3690
|
+
const proj = localStorage.getItem('beastmode-selected-project') || '';
|
|
3691
|
+
const boardParam = (proj && proj !== 'all') ? '?board=' + encodeURIComponent(proj) : '';
|
|
3692
|
+
fetch('/api/items/' + item.id + '/costs/summary' + boardParam)
|
|
3693
|
+
.then(r => r.ok ? r.json() : null)
|
|
3694
|
+
.then(data => setCostSummary(data && data.record_count > 0 ? data : null))
|
|
3695
|
+
.catch(() => setCostSummary(null));
|
|
3696
|
+
}, [item && item.id]);
|
|
3697
|
+
|
|
3567
3698
|
const refreshUpdates = useCallback(() => {
|
|
3568
3699
|
if (!item) return;
|
|
3569
3700
|
api('GET', '/api/board/items/' + item.id + '/updates')
|
|
@@ -3602,9 +3733,18 @@ function ItemDetailSidebar({ item, onClose, onStatusChange }) {
|
|
|
3602
3733
|
})
|
|
3603
3734
|
.catch(() => setAttachments([]))
|
|
3604
3735
|
.finally(() => setLoadingAttachments(false));
|
|
3736
|
+
setLoadingCost(true);
|
|
3737
|
+
const proj = localStorage.getItem('beastmode-selected-project') || '';
|
|
3738
|
+
const boardParam = (proj && proj !== 'all') ? '?board=' + encodeURIComponent(proj) : '';
|
|
3739
|
+
fetch('/api/items/' + item.id + '/costs/summary' + boardParam)
|
|
3740
|
+
.then(r => r.ok ? r.json() : null)
|
|
3741
|
+
.then(data => setCostSummary(data && data.record_count > 0 ? data : null))
|
|
3742
|
+
.catch(() => setCostSummary(null))
|
|
3743
|
+
.finally(() => setLoadingCost(false));
|
|
3605
3744
|
const interval = setInterval(() => {
|
|
3606
3745
|
refreshUpdates();
|
|
3607
3746
|
refreshAttachments();
|
|
3747
|
+
refreshCostSummary();
|
|
3608
3748
|
}, 10000);
|
|
3609
3749
|
return () => clearInterval(interval);
|
|
3610
3750
|
}, [item && item.id]);
|
|
@@ -3734,6 +3874,59 @@ function ItemDetailSidebar({ item, onClose, onStatusChange }) {
|
|
|
3734
3874
|
<label>Parent Epic</label>
|
|
3735
3875
|
<div class="detail-value">${item.parent_epic ? '#' + item.parent_epic : '\u2014'}</div>
|
|
3736
3876
|
</div>
|
|
3877
|
+
${loadingCost
|
|
3878
|
+
? html`
|
|
3879
|
+
<div class="detail-cost-section" data-testid="cost-section">
|
|
3880
|
+
<h4 class="detail-cost-title">Cost</h4>
|
|
3881
|
+
<div class="loading-text">Loading costs...</div>
|
|
3882
|
+
</div>`
|
|
3883
|
+
: (costSummary && html`
|
|
3884
|
+
<div class="detail-cost-section" data-testid="cost-section">
|
|
3885
|
+
<h4 class="detail-cost-title">
|
|
3886
|
+
<span style="font-family:var(--font-mono);color:var(--success);">$</span>
|
|
3887
|
+
Cost Summary
|
|
3888
|
+
</h4>
|
|
3889
|
+
<div class="cost-totals">
|
|
3890
|
+
<div class="cost-total-item">
|
|
3891
|
+
<span class="cost-total-label">Total Cost</span>
|
|
3892
|
+
<span class="cost-total-value cost-value-usd">${formatCost(costSummary.total_cost_usd) || '$0.00'}</span>
|
|
3893
|
+
</div>
|
|
3894
|
+
<div class="cost-total-item">
|
|
3895
|
+
<span class="cost-total-label">Tokens</span>
|
|
3896
|
+
<span class="cost-total-value">${formatTokens((costSummary.total_input_tokens || 0) + (costSummary.total_output_tokens || 0))}</span>
|
|
3897
|
+
</div>
|
|
3898
|
+
<div class="cost-total-item">
|
|
3899
|
+
<span class="cost-total-label">Duration</span>
|
|
3900
|
+
<span class="cost-total-value">${formatDuration(costSummary.total_duration_seconds)}</span>
|
|
3901
|
+
</div>
|
|
3902
|
+
</div>
|
|
3903
|
+
${costSummary.phases && Object.keys(costSummary.phases).length > 0 && html`
|
|
3904
|
+
<table class="cost-phase-table">
|
|
3905
|
+
<thead>
|
|
3906
|
+
<tr>
|
|
3907
|
+
<th>Phase</th>
|
|
3908
|
+
<th>Cost</th>
|
|
3909
|
+
<th>Tokens</th>
|
|
3910
|
+
<th>Duration</th>
|
|
3911
|
+
</tr>
|
|
3912
|
+
</thead>
|
|
3913
|
+
<tbody>
|
|
3914
|
+
${Object.values(costSummary.phases)
|
|
3915
|
+
.slice()
|
|
3916
|
+
.sort((a, b) => (b.cost_usd || 0) - (a.cost_usd || 0))
|
|
3917
|
+
.map((phase) => html`
|
|
3918
|
+
<tr key=${phase.phase}>
|
|
3919
|
+
<td class="cost-phase-name">${phase.phase}</td>
|
|
3920
|
+
<td class="cost-phase-value">${formatCost(phase.cost_usd) || '$0.00'}</td>
|
|
3921
|
+
<td class="cost-phase-value">${formatTokens((phase.input_tokens || 0) + (phase.output_tokens || 0))}</td>
|
|
3922
|
+
<td class="cost-phase-value">${formatDuration(phase.duration_seconds)}</td>
|
|
3923
|
+
</tr>
|
|
3924
|
+
`)}
|
|
3925
|
+
</tbody>
|
|
3926
|
+
</table>
|
|
3927
|
+
`}
|
|
3928
|
+
</div>
|
|
3929
|
+
`)}
|
|
3737
3930
|
${(loadingAttachments || attachments.length > 0) && html`
|
|
3738
3931
|
<div style="padding:12px 24px 0;">
|
|
3739
3932
|
<h4 style="margin:0 0 8px 0;font-size:13px;font-weight:600;">
|
|
@@ -3919,6 +4112,7 @@ function PipelineView({
|
|
|
3919
4112
|
columnSorts,
|
|
3920
4113
|
cycleSort,
|
|
3921
4114
|
sortColumnItems,
|
|
4115
|
+
costsByItem,
|
|
3922
4116
|
}) {
|
|
3923
4117
|
const [collapseKey, setCollapseKey] = useState(0);
|
|
3924
4118
|
const [, setEpicCollapseKey] = useState(0);
|
|
@@ -3943,6 +4137,11 @@ function PipelineView({
|
|
|
3943
4137
|
${item.priority && html`<span class=${'card-badge ' + priorityBadgeClass(item.priority)}>${item.priority}</span>`}
|
|
3944
4138
|
${item.task_type && html`<span class="card-badge badge-type">${item.task_type}</span>`}
|
|
3945
4139
|
${selectedProject === 'all' && item.project_id && html`<span class="card-badge badge-project">${item.project_id}</span>`}
|
|
4140
|
+
${(() => {
|
|
4141
|
+
const cost = costsByItem && costsByItem[String(item.id)];
|
|
4142
|
+
const label = cost ? formatCost(cost.total_cost_usd) : null;
|
|
4143
|
+
return label ? html`<span class="card-badge badge-cost" title=${'Total cost: ' + label}>${label}</span>` : null;
|
|
4144
|
+
})()}
|
|
3946
4145
|
<span class="card-time">${timeAgo(item.updated_at || item.created_at)}</span>
|
|
3947
4146
|
</div>
|
|
3948
4147
|
</div>
|
|
@@ -4057,6 +4256,7 @@ function BoardPage({ selectedProject }) {
|
|
|
4057
4256
|
const [filters, setFilters] = useState({ priority: '', taskType: '', project: '', dateRange: '', parentEpic: '' });
|
|
4058
4257
|
const [columnSorts, setColumnSorts] = useState({});
|
|
4059
4258
|
const [epicCollapseKey, setEpicCollapseKey] = useState(0);
|
|
4259
|
+
const [costsByItem, setCostsByItem] = useState({});
|
|
4060
4260
|
const viewMode = (() => {
|
|
4061
4261
|
try {
|
|
4062
4262
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -4073,20 +4273,70 @@ function BoardPage({ selectedProject }) {
|
|
|
4073
4273
|
.finally(() => setLoading(false));
|
|
4074
4274
|
}, [selectedProject]);
|
|
4075
4275
|
|
|
4276
|
+
// Cost endpoints are /api/costs/* and /api/items/*/costs/summary —
|
|
4277
|
+
// the api() helper only auto-scopes /api/board/*, so we manually
|
|
4278
|
+
// append ?board=<proj> here. Errors are swallowed: if the cost
|
|
4279
|
+
// batch endpoint is unreachable, cards render without badges.
|
|
4280
|
+
const fetchCosts = useCallback(() => {
|
|
4281
|
+
const proj = localStorage.getItem('beastmode-selected-project') || '';
|
|
4282
|
+
const boardParam = (proj && proj !== 'all') ? '?board=' + encodeURIComponent(proj) : '';
|
|
4283
|
+
fetch('/api/costs/by-items' + boardParam)
|
|
4284
|
+
.then(r => r.ok ? r.json() : {})
|
|
4285
|
+
.then(data => setCostsByItem(data || {}))
|
|
4286
|
+
.catch(() => {});
|
|
4287
|
+
}, [selectedProject]);
|
|
4288
|
+
|
|
4076
4289
|
// Re-fetch whenever the selected project changes — each project
|
|
4077
4290
|
// has its own SQLite board on the server.
|
|
4078
4291
|
// Also poll every 10s for live updates from the daemon.
|
|
4079
4292
|
useEffect(() => {
|
|
4080
4293
|
fetchItems();
|
|
4294
|
+
fetchCosts();
|
|
4081
4295
|
const interval = setInterval(() => {
|
|
4082
4296
|
// Silent refresh — don't show loading spinner on polls
|
|
4083
4297
|
api('GET', '/api/board/items')
|
|
4084
4298
|
.then(data => setItems(data.items || []))
|
|
4085
4299
|
.catch(() => {});
|
|
4300
|
+
fetchCosts();
|
|
4086
4301
|
}, 10000);
|
|
4087
4302
|
return () => clearInterval(interval);
|
|
4088
4303
|
}, [selectedProject]);
|
|
4089
4304
|
|
|
4305
|
+
// WebSocket listener for real-time cost updates (T4).
|
|
4306
|
+
// On any `cost_recorded` or `costs_cleared` event, re-fetch the
|
|
4307
|
+
// full batch — the endpoint is a single GROUP BY query and is
|
|
4308
|
+
// cheap. Auto-reconnect after 5s on disconnect.
|
|
4309
|
+
useEffect(() => {
|
|
4310
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
4311
|
+
const wsUrl = proto + '//' + location.host + '/ws';
|
|
4312
|
+
let ws;
|
|
4313
|
+
let reconnectTimer;
|
|
4314
|
+
|
|
4315
|
+
function connect() {
|
|
4316
|
+
ws = new WebSocket(wsUrl);
|
|
4317
|
+
ws.onmessage = (event) => {
|
|
4318
|
+
try {
|
|
4319
|
+
const msg = JSON.parse(event.data);
|
|
4320
|
+
if (msg.type === 'cost_recorded' || msg.type === 'costs_cleared') {
|
|
4321
|
+
fetchCosts();
|
|
4322
|
+
}
|
|
4323
|
+
} catch {}
|
|
4324
|
+
};
|
|
4325
|
+
ws.onclose = () => {
|
|
4326
|
+
reconnectTimer = setTimeout(connect, 5000);
|
|
4327
|
+
};
|
|
4328
|
+
ws.onerror = () => {
|
|
4329
|
+
ws.close();
|
|
4330
|
+
};
|
|
4331
|
+
}
|
|
4332
|
+
connect();
|
|
4333
|
+
|
|
4334
|
+
return () => {
|
|
4335
|
+
clearTimeout(reconnectTimer);
|
|
4336
|
+
if (ws) ws.close();
|
|
4337
|
+
};
|
|
4338
|
+
}, []);
|
|
4339
|
+
|
|
4090
4340
|
// Sync scroll thumb position with kanban scroll + drag support
|
|
4091
4341
|
useEffect(() => {
|
|
4092
4342
|
const kanban = document.querySelector('.kanban');
|
|
@@ -4420,6 +4670,7 @@ function BoardPage({ selectedProject }) {
|
|
|
4420
4670
|
columnSorts=${columnSorts}
|
|
4421
4671
|
cycleSort=${cycleSort}
|
|
4422
4672
|
sortColumnItems=${sortColumnItems}
|
|
4673
|
+
costsByItem=${costsByItem}
|
|
4423
4674
|
/>`
|
|
4424
4675
|
: html`
|
|
4425
4676
|
<div class="kanban-wrapper">
|
|
@@ -4485,6 +4736,11 @@ function BoardPage({ selectedProject }) {
|
|
|
4485
4736
|
${item.priority && html`<span class=${'card-badge ' + priorityBadgeClass(item.priority)}>${item.priority}</span>`}
|
|
4486
4737
|
${item.task_type && html`<span class="card-badge badge-type">${item.task_type}</span>`}
|
|
4487
4738
|
${selectedProject === 'all' && item.project_id && html`<span class="card-badge badge-project">${item.project_id}</span>`}
|
|
4739
|
+
${(() => {
|
|
4740
|
+
const cost = costsByItem && costsByItem[String(item.id)];
|
|
4741
|
+
const label = cost ? formatCost(cost.total_cost_usd) : null;
|
|
4742
|
+
return label ? html`<span class="card-badge badge-cost" title=${'Total cost: ' + label}>${label}</span>` : null;
|
|
4743
|
+
})()}
|
|
4488
4744
|
<span class="card-time">${timeAgo(item.updated_at || item.created_at)}</span>
|
|
4489
4745
|
</div>
|
|
4490
4746
|
</div>
|
package/dist/web/build-stamp.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
20260418-
|
|
1
|
+
20260418-125925-5c4a0ed
|