diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cdd34e..deaf021 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Fixed
+- A failing `GET /api/v1/bash-commands` no longer renders as the "no command detail in this data" explainer. The Bash section branched on row count alone, so a request that errored looked identical to one that legitimately returned nothing, blaming Claude Code's telemetry for what was actually a server fault. Fetch failures now show the error
+
+### Changed
+- Tools page is flat: the tools table sits directly on the page instead of inside an "All Tools" card. `DataTable` already draws its own bordered surface, so the card put a box inside a box for a heading the page title already gave. Matches the Users page
+- The Bash commands section no longer vanishes when it has no rows — it always renders and says why it is empty. Claude Code's tool spans carry `tool_name`, `tool_use_id` and `duration_ms` only: which tool ran, not what it ran. No `command` attribute is ever sent (verified against a captured live session, including the enhanced-telemetry beta), so this breakdown stays empty against Claude Code telemetry no matter what — the DuckDB filter-pushdown fix in 0.3.0 was a real bug fix but could not, on its own, put rows in this table. The endpoint is kept for OTLP producers that do send `command`
+
## [0.3.0] - 2026-08-09
### Fixed
diff --git a/docs/operations/api-reference.md b/docs/operations/api-reference.md
index b9f70be..4a23501 100644
--- a/docs/operations/api-reference.md
+++ b/docs/operations/api-reference.md
@@ -82,6 +82,12 @@ Per-command breakdown of `Bash` tool calls. The command text is read from the
`command` span attribute, falling back to `command` inside a JSON-encoded
`tool_input`.
+**Claude Code does not send either attribute.** Its tool spans carry `tool_name`,
+`tool_use_id` and `duration_ms` only — which tool ran, not what it ran — so
+against Claude Code telemetry this endpoint returns an empty `items` list and the
+dashboard's Bash commands section stays empty. The endpoint is kept for OTLP
+producers that do send a `command` attribute; cotel's ingest is generic.
+
| Sort key | Orders by |
|---|---|
| `command` | Command text |
diff --git a/frontend/src/pages/Tools.module.css b/frontend/src/pages/Tools.module.css
index 39f5f61..1651ee6 100644
--- a/frontend/src/pages/Tools.module.css
+++ b/frontend/src/pages/Tools.module.css
@@ -27,10 +27,21 @@
white-space: nowrap;
}
+.section {
+ margin-top: var(--space-6);
+}
+
+.sectionTitle {
+ font-size: var(--text-lg);
+ font-weight: 600;
+ color: var(--color-text-1);
+ margin: 0 0 var(--space-1);
+}
+
.sectionNote {
font-size: var(--text-sm);
color: var(--color-text-3);
- margin-bottom: var(--space-3);
+ margin: 0 0 var(--space-3);
}
.commandCell {
diff --git a/frontend/src/pages/Tools.tsx b/frontend/src/pages/Tools.tsx
index 835d90e..24b0ce7 100644
--- a/frontend/src/pages/Tools.tsx
+++ b/frontend/src/pages/Tools.tsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Search, ChevronLeft, ChevronRight } from 'lucide-react'
import { useTools, useBashCommands } from '../api'
-import { Card, DataTable, EmptyState, ErrorState, LoadingSkeleton, SegmentedControl, failRateBadge } from '../components'
+import { DataTable, EmptyState, ErrorState, LoadingSkeleton, SegmentedControl, failRateBadge } from '../components'
import type { ToolItem, BashCommandItem } from '../api'
import type { Column, SortState } from '../components'
import { RANGE_OPTIONS, useRangeCookie } from '../lib/range'
@@ -78,7 +78,7 @@ export default function Tools() {
user_id: userId,
})
- const { data: bashData, isLoading: bashLoading } = useBashCommands({
+ const { data: bashData, error: bashError, isLoading: bashLoading } = useBashCommands({
range,
sort: bashSort,
order: bashOrder,
@@ -174,62 +174,67 @@ export default function Tools() {
- Duration and error figures start from {formatDay(data.duration_stats_since)} — earlier - days were rolled up before those totals were recorded, so they count toward Calls only. -
- )} -+ Duration and error figures start from {formatDay(data.duration_stats_since)} — earlier + days were rolled up before those totals were recorded, so they count toward Calls only. +
)} -
+ Each distinct command passed to the Bash tool, read from the command span
+ attribute.
+
- Each distinct command passed to the Bash tool. Commands are extracted from the{' '}
- command span attribute.
-
- This breakdown starts from {formatDay(bashData.covered_since)} — older activity is kept - only as daily totals, which carry no command detail. -
- )} -+ This breakdown starts from {formatDay(bashData.covered_since)} — older activity is kept + only as daily totals, which carry no command detail. +
+ )} +