Skip to content

Commit fb2ed9e

Browse files
phurynclaude
andcommitted
feat(dashboard): v1.4.0 — single-line grouped model filter dropdown
Replace the wrapping row of model pills with a compact multi-select dropdown. The collapsed trigger summarises the selection ("All models", "No models", "All Anthropic", "All Anthropic +N", or the first two model names plus a "+N" overflow); the panel groups models by Anthropic vs Other providers with the All/None actions moved inside it. Default selection (billable models only) and ?models= URL persistence are unchanged. Bumps scanner.VERSION and vscode-extension/package.json to 1.4.0 in lockstep with the CHANGELOG heading. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 37d03de commit fb2ed9e

4 files changed

Lines changed: 149 additions & 21 deletions

File tree

CHANGELOG.md

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

3+
## v1.4.0 — 2026-06-15
4+
5+
### Dashboard
6+
7+
- Redesigned the model filter as a single-line multi-select dropdown. The filter bar now shows a compact trigger that opens a panel grouping models by **Anthropic** vs **Other providers**, with the All / None actions moved inside the panel. This replaces the wrapping row of pills, which grew unwieldy as new model versions accumulated. The trigger summarises the selection: **All models**, **No models**, **All Anthropic** (every opus/sonnet/haiku/mythos/fable selected, the default view) — optionally **All Anthropic +N** when other providers are also on — or otherwise the first two model names plus a **+N** overflow. The default selection (billable models only) and `?models=` URL persistence are unchanged.
8+
39
## v1.3.0 — 2026-06-15
410

511
### Dashboard

dashboard.py

