wendkeep 0.71.1 → 0.72.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.
@@ -96,11 +96,82 @@ export async function loadProjectMemory(fetchImpl = globalThis.fetch, projectId
96
96
  return { tree, sync };
97
97
  }
98
98
 
99
+ export function usageQuery(filters = {}) {
100
+ const params = new URLSearchParams();
101
+ const keys = ['from', 'to', 'agent_id', 'subagent_id', 'provider', 'model_provider', 'model', 'change', 'session_id', 'role'];
102
+ for (const key of keys) {
103
+ const value = filters[key] ?? filters[key.replace('_id', 'Id')] ?? '';
104
+ if (String(value).trim()) params.set(key, String(value));
105
+ }
106
+ const query = params.toString();
107
+ return query ? `?${query}` : '';
108
+ }
109
+
110
+ export async function loadProjectUsage(fetchImpl = globalThis.fetch, projectId = '', filters = {}) {
111
+ const id = encodeURIComponent(projectId);
112
+ const query = usageQuery(filters);
113
+ const [summary, breakdown, calls] = await Promise.all([
114
+ requestJson(fetchImpl, `/v1/projects/${id}/usage/summary${query}`),
115
+ requestJson(fetchImpl, `/v1/projects/${id}/usage/breakdown${query}`),
116
+ requestJson(fetchImpl, `/v1/projects/${id}/usage/calls${query}`),
117
+ ]);
118
+ return { summary, breakdown, calls, filters: { ...filters } };
119
+ }
120
+
121
+ export function buildUsageViewModel(usage = {}) {
122
+ const summary = usage.summary || {};
123
+ const agents = (Array.isArray(usage.breakdown?.agents) ? usage.breakdown.agents : []).map((agent) => ({ ...agent, children: [] }));
124
+ const byId = new Map(agents.map((agent) => [agent.agent_id, agent]));
125
+ for (const agent of agents) {
126
+ const parent = agent.parent_agent_id ? byId.get(agent.parent_agent_id) : null;
127
+ if (parent && parent !== agent) parent.children.push(agent);
128
+ }
129
+ const roots = agents.filter((agent) => !agent.parent_agent_id || !byId.has(agent.parent_agent_id));
130
+ const tokens = {
131
+ input: Number(summary.tokens?.input || 0),
132
+ cacheWrite: Number(summary.tokens?.cache_write || 0),
133
+ cacheRead: Number(summary.tokens?.cache_read || 0),
134
+ output: Number(summary.tokens?.output || 0),
135
+ reasoning: Number(summary.tokens?.reasoning || 0),
136
+ total: Number(summary.tokens?.total || 0),
137
+ };
138
+ const coverage = summary.coverage || {};
139
+ const complete = Number(coverage.complete || 0);
140
+ const summaryOnly = Number(coverage.summary_only || 0);
141
+ return {
142
+ totalCost: Number(summary.cost_usd || 0),
143
+ mainCost: Number(summary.main_cost_usd || 0),
144
+ subagentCost: Number(summary.subagent_cost_usd || 0),
145
+ wastedCost: Number(summary.wasted_usd || 0),
146
+ calls: Array.isArray(usage.calls?.calls) ? usage.calls.calls : [],
147
+ callCount: Number(summary.calls || usage.calls?.total || 0),
148
+ sessions: Number(summary.sessions || 0),
149
+ agentsCount: Number(summary.agents || 0),
150
+ subagentsCount: Number(summary.subagents || 0),
151
+ modelsCount: Number(summary.models || 0),
152
+ tokens,
153
+ trend: Array.isArray(summary.by_day) ? summary.by_day : [],
154
+ agents: roots,
155
+ allAgents: agents,
156
+ coverage: {
157
+ complete,
158
+ summaryOnly,
159
+ total: Number(coverage.transcripts || complete + summaryOnly),
160
+ label: `${complete} completo${complete === 1 ? '' : 's'} · ${summaryOnly} agregado${summaryOnly === 1 ? '' : 's'}`,
161
+ },
162
+ hasUnknownPricing: Number(summary.unknown_priced_rollups || 0) > 0,
163
+ };
164
+ }
165
+
99
166
  export async function loadMemoryDocument(fetchImpl = globalThis.fetch, projectId = '', logicalPath = '') {
100
167
  const query = new URLSearchParams({ path: logicalPath });
101
168
  return requestJson(fetchImpl, '/v1/projects/' + encodeURIComponent(projectId) + '/memory/document?' + query.toString());
102
169
  }
