diff --git a/src/codex_usage_tracker/diagnostic_reports.py b/src/codex_usage_tracker/diagnostic_reports.py index eab2344..d7aeeb6 100644 --- a/src/codex_usage_tracker/diagnostic_reports.py +++ b/src/codex_usage_tracker/diagnostic_reports.py @@ -22,6 +22,10 @@ DIAGNOSTIC_FACT_SORT_CHOICES = ( "uncached", "tokens", + "cached", + "output", + "cache", + "largest", "calls", "occurrences", "time", diff --git a/src/codex_usage_tracker/plugin_data/dashboard/dashboard_diagnostics.js b/src/codex_usage_tracker/plugin_data/dashboard/dashboard_diagnostics.js index 0522b62..7185d58 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/dashboard_diagnostics.js +++ b/src/codex_usage_tracker/plugin_data/dashboard/dashboard_diagnostics.js @@ -34,6 +34,7 @@ const factCallPageSize = 25; const factCallPayloads = new Map(); const factCallSorts = new Map(); + const factSorts = new Map(); function setActive(active) { diagnosticsPanelEl.hidden = !active; @@ -52,6 +53,7 @@ payloads = emptyPayloads(); factCallPayloads.clear(); factCallSorts.clear(); + factSorts.clear(); } function renderDiagnostics(dateRange) { @@ -86,10 +88,13 @@ const generation = requestGeneration + 1; requestGeneration = generation; try { + const factsSort = factSortState('facts'); + const toolsSort = factSortState('tools'); + const compactionsSort = factSortState('compactions'); const [facts, tools, compactions] = await Promise.all([ - fetchPayload('/api/diagnostics/facts', { ...filters, limit: '50', sort: 'uncached', direction: 'desc' }), - fetchPayload('/api/diagnostics/tools', { ...filters, limit: '25', sort: 'uncached', direction: 'desc' }), - fetchPayload('/api/diagnostics/compactions', { ...filters, limit: '25', sort: 'uncached', direction: 'desc' }), + fetchPayload('/api/diagnostics/facts', { ...filters, limit: '50', sort: factsSort.sort, direction: factsSort.direction }), + fetchPayload('/api/diagnostics/tools', { ...filters, limit: '25', sort: toolsSort.sort, direction: toolsSort.direction }), + fetchPayload('/api/diagnostics/compactions', { ...filters, limit: '25', sort: compactionsSort.sort, direction: compactionsSort.direction }), ]); if (generation !== requestGeneration || signature !== activeSignature) return; payloads = { facts, tools, compactions }; @@ -164,6 +169,20 @@ renderIfActive(); } + function sortFactRows(sectionKey, sortKey) { + if (!diagnosticFactSortLabels()[sortKey]) return; + const current = factSortState(sectionKey); + const next = current.sort === sortKey + ? { sort: sortKey, direction: current.direction === 'asc' ? 'desc' : 'asc' } + : { sort: sortKey, direction: defaultFactSortDirection(sortKey) }; + factSorts.set(sectionKey, next); + status = 'loading'; + errorMessage = ''; + const filters = getDiagnosticFilters(); + void fetchDiagnostics(activeSignature, filters); + renderIfActive(); + } + function sortFactCalls(sortKey) { if (!selectedFactKey || !diagnosticCallSortLabels()[sortKey]) return; const current = factCallSortState(selectedFactKey); @@ -206,30 +225,30 @@ ${readoutMetric('Compaction rows', payloads.compactions)} Structured labels only. Raw context remains on-demand in the call investigator. - ${renderFactSection('Top Diagnostic Facts', 'Ranked by associated uncached input tokens.', payloads.facts, loading)} - ${renderFactSection('Tool and Function Activity', 'Tool/function facts associated with model calls.', payloads.tools, loading)} - ${renderFactSection('Compaction Activity', 'Compaction facts and post-compaction associated costs.', payloads.compactions, loading)} + ${renderFactSection('facts', 'Top Diagnostic Facts', 'Structured facts associated with model calls.', payloads.facts, loading)} + ${renderFactSection('tools', 'Tool and Function Activity', 'Tool/function facts associated with model calls.', payloads.tools, loading)} + ${renderFactSection('compactions', 'Compaction Activity', 'Compaction facts and post-compaction associated costs.', payloads.compactions, loading)} `; } - function renderFactSection(title, caption, payload, loading) { + function renderFactSection(sectionKey, title, caption, payload, loading) { const rows = Array.isArray(payload?.rows) ? payload.rows : []; return `
${escapeHtml(caption)}
+${escapeHtml(`${caption} Sorted by ${diagnosticFactSortDescription(sectionKey)}.`)}
| ${escapeHtml(label)} | `; } + function diagnosticFactHeader(sectionKey, sortKey, label, numeric = false, tooltip = '') { + const state = factSortState(sectionKey); + const active = state.sort === sortKey; + const indicator = active ? (state.direction === 'asc' ? '▲' : '▼') : ''; + const ariaSort = active ? (state.direction === 'asc' ? 'ascending' : 'descending') : 'none'; + const tooltipAttr = tooltipAttributes(tooltip); + return ` ++ + | + `; + } + function diagnosticCallHeader(sortKey, label, numeric = false, tooltip = '') { const state = factCallSortState(selectedFactKey); const active = state.sort === sortKey; @@ -396,6 +444,28 @@ `; } + function diagnosticFactSortDescription(sectionKey) { + const state = factSortState(sectionKey); + const labels = diagnosticFactSortLabels(); + const label = labels[state.sort] || state.sort; + return `${label} ${state.direction === 'asc' ? 'ascending' : 'descending'}`; + } + + function diagnosticFactSortLabels() { + return { + cache: 'cache ratio', + cached: 'cached input tokens', + calls: 'associated calls', + fact: 'fact name', + largest: 'largest call', + occurrences: 'occurrences', + output: 'output tokens', + time: 'latest call time', + tokens: 'total tokens', + uncached: 'uncached input tokens', + }; + } + function diagnosticCallSortDescription() { const state = factCallSortState(selectedFactKey); const labels = diagnosticCallSortLabels(); @@ -419,6 +489,14 @@ }; } + function factSortState(sectionKey) { + return factSorts.get(sectionKey) || { sort: 'uncached', direction: 'desc' }; + } + + function defaultFactSortDirection(sortKey) { + return sortKey === 'fact' ? 'asc' : 'desc'; + } + function factCallSortState(key) { return factCallSorts.get(key) || { sort: 'tokens', direction: 'desc' }; } @@ -483,10 +561,25 @@ }; } + function captureScrollPosition() { + pendingScrollAnchor = { + key: '', + type: 'scroll', + top: 0, + scrollY: window.scrollY, + }; + } + function restoreScrollAnchor() { if (!pendingScrollAnchor) return; const anchor = pendingScrollAnchor; pendingScrollAnchor = null; + if (anchor.type === 'scroll') { + window.requestAnimationFrame(() => { + window.scrollTo({ top: anchor.scrollY, behavior: 'auto' }); + }); + return; + } window.requestAnimationFrame(() => { const target = anchor.type === 'load-more' ? diagnosticsPanelEl.querySelector('[data-diagnostics-call-load-more]') @@ -539,6 +632,17 @@ void fetchFactCalls(factType, factName, { append: true }); return; } + const factSortButton = target.closest('[data-diagnostics-fact-sort-key]'); + if (factSortButton && diagnosticsPanelEl.contains(factSortButton)) { + event.preventDefault(); + event.stopPropagation(); + captureScrollPosition(); + sortFactRows( + factSortButton.dataset.diagnosticsFactSection || 'facts', + factSortButton.dataset.diagnosticsFactSortKey || '', + ); + return; + } const sortButton = target.closest('[data-diagnostics-call-sort-key]'); if (sortButton && diagnosticsPanelEl.contains(sortButton)) { event.preventDefault(); diff --git a/src/codex_usage_tracker/plugin_data/dashboard/dashboard_tables.css b/src/codex_usage_tracker/plugin_data/dashboard/dashboard_tables.css index a8364d5..f1182d3 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/dashboard_tables.css +++ b/src/codex_usage_tracker/plugin_data/dashboard/dashboard_tables.css @@ -323,12 +323,46 @@ min-width: 1120px; table-layout: fixed; } + .diagnostics-facts-table { + min-width: 1320px; + } + .diagnostics-facts-table .diagnostics-fact-col { width: 26%; } + .diagnostics-facts-table .diagnostics-count-col { width: 5.5%; } + .diagnostics-facts-table .diagnostics-token-col { width: 7.5%; } + .diagnostics-facts-table .diagnostics-ratio-col { width: 5%; } + .diagnostics-facts-table .diagnostics-latest-col { width: 14.5%; } + .diagnostics-facts-table .diagnostics-action-col { width: 6%; } .diagnostics-call-table { min-width: 1060px; } .diagnostics-table th { position: static; } + .diagnostics-facts-table th { + position: sticky; + top: 0; + z-index: 12; + background: #fbfcfe; + } + .diagnostics-facts-table th:first-child, + .diagnostics-facts-table td.diagnostics-fact-cell { + position: sticky; + left: 0; + z-index: 3; + background: #ffffff; + box-shadow: 1px 0 0 var(--line); + } + .diagnostics-facts-table th:first-child { + z-index: 15; + background: #fbfcfe; + } + .diagnostics-facts-table tr.selected-row td.diagnostics-fact-cell { + background: #eff6ff; + } + .diagnostics-facts-table tbody tr:not(.diagnostics-drilldown-row):hover td, + .diagnostics-facts-table tbody tr:not(.diagnostics-drilldown-row):hover td.diagnostics-fact-cell { + background: #eff6ff; + } .diagnostics-table td, .diagnostics-table th { padding: 8px 10px; @@ -342,9 +376,10 @@ min-width: 0; } .diagnostic-fact strong { - overflow-wrap: anywhere; + overflow-wrap: break-word; font-size: 12px; line-height: 1.25; + word-break: normal; } .diagnostic-fact span { color: var(--muted); diff --git a/src/codex_usage_tracker/store.py b/src/codex_usage_tracker/store.py index 4455bb7..0af764a 100644 --- a/src/codex_usage_tracker/store.py +++ b/src/codex_usage_tracker/store.py @@ -553,6 +553,10 @@ def query_diagnostic_facts( sort_map = { "uncached": "associated_uncached_input_tokens", "tokens": "associated_total_tokens", + "cached": "associated_cached_input_tokens", + "output": "associated_output_tokens", + "cache": "avg_cache_ratio", + "largest": "largest_call_tokens", "calls": "associated_calls", "occurrences": "occurrences", "time": "latest_event_timestamp", @@ -681,6 +685,10 @@ def query_diagnostic_summary( sort_map = { "uncached": "associated_uncached_input_tokens", "tokens": "associated_total_tokens", + "cached": "associated_cached_input_tokens", + "output": "associated_output_tokens", + "cache": "avg_cache_ratio", + "largest": "largest_call_tokens", "calls": "associated_calls", "occurrences": "occurrences", "time": "latest_event_timestamp", diff --git a/tests/test_dashboard_payload.py b/tests/test_dashboard_payload.py index c8efebb..19bb5ba 100644 --- a/tests/test_dashboard_payload.py +++ b/tests/test_dashboard_payload.py @@ -292,6 +292,14 @@ def test_dashboard_and_csv_are_aggregate_only(tmp_path: Path) -> None: assert "Occurrences: count of matching diagnostic fact events" in dashboard_diagnostics_js assert "Associated total tokens for those calls" in dashboard_diagnostics_js assert "Average cache ratio across associated calls" in dashboard_diagnostics_js + assert "data-diagnostics-fact-sort-key" in dashboard_diagnostics_js + assert "data-diagnostics-fact-sort-active" in dashboard_diagnostics_js + assert "sortFactRows" in dashboard_diagnostics_js + assert "diagnosticFactHeader" in dashboard_diagnostics_js + assert "diagnostics-facts-table" in dashboard_surface + assert "diagnostics-fact-cell" in dashboard_surface + assert "diagnostics-facts-table th:first-child" in dashboard_css + assert "td.diagnostics-fact-cell" in dashboard_css assert "captureScrollAnchor" in dashboard_diagnostics_js assert "restoreScrollAnchor" in dashboard_diagnostics_js assert "data-diagnostics-call-load-more" in dashboard_diagnostics_js diff --git a/tests/test_dashboard_server.py b/tests/test_dashboard_server.py index 6ee9a25..a174f76 100644 --- a/tests/test_dashboard_server.py +++ b/tests/test_dashboard_server.py @@ -462,6 +462,9 @@ def test_dashboard_server_live_sql_api_slices_are_aggregate_only(tmp_path: Path) recommendations_payload = _read_json(f"{base_url}/api/recommendations?limit=5") diagnostics_summary_payload = _read_json(f"{base_url}/api/diagnostics/summary?limit=5") diagnostics_facts_payload = _read_json(f"{base_url}/api/diagnostics/facts?limit=5") + diagnostics_sorted_facts_payload = _read_json( + f"{base_url}/api/diagnostics/facts?limit=5&sort=cached&direction=desc" + ) diagnostics_compactions_payload = _read_json( f"{base_url}/api/diagnostics/compactions?limit=5" ) @@ -526,6 +529,9 @@ def test_dashboard_server_live_sql_api_slices_are_aggregate_only(tmp_path: Path) assert {row["fact_name"] for row in diagnostics_facts_payload["rows"]} >= { "post_compaction" } + assert diagnostics_sorted_facts_payload["filters"]["sort"] == "cached" + assert diagnostics_sorted_facts_payload["filters"]["direction"] == "desc" + _assert_contract(diagnostics_sorted_facts_payload) assert diagnostics_compactions_payload["filters"]["fact_type"] == "compaction" _assert_contract(diagnostics_compactions_payload) assert {row["fact_type"] for row in diagnostics_compactions_payload["rows"]} == {
|---|