Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/operations/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/pages/Tools.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
109 changes: 57 additions & 52 deletions frontend/src/pages/Tools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -174,62 +174,67 @@ export default function Tools() {
<LoadingSkeleton rows={8} height={40} />
) : error ? (
<ErrorState message={error.message} />
) : total === 0 ? (
q ? (
<div className={styles.noResults}>No tools match "{q}"</div>
) : (
<EmptyState
heading="No tool data"
subtext="Tool usage will appear here once sessions are recorded in the selected range."
/>
)
) : (
<Card title="All Tools">
{total === 0 ? (
q ? (
<div className={styles.noResults}>No tools match "{q}"</div>
) : (
<EmptyState
heading="No tool data"
subtext="Tool usage will appear here once sessions are recorded in the selected range."
/>
)
) : (
<>
<DataTable<ToolItem>
columns={toolColumns}
rows={data!.items}
sort={{ key: sort as keyof ToolItem, dir: order }}
onSortChange={handleSortChange}
/>
{data?.duration_stats_since && (
<p className={styles.statsNote}>
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.
</p>
)}
<Pagination page={page} total={total} onChange={setPage} />
</>
<>
<DataTable<ToolItem>
columns={toolColumns}
rows={data!.items}
sort={{ key: sort as keyof ToolItem, dir: order }}
onSortChange={handleSortChange}
/>
{data?.duration_stats_since && (
<p className={styles.statsNote}>
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.
</p>
)}
</Card>
<Pagination page={page} total={total} onChange={setPage} />
</>
)}

{bashLoading && !bashData ? (
<Card title="Bash Command Breakdown">
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Bash commands</h2>
<p className={styles.sectionNote}>
Each distinct command passed to the Bash tool, read from the <code>command</code> span
attribute.
</p>

{bashLoading && !bashData ? (
<LoadingSkeleton rows={4} height={36} />
</Card>
) : bashTotal > 0 ? (
<Card title="Bash Command Breakdown">
<p className={styles.sectionNote}>
Each distinct command passed to the Bash tool. Commands are extracted from the{' '}
<code>command</code> span attribute.
</p>
<DataTable<BashCommandItem>
columns={bashColumns}
rows={bashData!.items}
sort={{ key: bashSort as keyof BashCommandItem, dir: bashOrder }}
onSortChange={handleBashSortChange}
) : bashError ? (
<ErrorState message={bashError.message} />
) : bashTotal === 0 ? (
<EmptyState
heading="No command detail in this data"
subtext="Claude Code reports which tool ran, not what it ran — its Bash spans carry no command text. This fills in only for spans from a producer that sends the attribute."
/>
{bashData?.covered_since && (
<p className={styles.statsNote}>
This breakdown starts from {formatDay(bashData.covered_since)} — older activity is kept
only as daily totals, which carry no command detail.
</p>
)}
<Pagination page={bashPage} total={bashTotal} onChange={setBashPage} />
</Card>
) : null}
) : (
<>
<DataTable<BashCommandItem>
columns={bashColumns}
rows={bashData!.items}
sort={{ key: bashSort as keyof BashCommandItem, dir: bashOrder }}
onSortChange={handleBashSortChange}
/>
{bashData?.covered_since && (
<p className={styles.statsNote}>
This breakdown starts from {formatDay(bashData.covered_since)} — older activity is kept
only as daily totals, which carry no command detail.
</p>
)}
<Pagination page={bashPage} total={bashTotal} onChange={setBashPage} />
</>
)}
</section>
</div>
)
}
4 changes: 2 additions & 2 deletions internal/dashboard/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<title>cotel</title>
<script type="module" crossorigin src="/assets/index-Cnsd225m.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Avbypms3.css">
<script type="module" crossorigin src="/assets/index-B0ujC8Ov.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CoQRqzyQ.css">
</head>
<body>
<div id="root"></div>
Expand Down