Lines changed: 141 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,24 @@ def get_dashboard_data(db_path=DB_PATH):
196196
#filter-bar { background: var(--card); border-bottom: 1px solid var(--border); padding: 10px 24px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
197197
.filter-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); white-space: nowrap; }
198198
.filter-sep { width: 1px; height: 22px; background: var(--border); flex-shrink: 0; }
199-
#model-checkboxes { display: flex; flex-wrap: wrap; gap: 6px; }
200-
.model-cb-label { display: flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 20px; border: 1px solid var(--border); cursor: pointer; font-size: 12px; color: var(--muted); transition: border-color 0.15s, color 0.15s, background 0.15s; user-select: none; }
201-
.model-cb-label:hover { border-color: var(--accent); color: var(--text); }
202-
.model-cb-label.checked { background: var(--selected); border-color: var(--accent); color: var(--text); }
199+
/* Model multi-select: a compact trigger in the bar that opens a grouped panel. */
200+
.model-select { position: relative; flex-shrink: 0; }
201+
.model-trigger { display: flex; align-items: center; gap: 8px; min-width: 170px; max-width: 320px; padding: 5px 10px; background: var(--card); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 12px; cursor: pointer; transition: border-color 0.15s; }
202+
.model-trigger:hover, .model-trigger.open { border-color: var(--accent); }
203+
#model-trigger-label { flex: 1; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
204+
.model-caret { color: var(--muted); font-size: 10px; flex-shrink: 0; transition: transform 0.15s; }
205+
.model-trigger.open .model-caret { transform: rotate(180deg); }
206+
.model-panel { position: absolute; top: calc(100% + 6px); left: 0; z-index: 50; min-width: 250px; max-width: 340px; max-height: 360px; overflow-y: auto; background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.35); }
207+
.model-panel[hidden] { display: none; }
208+
.model-panel-actions { display: flex; gap: 6px; padding-bottom: 8px; margin-bottom: 4px; border-bottom: 1px solid var(--border); }
209+
.model-group-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); padding: 8px 8px 4px; }
210+
.model-cb-label { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; color: var(--muted); transition: background 0.12s, color 0.12s; user-select: none; }
211+
.model-cb-label:hover { background: var(--raised); color: var(--text); }
212+
.model-cb-label.checked { color: var(--text); }
203213
.model-cb-label input { display: none; }
214+
.model-cb-box { width: 15px; height: 15px; flex-shrink: 0; border-radius: 4px; border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; color: transparent; transition: background 0.12s, border-color 0.12s; }
215+
.model-cb-label.checked .model-cb-box { background: var(--accent); border-color: var(--accent); color: #fff; }
216+
.model-cb-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
204217
.filter-btn { padding: 3px 10px; border-radius: 4px; border: 1px solid var(--border); background: transparent; color: var(--muted); font-size: 11px; cursor: pointer; white-space: nowrap; }
205218
.filter-btn:hover { border-color: var(--accent); color: var(--text); }
206219
.range-group { display: flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; flex-shrink: 0; }
@@ -287,9 +300,19 @@ def get_dashboard_data(db_path=DB_PATH):
287300
288301
<div id="filter-bar">
289302
<div class="filter-label">Models</div>
290-
<div id="model-checkboxes"></div>
291-
<button class="filter-btn" onclick="selectAllModels()">All</button>
292-
<button class="filter-btn" onclick="clearAllModels()">None</button>
303+
<div class="model-select" id="model-select">
304+
<button class="model-trigger" id="model-trigger" aria-haspopup="true" aria-expanded="false" onclick="toggleModelPanel(event)">
305+
<span id="model-trigger-label">All models</span>
306+
<span class="model-caret">&#9662;</span>
307+
</button>
308+
<div class="model-panel" id="model-panel" hidden>
309+
<div class="model-panel-actions">
310+
<button class="filter-btn" onclick="selectAllModels()">All</button>
311+
<button class="filter-btn" onclick="clearAllModels()">None</button>
312+
</div>
313+
<div id="model-checkboxes"></div>
314+
</div>
315+
</div>
293316
<div class="filter-sep"></div>
294317
<div class="filter-label">Range</div>
295318
<div class="range-group">
@@ -426,6 +449,7 @@ def get_dashboard_data(db_path=DB_PATH):
426449
// ── State ──────────────────────────────────────────────────────────────────
427450
let rawData = null;
428451
let selectedModels = new Set();
452+
let allModelsList = [];
429453
let selectedRange = '30d';
430454
let charts = {};
431455
let sessionSortCol = 'last';
@@ -707,6 +731,35 @@ def get_dashboard_data(db_path=DB_PATH):
707731
return 4;
708732
}
709733
734+
function sortedModels(models) {
735+
return [...models].sort((a, b) => {
736+
const pa = modelPriority(a), pb = modelPriority(b);
737+
return pa !== pb ? pa - pb : a.localeCompare(b);
738+
});
739+
}
740+
741+
// Compact display name for the collapsed trigger, e.g. "claude-opus-4-8" ->
742+
// "Opus 4.8", "claude-fable-5" -> "Fable 5". Non-Anthropic ids fall back to the
743+
// basename with any provider prefix and trailing date suffix stripped.
744+
function shortModelName(m) {
745+
const ml = m.toLowerCase();
746+
let family = null;
747+
if (ml.includes('fable')) family = 'Fable';
748+
else if (ml.includes('mythos')) family = 'Mythos';
749+
else if (ml.includes('opus')) family = 'Opus';
750+
else if (ml.includes('sonnet')) family = 'Sonnet';
751+
else if (ml.includes('haiku')) family = 'Haiku';
752+
if (family) {
753+
const two = m.match(/(\d+)[._-](\d+)/);
754+
if (two) return family + ' ' + two[1] + '.' + two[2];
755+
const one = m.match(/(\d+)/);
756+
return one ? family + ' ' + one[1] : family;
757+
}
758+
let base = m.split('/').pop().split(':')[0];
759+
base = base.replace(/[-_]?\d{6,}.*$/, '');
760+
return base || m;
761+
}
762+
710763
function readURLModels(allModels) {
711764
const param = new URLSearchParams(window.location.search).get('models');
712765
if (!param) {
@@ -727,25 +780,94 @@ def get_dashboard_data(db_path=DB_PATH):
727780
}
728781
729782
function buildFilterUI(allModels) {
730-
const sorted = [...allModels].sort((a, b) => {
731-
const pa = modelPriority(a), pb = modelPriority(b);
732-
return pa !== pb ? pa - pb : a.localeCompare(b);
733-
});
783+
allModelsList = [...allModels];
734784
selectedModels = readURLModels(allModels);
735-
const container = document.getElementById('model-checkboxes');
736-
container.innerHTML = sorted.map(m => {
785+
const sorted = sortedModels(allModels);
786+
const anthropic = sorted.filter(m => isBillable(m));
787+
const other = sorted.filter(m => !isBillable(m));
788+
const rowHTML = m => {
737789
const checked = selectedModels.has(m);
738-
return `<label class="model-cb-label ${checked ? 'checked' : ''}" data-model="${esc(m)}">
790+
return `<label class="model-cb-label ${checked ? 'checked' : ''}" data-model="${esc(m)}" title="${esc(m)}">
739791
<input type="checkbox" value="${esc(m)}" ${checked ? 'checked' : ''} onchange="onModelToggle(this)">
740-
${esc(m)}
792+
<span class="model-cb-box">&#10003;</span>
793+
<span class="model-cb-text">${esc(m)}</span>
741794
</label>`;
742-
}).join('');
743-
}
795+
};
796+
let html = '';
797+
// Only show a group heading when both groups are present — a single-group
798+
// list doesn't need a label.
799+
const labelled = anthropic.length && other.length;
800+
if (anthropic.length) {
801+
if (labelled) html += '<div class="model-group-label">Anthropic</div>';
802+
html += anthropic.map(rowHTML).join('');
803+
}
804+
if (other.length) {
805+
if (labelled) html += '<div class="model-group-label">Other providers</div>';
806+
html += other.map(rowHTML).join('');
807+
}
808+
document.getElementById('model-checkboxes').innerHTML = html;
809+
updateModelTriggerLabel();
810+
}
811+
812+
// Collapsed trigger text, in priority order:
813+
// "All models" — everything selected
814+
// "No models" — nothing selected
815+
// "All Anthropic" — every Anthropic model (opus/sonnet/haiku/mythos/fable)
816+
// selected and no other provider; "+N" if some others too
817+
// "Fable 5, Opus 4.7 +5" — otherwise, first two names + overflow count
818+
function updateModelTriggerLabel() {
819+
const labelEl = document.getElementById('model-trigger-label');
820+
if (!labelEl) return;
821+
const n = selectedModels.size;
822+
if (n === 0) { labelEl.textContent = 'No models'; return; }
823+
if (n === allModelsList.length) { labelEl.textContent = 'All models'; return; }
824+
const anthropic = allModelsList.filter(m => isBillable(m));
825+
const others = allModelsList.filter(m => !isBillable(m));
826+
if (anthropic.length && anthropic.every(m => selectedModels.has(m))) {
827+
// n < total (handled above), so when others exist at least one is unselected.
828+
const otherSel = others.filter(m => selectedModels.has(m)).length;
829+
labelEl.textContent = otherSel ? 'All Anthropic +' + otherSel : 'All Anthropic';
830+
return;
831+
}
832+
const chosen = sortedModels(allModelsList).filter(m => selectedModels.has(m));
833+
const shown = chosen.slice(0, 2).map(shortModelName);
834+
const extra = chosen.length - shown.length;
835+
labelEl.textContent = shown.join(', ') + (extra > 0 ? ' +' + extra : '');
836+
}
837+
838+
function toggleModelPanel(event) {
839+
if (event) event.stopPropagation();
840+
const panel = document.getElementById('model-panel');
841+
const trigger = document.getElementById('model-trigger');
842+
const open = panel.hidden;
843+
panel.hidden = !open;
844+
trigger.classList.toggle('open', open);
845+
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
846+
}
847+
848+
function closeModelPanel() {
849+
const panel = document.getElementById('model-panel');
850+
if (!panel || panel.hidden) return;
851+
panel.hidden = true;
852+
const trigger = document.getElementById('model-trigger');
853+
trigger.classList.remove('open');
854+
trigger.setAttribute('aria-expanded', 'false');
855+
}
856+
857+
// Close the panel on outside click or Escape. Clicks inside #model-select
858+
// (including the checkboxes and All/None) keep it open so multiple models can
859+
// be toggled in one pass.
860+
document.addEventListener('click', (e) => {
861+
const sel = document.getElementById('model-select');
862+
if (sel && !sel.contains(e.target)) closeModelPanel();
863+
});
864+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModelPanel(); });
744865
745866
function onModelToggle(cb) {
746867
const label = cb.closest('label');
747868
if (cb.checked) { selectedModels.add(cb.value); label.classList.add('checked'); }
748869
else { selectedModels.delete(cb.value); label.classList.remove('checked'); }
870+
updateModelTriggerLabel();
749871
updateURL();
750872
applyFilter();
751873
}
@@ -754,14 +876,14 @@ def get_dashboard_data(db_path=DB_PATH):
754876
document.querySelectorAll('#model-checkboxes input').forEach(cb => {
755877
cb.checked = true; selectedModels.add(cb.value); cb.closest('label').classList.add('checked');
756878
});
757-
updateURL(); applyFilter();
879+
updateModelTriggerLabel(); updateURL(); applyFilter();
758880
}
759881
760882
function clearAllModels() {
761883
document.querySelectorAll('#model-checkboxes input').forEach(cb => {
762884
cb.checked = false; selectedModels.delete(cb.value); cb.closest('label').classList.remove('checked');
763885
});
764-
updateURL(); applyFilter();
886+
updateModelTriggerLabel(); updateURL(); applyFilter();
765887
}
766888
767889
// ── URL persistence ────────────────────────────────────────────────────────

scanner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# runtime version has to live here as a constant. Keep this in lockstep with the
1616
# top CHANGELOG heading and vscode-extension/package.json (a parity test guards
1717
# all three; see tests/test_version.py).
18-
VERSION = "1.3.0"
18+
VERSION = "1.4.0"
1919

2020
PROJECTS_DIR = Path.home() / ".claude" / "projects"
2121
XCODE_PROJECTS_DIR = Path.home() / "Library" / "Developer" / "Xcode" / "CodingAssistant" / "ClaudeAgentConfig" / "projects"

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.3.0",
5+
"version": "1.4.0",
66
"publisher": "PawelHuryn",
77
"author": {
88
"name": "Paweł Huryn",

0 commit comments

Comments
 (0)