Usage panel: scope to open panes, lead with tokens, label $ as API estimate
Addresses feedback on the usage panel: - It was summing every recent session on the distro (all projects, mounted + home dirs), not the open panes' work — which read as inflated/double- counted. (Verified there's no literal double count: every transcript is read once and no two project dirs share a cwd, since claude resolves symlinks/mounts to the real path before mangling.) Now the panel + the titlebar chip default to sessions whose cwd matches an open pane, with an 'open panes / all recent' toggle to see the full per-distro list. - Token volume is now the headline figure; the API-cost estimate is shown as a clearly-labeled '~$' secondary, with a footer note that it's n/a on a Pro/Max subscription and can't reflect /usage quota. Kept visible (not hidden) so it can be validated against real API billing. Frontend-only; backend still returns the full recent set for the toggle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e3c3810ba0
commit
ebbf8db407
3 changed files with 80 additions and 11 deletions
|
|
@ -804,6 +804,13 @@ export default function App() {
|
|||
[tree],
|
||||
);
|
||||
|
||||
// Titlebar chip total — scoped to the open panes ("this workspace"), matching
|
||||
// the usage panel's default view, so it isn't inflated by unrelated projects.
|
||||
const workspaceUsageTotal = useMemo(() => {
|
||||
const cwds = new Set(openPanes.map((p) => p.cwd).filter(Boolean));
|
||||
return totalCost(usageSessions.filter((s) => cwds.has(s.cwd)));
|
||||
}, [openPanes, usageSessions]);
|
||||
|
||||
// Outside-click dismissal for the titlebar dropdowns. Mirrors the
|
||||
// per-pane shell-picker pattern in LeafPane.tsx.
|
||||
useEffect(() => {
|
||||
|
|
@ -2153,7 +2160,7 @@ export default function App() {
|
|||
title="claude token usage & estimated cost (Ctrl+Shift+U)"
|
||||
aria-label="Usage"
|
||||
>
|
||||
💰{usageSessions.length > 0 ? ` ${formatUsd(totalCost(usageSessions))}` : ""}
|
||||
💰{workspaceUsageTotal > 0 ? ` ~${formatUsd(workspaceUsageTotal)}` : ""}
|
||||
</button>
|
||||
<button
|
||||
className="palette-btn"
|
||||
|
|
|
|||
|
|
@ -32,11 +32,38 @@
|
|||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
.usage-scope {
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
background: transparent;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 3px;
|
||||
color: #9aa0a6;
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
margin-right: auto;
|
||||
}
|
||||
.usage-scope:hover {
|
||||
background: #2a2a2a;
|
||||
color: #ddd;
|
||||
}
|
||||
.usage-total {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.usage-total-usd {
|
||||
color: #6cc04a;
|
||||
margin-right: auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
.usage-link {
|
||||
font: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #6ca0d8;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
padding: 0;
|
||||
}
|
||||
.usage-refresh,
|
||||
.usage-close {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { SessionUsage } from "../ipc";
|
||||
import {
|
||||
sessionCost,
|
||||
|
|
@ -32,6 +32,10 @@ export default function UsagePanel({
|
|||
onClose,
|
||||
openPanes,
|
||||
}: UsagePanelProps) {
|
||||
// Default to the open panes' sessions ("this workspace"); toggle to see
|
||||
// every recent session on the distros.
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
// Refresh on open and on a light interval while open.
|
||||
useEffect(() => {
|
||||
onRefresh();
|
||||
|
|
@ -46,8 +50,11 @@ export default function UsagePanel({
|
|||
if (p.cwd && !paneByCwd.has(p.cwd)) paneByCwd.set(p.cwd, p.label);
|
||||
}
|
||||
|
||||
const matched = sessions.filter((s) => paneByCwd.has(s.cwd));
|
||||
const shown = showAll ? sessions : matched;
|
||||
|
||||
const nowMs = Date.now();
|
||||
const total = totalCost(sessions);
|
||||
const total = totalCost(shown);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -55,8 +62,26 @@ export default function UsagePanel({
|
|||
<div className="usage-panel" role="dialog" aria-label="Token usage">
|
||||
<header className="usage-header">
|
||||
<span className="usage-title">Usage</span>
|
||||
<span className="usage-total" title="Estimated total across listed sessions">
|
||||
{formatUsd(total)}
|
||||
<button
|
||||
className="usage-scope"
|
||||
onClick={() => setShowAll((v) => !v)}
|
||||
title={
|
||||
showAll
|
||||
? "Showing every recent session on the open panes' distros"
|
||||
: "Showing only sessions for currently-open panes"
|
||||
}
|
||||
>
|
||||
{showAll ? `all recent (${sessions.length})` : `open panes (${matched.length})`}
|
||||
</button>
|
||||
<span
|
||||
className="usage-total"
|
||||
title="Total tokens across the listed sessions"
|
||||
>
|
||||
{formatTokens(shown.reduce((a, s) => a + sessionTokens(s), 0))} tok
|
||||
<span className="usage-total-usd" title="API-pricing estimate — n/a on a Pro/Max subscription">
|
||||
{" · ~"}
|
||||
{formatUsd(total)}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
className="usage-refresh"
|
||||
|
|
@ -73,15 +98,25 @@ export default function UsagePanel({
|
|||
</header>
|
||||
|
||||
<div className="usage-body">
|
||||
{sessions.length === 0 ? (
|
||||
{shown.length === 0 ? (
|
||||
<p className="usage-empty">
|
||||
{loading
|
||||
? "Reading transcripts…"
|
||||
: "No recent claude sessions found in the open panes' WSL distros."}
|
||||
: sessions.length > 0 && !showAll
|
||||
? "No open pane has a matching claude session yet."
|
||||
: "No recent claude sessions found in the open panes' WSL distros."}
|
||||
{sessions.length > 0 && !showAll && (
|
||||
<>
|
||||
{" "}
|
||||
<button className="usage-link" onClick={() => setShowAll(true)}>
|
||||
Show all recent ({sessions.length})
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="usage-list">
|
||||
{sessions.map((s) => {
|
||||
{shown.map((s) => {
|
||||
const paneLabel = paneByCwd.get(s.cwd);
|
||||
const open = paneLabel !== undefined;
|
||||
return (
|
||||
|
|
@ -121,8 +156,8 @@ export default function UsagePanel({
|
|||
</div>
|
||||
|
||||
<footer className="usage-foot">
|
||||
● = open pane · estimate (rates may drift) · recent
|
||||
sessions only
|
||||
● = open pane · ~$ is an API-pricing estimate (n/a on Pro/Max;
|
||||
can't reflect <code>/usage</code> quota) · recent sessions only
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue