Files
openclaw/src/utils/transcript-tools.ts
Tak Hoffman 8a352c8f9d Web UI: add token usage dashboard (#10072)
* feat(ui): Token Usage dashboard with session analytics

Adds a comprehensive Token Usage view to the dashboard:

Backend:
- Extended session-cost-usage.ts with per-session daily breakdown
- Added date range filtering (startMs/endMs) to API endpoints
- New sessions.usage, sessions.usage.timeseries, sessions.usage.logs endpoints
- Cost breakdown by token type (input/output/cache read/write)

Frontend:
- Two-column layout: Daily chart + breakdown | Sessions list
- Interactive daily bar chart with click-to-filter and shift-click range select
- Session detail panel with usage timeline, conversation logs, context weight
- Filter chips for active day/session selections
- Toggle between tokens/cost view modes (default: cost)
- Responsive design for smaller screens

UX improvements:
- 21-day default date range
- Debounced date input (400ms)
- Session list shows filtered totals when days selected
- Context weight breakdown shows skills, tools, files contribution

* fix(ui): restore gatewayUrl validation and syncUrlWithSessionKey signature

- Restore normalizeGatewayUrl() to validate ws:/wss: protocol
- Restore isTopLevelWindow() guard for iframe security
- Revert syncUrlWithSessionKey signature (host param was unused)

* feat(ui): Token Usage dashboard with session analytics

Adds a comprehensive Token Usage view to the dashboard:

Backend:
- Extended session-cost-usage.ts with per-session daily breakdown
- Added date range filtering (startMs/endMs) to API endpoints
- New sessions.usage, sessions.usage.timeseries, sessions.usage.logs endpoints
- Cost breakdown by token type (input/output/cache read/write)

Frontend:
- Two-column layout: Daily chart + breakdown | Sessions list
- Interactive daily bar chart with click-to-filter and shift-click range select
- Session detail panel with usage timeline, conversation logs, context weight
- Filter chips for active day/session selections
- Toggle between tokens/cost view modes (default: cost)
- Responsive design for smaller screens

UX improvements:
- 21-day default date range
- Debounced date input (400ms)
- Session list shows filtered totals when days selected
- Context weight breakdown shows skills, tools, files contribution

* fix: usage dashboard data + cost handling (#8462) (thanks @mcinteerj)

* Usage: enrich metrics dashboard

* Usage: add latency + model trends

* Gateway: improve usage log parsing

* UI: add usage query helpers

* UI: client-side usage filter + debounce

* Build: harden write-cli-compat timing

* UI: add conversation log filters

* UI: fix usage dashboard lint + state

* Web UI: default usage dates to local day

* Protocol: sync session usage params (#8462) (thanks @mcinteerj, @TakHoffman)

---------

Co-authored-by: Jake McInteer <mcinteerj@gmail.com>
2026-02-05 22:35:46 -06:00

74 lines
1.8 KiB
TypeScript

type ToolResultCounts = {
total: number;
errors: number;
};
const TOOL_CALL_TYPES = new Set(["tool_use", "toolcall", "tool_call"]);
const TOOL_RESULT_TYPES = new Set(["tool_result", "tool_result_error"]);
const normalizeType = (value: unknown): string => {
if (typeof value !== "string") {
return "";
}
return value.trim().toLowerCase();
};
export const extractToolCallNames = (message: Record<string, unknown>): string[] => {
const names = new Set<string>();
const toolNameRaw = message.toolName ?? message.tool_name;
if (typeof toolNameRaw === "string" && toolNameRaw.trim()) {
names.add(toolNameRaw.trim());
}
const content = message.content;
if (!Array.isArray(content)) {
return Array.from(names);
}
for (const entry of content) {
if (!entry || typeof entry !== "object") {
continue;
}
const block = entry as Record<string, unknown>;
const type = normalizeType(block.type);
if (!TOOL_CALL_TYPES.has(type)) {
continue;
}
const name = block.name;
if (typeof name === "string" && name.trim()) {
names.add(name.trim());
}
}
return Array.from(names);
};
export const hasToolCall = (message: Record<string, unknown>): boolean =>
extractToolCallNames(message).length > 0;
export const countToolResults = (message: Record<string, unknown>): ToolResultCounts => {
const content = message.content;
if (!Array.isArray(content)) {
return { total: 0, errors: 0 };
}
let total = 0;
let errors = 0;
for (const entry of content) {
if (!entry || typeof entry !== "object") {
continue;
}
const block = entry as Record<string, unknown>;
const type = normalizeType(block.type);
if (!TOOL_RESULT_TYPES.has(type)) {
continue;
}
total += 1;
if (block.is_error === true) {
errors += 1;
}
}
return { total, errors };
};