Skip to content

Commit be28e2a

Browse files
committed
Release v1.2.3
2 parents eb8ab09 + b87cb7f commit be28e2a

8 files changed

Lines changed: 62 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## v1.2.3 — 2026-05-30
4+
5+
### Extension
6+
7+
- Fixed CSV export (Download CSV buttons / "Download CSV to see all" links) not working inside the VS Code webview. The dashboard iframe's sandbox was missing `allow-downloads`, so Chromium silently blocked the Blob download; added the token. Keeps the export client-side, so it still respects the current model/range/sort filters.
8+
- The extension no longer pops a system browser tab when opening the dashboard — it passes the new `--no-browser` flag to the bundled `cli.py dashboard`. Running `python cli.py dashboard` as a script still opens the browser as before.
9+
- Matched the webview's pre-load / behind-iframe background to the dashboard (`#191A1B`), removing the brief `#0f1117` flash before the dashboard renders.
10+
11+
### Scanner / CLI
12+
13+
- Added a `--no-browser` flag to `cli.py dashboard` to start the server without opening a browser (used by the VS Code extension; standalone CLI usage is unchanged).
14+
315
## v1.2.2 — 2026-05-30
416

517
### Dashboard

cli.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,7 @@ def cmd_stats():
357357
conn.close()
358358

359359

360-
def cmd_dashboard(projects_dir=None, host=None, port=None):
361-
import webbrowser
362-
import threading
363-
import time
364-
360+
def cmd_dashboard(projects_dir=None, host=None, port=None, no_browser=False):
365361
print("Running scan first...")
366362
cmd_scan(projects_dir=projects_dir)
367363

@@ -371,12 +367,19 @@ def cmd_dashboard(projects_dir=None, host=None, port=None):
371367
host = host or os.environ.get("HOST", "localhost")
372368
port = int(port or os.environ.get("PORT", "8080"))
373369

374-
def open_browser():
375-
time.sleep(1.0)
376-
webbrowser.open(f"http://{host}:{port}")
370+
# Open a browser for users running this as a script (see README). The VS Code
371+
# extension passes --no-browser since it embeds the dashboard in a webview.
372+
if not no_browser:
373+
import webbrowser
374+
import threading
375+
import time
376+
377+
def open_browser():
378+
time.sleep(1.0)
379+
webbrowser.open(f"http://{host}:{port}")
380+
381+
threading.Thread(target=open_browser, daemon=True).start()
377382

378-
t = threading.Thread(target=open_browser, daemon=True)
379-
t.start()
380383
serve(host=host, port=port)
381384

382385