103
170
 
171
+ export async function loadProjectTranscript(fetchImpl = globalThis.fetch, projectId = '', transcriptId = '') {
172
+ return requestJson(fetchImpl, `/v1/projects/${encodeURIComponent(projectId)}/transcripts/${encodeURIComponent(transcriptId)}`);
173
+ }
174
+
104
175
  export async function searchProjectMemory(fetchImpl = globalThis.fetch, projectId = '', query = '') {
105
176
  const params = new URLSearchParams({ q: query });
106
177
  return requestJson(fetchImpl, '/v1/projects/' + encodeURIComponent(projectId) + '/memory/search?' + params.toString());
@@ -401,6 +472,165 @@ function renderSync(container, sync, projectId = '') {
401
472
  container.replaceChildren(heading, facts, note, exportLink);
402
473
  }
403
474
 
475
+ function formatNumber(value) {
476
+ return new Intl.NumberFormat('pt-BR').format(Number(value) || 0);
477
+ }
478
+
479
+ function formatUsd(value) {
480
+ return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 }).format(Number(value) || 0);
481
+ }
482
+
483
+ function usageFilter(label, name, value, type = 'text') {
484
+ const wrapper = node('label', 'usage-filter');
485
+ wrapper.append(node('span', '', label));
486
+ const input = node('input');
487
+ input.name = name;
488
+ input.type = type;
489
+ input.value = String(value || '');
490
+ wrapper.append(input);
491
+ return wrapper;
492
+ }
493
+
494
+ function usageSelect(label, name, value, options) {
495
+ const wrapper = node('label', 'usage-filter');
496
+ wrapper.append(node('span', '', label));
497
+ const select = node('select');
498
+ select.name = name;
499
+ for (const [optionValue, optionLabel] of options) {
500
+ const option = node('option', '', optionLabel);
501
+ option.value = optionValue;
502
+ option.selected = optionValue === String(value || '');
503
+ select.append(option);
504
+ }
505
+ wrapper.append(select);
506
+ return wrapper;
507
+ }
508
+
509
+ function renderUsage(container, projectId, usage, { onFiltersChanged, onTranscript } = {}) {
510
+ const view = buildUsageViewModel(usage);
511
+ const filters = usage?.filters || {};
512
+ const heading = node('div', 'workspace-section-heading');
513
+ heading.append(node('p', 'eyebrow', 'PROJECT CONSUMPTION'), node('h2', '', 'Consumo'));
514
+
515
+ const filterForm = node('form', 'usage-filters');
516
+ filterForm.append(
517
+ usageFilter('De', 'from', filters.from, 'date'),
518
+ usageFilter('Até', 'to', filters.to, 'date'),
519
+ usageSelect('Escopo', 'role', filters.role, [['', 'Principal + subagentes'], ['main', 'Principal'], ['subagent', 'Subagentes']]),
520
+ usageFilter('Agente', 'agent_id', filters.agent_id || filters.agentId),
521
+ usageFilter('Subagente', 'subagent_id', filters.subagent_id || filters.subagentId),
522
+ usageFilter('Provedor', 'provider', filters.provider),
523
+ usageFilter('Provedor do modelo', 'model_provider', filters.model_provider || filters.modelProvider),
524
+ usageFilter('Modelo', 'model', filters.model),
525
+ usageFilter('Change', 'change', filters.change || filters.changeSlug),
526
+ usageFilter('Sessão', 'session_id', filters.session_id || filters.sessionId),
527
+ node('button', 'button-primary', 'Aplicar filtros'),
528
+ );
529
+ filterForm.addEventListener('submit', (event) => {
530
+ event.preventDefault();
531
+ const form = new FormData(filterForm);
532
+ onFiltersChanged?.({
533
+ from: form.get('from') || '', to: form.get('to') || '', role: form.get('role') || '',
534
+ agent_id: form.get('agent_id') || '', subagent_id: form.get('subagent_id') || '',
535
+ provider: form.get('provider') || '', model_provider: form.get('model_provider') || '', model: form.get('model') || '',
536
+ change: form.get('change') || '', session_id: form.get('session_id') || '',
537
+ });
538
+ });
539
+
540
+ const cards = node('div', 'usage-summary-grid');
541
+ const summaryCards = [
542
+ ['Custo total', formatUsd(view.totalCost), `${formatUsd(view.mainCost)} principal · ${formatUsd(view.subagentCost)} subagentes`],
543
+ ['Tokens', formatNumber(view.tokens.total), `entrada ${formatNumber(view.tokens.input)} · saída ${formatNumber(view.tokens.output)}`],
544
+ ['Chamadas', formatNumber(view.callCount), `${formatNumber(view.sessions)} sessões · ${formatNumber(view.modelsCount)} modelos`],
545
+ ['Cobertura', view.coverage.label, `${formatNumber(view.coverage.total)} transcript(s)`],
546
+ ];
547
+ for (const [label, value, caption] of summaryCards) {
548
+ const card = node('article', 'usage-summary-card');
549
+ card.append(node('span', 'metric-label', label), node('strong', 'usage-summary-value', value), node('span', 'metric-caption', caption));
550
+ cards.append(card);
551
+ }
552
+
553
+ const content = [];
554
+ if (view.hasUnknownPricing) content.push(node('div', 'usage-warning', 'Há modelos sem tarifa conhecida. Os custos históricos registrados não foram recalculados.'));
555
+ if (view.wastedCost > 0) content.push(node('div', 'usage-callout', `Desperdício registrado em workflows interrompidos: ${formatUsd(view.wastedCost)}.`));
556
+
557
+ const tokenPanel = node('section', 'usage-panel');
558
+ tokenPanel.append(node('h3', '', 'Tokens por categoria'));
559
+ const tokenGrid = node('div', 'usage-token-grid');
560
+ for (const [label, value] of [['Entrada', view.tokens.input], ['Cache write', view.tokens.cacheWrite], ['Cache read', view.tokens.cacheRead], ['Saída', view.tokens.output], ['Reasoning', view.tokens.reasoning], ['Total', view.tokens.total]]) {
561
+ tokenGrid.append(fact(label, formatNumber(value)));
562
+ }
563
+ tokenPanel.append(tokenGrid);
564
+ content.push(tokenPanel);
565
+
566
+ const trendPanel = node('section', 'usage-panel');
567
+ trendPanel.append(node('h3', '', 'Tendência diária'));
568
+ const trend = node('div', 'usage-trend');
569
+ if (!view.trend.length) trend.append(node('p', 'muted', 'Sem atividade no período selecionado.'));
570
+ else for (const day of view.trend) {
571
+ const row = node('div', 'usage-trend-row');
572
+ row.append(node('span', '', day.date), node('span', '', `${formatNumber(day.tokens_total)} tokens`), node('strong', '', formatUsd(day.cost_usd)));
573
+ trend.append(row);
574
+ }
575
+ trendPanel.append(trend);
576
+ content.push(trendPanel);
577
+
578
+ const hierarchy = node('section', 'usage-panel usage-hierarchy');
579
+ hierarchy.append(node('h3', '', 'Agentes, modelos e subagentes'));
580
+ if (!view.agents.length) hierarchy.append(node('p', 'muted', 'Nenhum agente com consumo no período.'));
581
+ const renderAgent = (agent) => {
582
+ const details = node('details', 'usage-agent');
583
+ const summary = node('summary');
584
+ summary.append(node('strong', '', agent.agent_name || agent.agent_id), node('span', 'usage-agent-meta', `${agent.role} · ${formatNumber(agent.tokens_total)} tokens · ${formatUsd(agent.cost_usd)}`));
585
+ details.append(summary);
586
+ const models = node('div', 'usage-model-list');
587
+ for (const model of agent.models || []) {
588
+ const row = node('div', 'usage-model-row');
589
+ row.append(node('span', '', `${model.model_provider || '—'} · ${model.model || 'modelo sem nome'}`), node('span', '', `${formatNumber(model.tokens_total)} tokens`), node('strong', '', formatUsd(model.cost_usd)));
590
+ models.append(row);
591
+ }
592
+ details.append(models);
593
+ for (const child of agent.children || []) details.append(renderAgent(child));
594
+ return details;
595
+ };
596
+ for (const agent of view.agents) hierarchy.append(renderAgent(agent));
597
+ content.push(hierarchy);
598
+
599
+ const callsPanel = node('section', 'usage-panel usage-calls');
600
+ callsPanel.append(node('h3', '', 'Chamadas e transcripts'));
601
+ if (!view.calls.length) callsPanel.append(node('p', 'muted', 'Nenhuma chamada individual disponível; históricos agregados aparecem acima.'));
602
+ for (const call of view.calls) {
603
+ const details = node('details', 'usage-call');
604
+ const summary = node('summary');
605
+ summary.append(node('strong', '', call.model || 'modelo sem nome'), node('span', 'usage-agent-meta', `${call.role || 'principal'} · ${formatNumber(call.tokens?.total)} tokens · ${formatUsd(call.cost_usd)}`));
606
+ details.append(summary);
607
+ const copy = node('div', 'usage-call-copy');
608
+ copy.append(node('p', '', `Sessão: ${call.session_id || '—'} · Agente: ${call.agent_id || '—'}`));
609
+ copy.append(node('h4', '', 'Prompt'), node('pre', '', call.prompt || '—'), node('h4', '', 'Resposta'), node('pre', '', call.response || '—'));
610
+ if (call.transcript_id) {
611
+ const transcriptButton = node('button', 'reader-toggle', 'Abrir transcript completo');
612
+ const transcriptTarget = node('pre', 'usage-transcript', 'Carregando transcript…');
613
+ transcriptTarget.hidden = true;
614
+ transcriptButton.addEventListener('click', async () => {
615
+ transcriptTarget.hidden = false;
616
+ if (transcriptTarget.dataset.loaded) return;
617
+ try {
618
+ const transcript = await onTranscript?.(call.transcript_id);
619
+ transcriptTarget.textContent = transcript?.content || 'Transcript vazio.';
620
+ transcriptTarget.dataset.loaded = 'true';
621
+ } catch (error) {
622
+ transcriptTarget.textContent = error.message || 'Transcript indisponível.';
623
+ }
624
+ });
625
+ copy.append(transcriptButton, transcriptTarget);
626
+ }
627
+ details.append(copy);
628
+ callsPanel.append(details);
629
+ }
630
+ content.push(callsPanel);
631
+ container.replaceChildren(heading, filterForm, cards, ...content);
632
+ }
633
+
404
634
  function renderReader(container, document) {
405
635
  const heading = node('div', 'reader-heading');
406
636
  heading.append(node('p', 'eyebrow', document.category), node('h2', '', document.title));
@@ -438,6 +668,8 @@ function startDashboardV2() {
438
668
  selectedId: '',
439
669
  filter: '',
440
670
  memory: new Map(),
671
+ usage: new Map(),
672
+ usageFilters: new Map(),
441
673
  route: parseObserverRoute(globalThis.location?.hash || ''),
442
674
  };
443
675
  const dashboardError = byId('dashboard-error');
@@ -497,6 +729,12 @@ function startDashboardV2() {
497
729
  if (!state.memory.has(projectId)) state.memory.set(projectId, await loadProjectMemory(fetchJson, projectId));
498
730
  return state.memory.get(projectId);
499
731
  };
732
+ const ensureProjectUsage = async (projectId) => {
733
+ const filters = state.usageFilters.get(projectId) || {};
734
+ const usage = await loadProjectUsage(fetchJson, projectId, filters);
735
+ state.usage.set(projectId, usage);
736
+ return usage;
737
+ };
500
738
  const renderSearchRoute = async (query) => {
501
739
  const model = state.models.find((item) => item.projectId === state.selectedId) || state.models[0];
502
740
  setHidden(dashboardPanel, true);
@@ -542,7 +780,16 @@ function startDashboardV2() {
542
780
  renderReader(workspaceContent, buildMemoryDocumentViewModel(payload, payload.content));
543
781
  return;
544
782
  }
545
- if (route.section === 'sessions') renderSessions(workspaceContent, model.projectId, documents);
783
+ if (route.section === 'usage') {
784
+ const usage = await ensureProjectUsage(model.projectId);
785
+ renderUsage(workspaceContent, model.projectId, usage, {
786
+ onFiltersChanged: (filters) => {
787
+ state.usageFilters.set(model.projectId, filters);
788
+ renderWorkspaceRoute({ ...route });
789
+ },
790
+ onTranscript: (transcriptId) => loadProjectTranscript(fetchJson, model.projectId, transcriptId),
791
+ });
792
+ } else if (route.section === 'sessions') renderSessions(workspaceContent, model.projectId, documents);
546
793
  else if (route.section === 'memory') renderMemory(workspaceContent, model.projectId, documents);
547
794
  else if (route.section === 'changes') renderChanges(workspaceContent, model.projectId, documents);
548
795
  else if (route.section === 'sync') renderSync(workspaceContent, memory.sync, model.projectId);
@@ -96,6 +96,7 @@
96
96
  <aside class="workspace-rail panel" aria-label="Navegação do projeto">
97
97
  <nav id="workspace-nav" class="workspace-nav">
98
98
  <a data-workspace-section="overview" href="#project/project-a/overview">Overview</a>
99
+ <a data-workspace-section="usage" href="#project/project-a/usage">Consumo</a>
99
100
  <a data-workspace-section="sessions" href="#project/project-a/sessions">Sessões</a>
100
101
  <a data-workspace-section="memory" href="#project/project-a/memory">Memória</a>
101
102
  <a data-workspace-section="changes" href="#project/project-a/changes">Changes</a>
@@ -183,6 +183,33 @@ input::placeholder { color: var(--faint); }
183
183
  .markdown-gap { height: .35em; }
184
184
  .sync-callout, .workspace-error { margin-top: 24px; padding: 17px; border: 1px solid rgba(156, 224, 198, .2); border-radius: var(--radius-sm); color: var(--muted); background: rgba(156, 224, 198, .05); font-size: .8rem; line-height: 1.5; }
185
185
  .workspace-error { border-color: rgba(245, 139, 131, .28); color: var(--red); background: rgba(245, 139, 131, .07); }
186
+ .usage-filters { display: flex; flex-wrap: wrap; align-items: end; gap: 9px; margin: 20px 0; padding: 13px; border: 1px solid var(--line); border-radius: var(--radius-sm); background: rgba(7, 16, 21, .28); }
187
+ .usage-filter { display: grid; gap: 5px; min-width: 112px; color: var(--faint); font-size: .61rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
188
+ .usage-filter input, .usage-filter select { min-width: 0; padding: 8px 9px; border: 1px solid var(--line); border-radius: 6px; color: var(--text); background: rgba(7, 16, 21, .65); font-size: .73rem; text-transform: none; }
189
+ .button-primary { padding: 9px 12px; border: 1px solid rgba(156, 224, 198, .3); border-radius: 7px; color: var(--ink-950); background: var(--mint); font-size: .7rem; font-weight: 700; }
190
+ .button-primary:hover { background: var(--mint-bright); }
191
+ .usage-summary-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 9px; margin-bottom: 14px; }
192
+ .usage-summary-card { min-width: 0; padding: 15px; border: 1px solid var(--line); border-radius: var(--radius-md); background: rgba(20, 43, 50, .45); }
193
+ .usage-summary-value { display: block; overflow: hidden; margin: 10px 0 5px; color: var(--mint-bright); font-family: Georgia, serif; font-size: clamp(1.15rem, 2vw, 1.75rem); font-weight: 400; text-overflow: ellipsis; white-space: nowrap; }
194
+ .usage-warning { margin-bottom: 12px; padding: 13px 15px; border: 1px solid rgba(246, 189, 114, .3); border-radius: var(--radius-sm); color: var(--amber); background: rgba(246, 189, 114, .07); font-size: .76rem; line-height: 1.45; }
195
+ .usage-panel { margin-top: 14px; padding: 17px; border: 1px solid var(--line); border-radius: var(--radius-md); background: rgba(7, 16, 21, .25); }
196
+ .usage-panel h3 { margin: 0 0 13px; color: var(--mint-bright); font-size: .82rem; letter-spacing: .06em; text-transform: uppercase; }
197
+ .usage-token-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 7px; }
198
+ .usage-token-grid .fact { min-width: 0; padding: 10px; }
199
+ .usage-trend { display: grid; gap: 6px; }
200
+ .usage-trend-row, .usage-model-row { display: grid; grid-template-columns: minmax(100px, 1fr) auto auto; gap: 12px; align-items: center; padding: 9px 11px; border-radius: 7px; color: var(--muted); background: rgba(20, 43, 50, .38); font-size: .72rem; }
201
+ .usage-trend-row strong, .usage-model-row strong { color: var(--mint); font-weight: 600; }
202
+ .usage-agent { border-top: 1px solid var(--line); }
203
+ .usage-agent:first-of-type { border-top: 0; }
204
+ .usage-agent summary { display: flex; justify-content: space-between; gap: 12px; padding: 12px 2px; cursor: pointer; color: var(--text); font-size: .78rem; }
205
+ .usage-agent-meta { color: var(--muted); font-size: .68rem; font-weight: 400; }
206
+ .usage-model-list { display: grid; gap: 6px; padding: 0 0 12px 17px; }
207
+ .usage-call { border-top: 1px solid var(--line); }
208
+ .usage-call:first-of-type { border-top: 0; }
209
+ .usage-call summary { display: flex; justify-content: space-between; gap: 12px; padding: 11px 2px; cursor: pointer; font-size: .76rem; }
210
+ .usage-call-copy { padding: 0 0 15px 17px; color: var(--muted); font-size: .72rem; }
211
+ .usage-call-copy h4 { margin: 13px 0 6px; color: var(--faint); font-size: .63rem; letter-spacing: .1em; text-transform: uppercase; }
212
+ .usage-call-copy pre, .usage-transcript { max-height: 260px; overflow: auto; margin: 6px 0; padding: 11px; border: 1px solid var(--line); border-radius: 6px; color: var(--mint-bright); background: rgba(7, 16, 21, .62); font-family: "Cascadia Code", "Consolas", monospace; font-size: .7rem; line-height: 1.5; white-space: pre-wrap; }
186
213
  .footer-note { display: flex; justify-content: center; gap: 8px; padding: 18px 0 28px; color: var(--faint); font-size: .7rem; }
187
214
  .footer-separator { color: var(--line-strong); }
188
215
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
@@ -196,7 +223,7 @@ input::placeholder { color: var(--faint); }
196
223
  .detail-panel { min-height: 420px; }
197
224
  .workspace-layout { grid-template-columns: 1fr; }
198
225
  .workspace-rail { order: 0; }
199
- .workspace-nav { grid-template-columns: repeat(5, 1fr); }
226
+ .workspace-nav { grid-template-columns: repeat(6, 1fr); }
200
227
  .workspace-nav a { border-left: 0; border-bottom: 2px solid transparent; padding: 9px 6px; text-align: center; }
201
228
  .workspace-nav a:hover, .workspace-nav a.is-active { border-bottom-color: var(--amber); border-left-color: transparent; }
202
229
  .workspace-meta { display: none; }
@@ -212,12 +239,18 @@ input::placeholder { color: var(--faint); }
212
239
  .panel-heading { flex-direction: column; }
213
240
  .search-field, .search-field input { width: 100%; }
214
241
  .detail-facts { grid-template-columns: 1fr; }
242
+ .usage-summary-grid { grid-template-columns: repeat(2, 1fr); }
243
+ .usage-token-grid { grid-template-columns: repeat(3, 1fr); }
215
244
  .detail-header { flex-direction: column; }
216
245
  .workspace-header { grid-template-columns: 1fr auto; align-items: start; gap: 13px; }
217
246
  .workspace-header .back-link { grid-column: 1 / -1; }
218
247
  .workspace-header h1 { font-size: 2.8rem; }
219
248
  .workspace-content { padding: 20px 15px; }
220
249
  .workspace-nav { grid-template-columns: repeat(3, 1fr); }
250
+ .usage-filters { align-items: stretch; flex-direction: column; }
251
+ .usage-filter, .usage-filter input, .usage-filter select, .button-primary { width: 100%; }
252
+ .usage-summary-grid { grid-template-columns: 1fr; }
253
+ .usage-token-grid { grid-template-columns: repeat(2, 1fr); }
221
254
  .memory-document-row { grid-template-columns: 28px minmax(0, 1fr); }
222
255
  .memory-document-size { display: none; }
223
256
  .footer-note { flex-direction: column; align-items: center; gap: 4px; }