From 877bce4fe4d0681b4fca416b10cea3a1d0ab9c9f Mon Sep 17 00:00:00 2001 From: Daedalus Date: Sun, 9 Aug 2026 23:45:34 +0200 Subject: [PATCH 1/3] refactor(tools): flatten the page and stop the Bash section from vanishing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tools table was wrapped in a Card titled "All Tools". DataTable already draws its own bordered, shadowed surface, so that was a box inside a box for a heading the page title already gave. The table now sits directly on the page, matching the Users list. The Bash breakdown rendered as null whenever it had no rows, so it disappeared with no trace and no explanation. It is now a plain section that always renders and states why it is empty. It is empty for a reason no fix can change: Claude Code's tool spans carry tool_name, tool_use_id and duration_ms only — which tool ran, not what it ran. Captured from a live session (enhanced-telemetry beta, and with user-prompt logging on) no span carries a command or tool_input attribute at any point. The endpoint stays for OTLP producers that do send one; the docs and changelog now say so instead of implying the breakdown works against Claude Code. Co-Authored-By: Daedalus Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 4 + docs/operations/api-reference.md | 6 ++ frontend/src/pages/Tools.module.css | 13 +++- frontend/src/pages/Tools.tsx | 105 ++++++++++++++------------- internal/dashboard/static/index.html | 4 +- 5 files changed, 78 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cdd34e..97fe376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### 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..d64e771 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' @@ -174,62 +174,65 @@ export default function Tools() { ) : error ? ( + ) : total === 0 ? ( + q ? ( +
No tools match "{q}"
+ ) : ( + + ) ) : ( - - {total === 0 ? ( - q ? ( -
No tools match "{q}"
- ) : ( - - ) - ) : ( - <> - - columns={toolColumns} - rows={data!.items} - sort={{ key: sort as keyof ToolItem, dir: order }} - onSortChange={handleSortChange} - /> - {data?.duration_stats_since && ( -

- 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. -

- )} - - + <> + + columns={toolColumns} + rows={data!.items} + sort={{ key: sort as keyof ToolItem, dir: order }} + onSortChange={handleSortChange} + /> + {data?.duration_stats_since && ( +

+ 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. +

)} -
+ + )} - {bashLoading && !bashData ? ( - +
+

Bash commands

+

+ Each distinct command passed to the Bash tool, read from the command span + attribute. +

+ + {bashLoading && !bashData ? ( - - ) : bashTotal > 0 ? ( - -

- Each distinct command passed to the Bash tool. Commands are extracted from the{' '} - command span attribute. -

- - columns={bashColumns} - rows={bashData!.items} - sort={{ key: bashSort as keyof BashCommandItem, dir: bashOrder }} - onSortChange={handleBashSortChange} + ) : bashTotal === 0 ? ( + - {bashData?.covered_since && ( -

- This breakdown starts from {formatDay(bashData.covered_since)} — older activity is kept - only as daily totals, which carry no command detail. -

- )} - -
- ) : null} + ) : ( + <> + + columns={bashColumns} + rows={bashData!.items} + sort={{ key: bashSort as keyof BashCommandItem, dir: bashOrder }} + onSortChange={handleBashSortChange} + /> + {bashData?.covered_since && ( +

+ This breakdown starts from {formatDay(bashData.covered_since)} — older activity is kept + only as daily totals, which carry no command detail. +

+ )} + + + )} +
) } diff --git a/internal/dashboard/static/index.html b/internal/dashboard/static/index.html index b5eb3d3..fce5c2f 100644 --- a/internal/dashboard/static/index.html +++ b/internal/dashboard/static/index.html @@ -5,8 +5,8 @@ cotel - - + +
From e3d7dd8e176d4a82571b16b316bb4acc74309479 Mon Sep 17 00:00:00 2001 From: Daedalus Date: Mon, 10 Aug 2026 00:02:00 +0200 Subject: [PATCH 2/3] fix(tools): surface Bash breakdown fetch errors instead of blaming telemetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Bash section only branched on loading and row count, so a failed /bash-commands request fell through to the empty state and told the user Claude Code sends no command text — attributing an API failure to the producer's telemetry. Destructure SWR's error and render ErrorState. Co-Authored-By: Daedalus Co-Authored-By: Claude Opus 4.8 --- frontend/src/pages/Tools.tsx | 4 +++- internal/dashboard/static/index.html | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Tools.tsx b/frontend/src/pages/Tools.tsx index d64e771..24b0ce7 100644 --- a/frontend/src/pages/Tools.tsx +++ b/frontend/src/pages/Tools.tsx @@ -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, @@ -210,6 +210,8 @@ export default function Tools() { {bashLoading && !bashData ? ( + ) : bashError ? ( + ) : bashTotal === 0 ? ( cotel - + From 9b5941f8a1d3ffbe1e90cf5f08f238e34c117065 Mon Sep 17 00:00:00 2001 From: Daedalus Date: Mon, 10 Aug 2026 00:05:04 +0200 Subject: [PATCH 3/3] docs(changelog): record the Bash-section error-state fix Co-Authored-By: Daedalus Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97fe376..deaf021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ 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`