@@ -390,8 +393,8 @@ def open_browser():
390393
python cli.py today Show today's usage summary
391394
python cli.py week Show last 7 days (per-day + by-model)
392395
python cli.py stats Show all-time statistics
393-
python cli.py dashboard [--projects-dir PATH] [--host HOST] [--port PORT]
394-
Scan + start dashboard
396+
python cli.py dashboard [--projects-dir PATH] [--host HOST] [--port PORT] [--no-browser]
397+
Scan + start dashboard (opens a browser unless --no-browser)
395398
"""
396399

397400
COMMANDS = {
@@ -423,6 +426,7 @@ def parse_named_arg(args, flag):
423426
projects_dir=projects_dir,
424427
host=parse_named_arg(rest, "--host"),
425428
port=parse_named_arg(rest, "--port"),
429+
no_browser="--no-browser" in rest,
426430
)
427431
elif command == "scan" and projects_dir:
428432
cmd_scan(projects_dir=projects_dir)

tests/test_cli.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
"""Tests for cli.py - pricing, formatting, and cost calculation."""
22

3+
import io
34
import unittest
5+
from contextlib import redirect_stdout
6+
from unittest import mock
7+
import cli
48
from cli import get_pricing, calc_cost, fmt, fmt_cost, PRICING
59

610

@@ -164,5 +168,18 @@ def test_haiku_pricing(self):
164168
self.assertEqual(p["output"], 5.00, f"{model} output price wrong")
165169

166170

171+
class TestDashboardNoBrowser(unittest.TestCase):
172+
"""The VS Code extension passes --no-browser; CLI users get a browser."""
173+
174+
def test_no_browser_suppresses_webbrowser(self):
175+
with mock.patch.object(cli, "cmd_scan"), \
176+
mock.patch("dashboard.serve") as mock_serve, \
177+
mock.patch("webbrowser.open") as mock_open, \
178+
redirect_stdout(io.StringIO()):
179+
cli.cmd_dashboard(host="127.0.0.1", port=9999, no_browser=True)
180+
mock_open.assert_not_called()
181+
mock_serve.assert_called_once()
182+
183+
167184
if __name__ == "__main__":
168185
unittest.main()

vscode-extension/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vscode-extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "claude-usage-phuryn",
33
"displayName": "Claude Code Usage by Paweł Huryn",
44
"description": "Embed your Claude Code usage dashboard (token counts, costs, sessions, projects) directly inside VS Code. Reads local JSONL transcripts, no API calls.",
5-
"version": "1.2.2",
5+
"version": "1.2.3",
66
"publisher": "PawelHuryn",
77
"author": {
88
"name": "Paweł Huryn",

vscode-extension/src/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ class Extension {
111111
// Probe a dashboard-specific endpoint so we don't get fooled by some
112112
// other localhost service listening on the same port.
113113
const probeUrl = `http://${host}:${port}/api/data`;
114-
const spawnArgs = dashboardSpawnArgs(mode, python, ["--host", host, "--port", String(port)]);
114+
// --no-browser: the dashboard is embedded in the webview, so the bundled
115+
// cli.py must not also pop a system browser (it does by default for CLI users).
116+
const spawnArgs = dashboardSpawnArgs(mode, python, ["--no-browser", "--host", host, "--port", String(port)]);
115117
if (!spawnArgs) {
116118
const msg = "Could not assemble a valid command to spawn the dashboard.";
117119
this.output.appendLine(msg);

vscode-extension/src/sidebar.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import { ServerManager } from "./server-manager";
1313
* We rely on VS Code's webview Content-Security-Policy. Allowing the
1414
* dashboard's localhost origin via `frame-src http://127.0.0.1:* http://localhost:*`
1515
* is enough; the dashboard ships its own CSP for what it loads inside.
16+
*
17+
* The iframe sandbox includes `allow-downloads` so the dashboard's CSV export
18+
* (a Blob + `a.download` click) works inside the webview — without it Chromium
19+
* silently blocks the download.
1620
*/
1721
export function renderHtml(url: string | null, statusText: string, nonce: string): string {
1822
if (url) {
@@ -24,12 +28,12 @@ export function renderHtml(url: string | null, statusText: string, nonce: string
2428
content="default-src 'none'; frame-src http://127.0.0.1:* http://localhost:*; style-src 'unsafe-inline'; script-src 'nonce-${nonce}';">
2529
<title>Claude Usage</title>
2630
<style>
27-
html, body { margin: 0; padding: 0; height: 100%; background: #0f1117; }
31+
html, body { margin: 0; padding: 0; height: 100%; background: #191A1B; }
2832
iframe { border: 0; width: 100%; height: 100vh; display: block; }
2933
</style>
3034
</head>
3135
<body>
32-
<iframe src="${escapeHtml(url)}" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
36+
<iframe src="${escapeHtml(url)}" sandbox="allow-scripts allow-same-origin allow-forms allow-downloads"></iframe>
3337
</body>
3438
</html>`;
3539
}
@@ -42,7 +46,7 @@ export function renderHtml(url: string | null, statusText: string, nonce: string
4246
content="default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}';">
4347
<title>Claude Usage</title>
4448
<style>
45-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e2e8f0; background: #0f1117; padding: 24px; line-height: 1.5; }
49+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #e2e8f0; background: #191A1B; padding: 24px; line-height: 1.5; }
4650
h2 { color: #d97757; font-weight: 600; font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; }
4751
pre { background: #1a1d27; border: 1px solid #2a2d3a; border-radius: 6px; padding: 12px; font-size: 12px; color: #8892a4; white-space: pre-wrap; word-break: break-word; max-width: 100%; }
4852
p { color: #8892a4; font-size: 13px; }

vscode-extension/test/sidebar.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,11 @@ describe("renderHtml with iframe URL", () => {
7777
expect(html).toContain(`script-src 'nonce-${NONCE}'`);
7878
});
7979

80-
it("sandbox attribute restricts iframe to the minimum needed", () => {
80+
it("sandbox grants only what the dashboard needs (incl. downloads for CSV export)", () => {
8181
const html = renderHtml("http://127.0.0.1:9000/", "", NONCE);
82-
expect(html).toContain("sandbox=\"allow-scripts allow-same-origin allow-forms\"");
83-
// Specifically NOT allow-popups (the dashboard doesn't open new windows).
82+
expect(html).toContain("sandbox=\"allow-scripts allow-same-origin allow-forms allow-downloads\"");
83+
// allow-downloads lets the dashboard's CSV export (a Blob + a.download click)
84+
// work inside the webview. Specifically NOT allow-popups — it doesn't open windows.
8485
expect(html).not.toContain("allow-popups");
8586
});
8687

0 commit comments

Comments
 (0)