mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor: dedupe shared helpers across ui/gateway/extensions
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
import type { IconName } from "./icons.ts";
|
||||
import {
|
||||
defaultTitle,
|
||||
normalizeToolName,
|
||||
normalizeVerb,
|
||||
resolveActionSpec,
|
||||
resolveDetailFromKeys,
|
||||
resolveReadDetail,
|
||||
resolveWriteDetail,
|
||||
type ToolDisplaySpec as ToolDisplaySpecBase,
|
||||
} from "../../../src/agents/tool-display-common.js";
|
||||
import { shortenHomeInString } from "../../../src/utils.js";
|
||||
import rawConfig from "./tool-display.json" with { type: "json" };
|
||||
|
||||
type ToolDisplayActionSpec = {
|
||||
label?: string;
|
||||
detailKeys?: string[];
|
||||
};
|
||||
|
||||
type ToolDisplaySpec = {
|
||||
type ToolDisplaySpec = ToolDisplaySpecBase & {
|
||||
icon?: string;
|
||||
title?: string;
|
||||
label?: string;
|
||||
detailKeys?: string[];
|
||||
actions?: Record<string, ToolDisplayActionSpec>;
|
||||
};
|
||||
|
||||
type ToolDisplayConfig = {
|
||||
@@ -33,129 +35,6 @@ const TOOL_DISPLAY_CONFIG = rawConfig as ToolDisplayConfig;
|
||||
const FALLBACK = TOOL_DISPLAY_CONFIG.fallback ?? { icon: "puzzle" };
|
||||
const TOOL_MAP = TOOL_DISPLAY_CONFIG.tools ?? {};
|
||||
|
||||
function normalizeToolName(name?: string): string {
|
||||
return (name ?? "tool").trim();
|
||||
}
|
||||
|
||||
function defaultTitle(name: string): string {
|
||||
const cleaned = name.replace(/_/g, " ").trim();
|
||||
if (!cleaned) {
|
||||
return "Tool";
|
||||
}
|
||||
return cleaned
|
||||
.split(/\s+/)
|
||||
.map((part) =>
|
||||
part.length <= 2 && part.toUpperCase() === part
|
||||
? part
|
||||
: `${part.at(0)?.toUpperCase() ?? ""}${part.slice(1)}`,
|
||||
)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function normalizeVerb(value?: string): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return trimmed.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
function coerceDisplayValue(value: unknown): string | undefined {
|
||||
if (value === null || value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const firstLine = trimmed.split(/\r?\n/)[0]?.trim() ?? "";
|
||||
if (!firstLine) {
|
||||
return undefined;
|
||||
}
|
||||
return firstLine.length > 160 ? `${firstLine.slice(0, 157)}…` : firstLine;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const values = value
|
||||
.map((item) => coerceDisplayValue(item))
|
||||
.filter((item): item is string => Boolean(item));
|
||||
if (values.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const preview = values.slice(0, 3).join(", ");
|
||||
return values.length > 3 ? `${preview}…` : preview;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function lookupValueByPath(args: unknown, path: string): unknown {
|
||||
if (!args || typeof args !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
let current: unknown = args;
|
||||
for (const segment of path.split(".")) {
|
||||
if (!segment) {
|
||||
return undefined;
|
||||
}
|
||||
if (!current || typeof current !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const record = current as Record<string, unknown>;
|
||||
current = record[segment];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function resolveDetailFromKeys(args: unknown, keys: string[]): string | undefined {
|
||||
for (const key of keys) {
|
||||
const value = lookupValueByPath(args, key);
|
||||
const display = coerceDisplayValue(value);
|
||||
if (display) {
|
||||
return display;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveReadDetail(args: unknown): string | undefined {
|
||||
if (!args || typeof args !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const record = args as Record<string, unknown>;
|
||||
const path = typeof record.path === "string" ? record.path : undefined;
|
||||
if (!path) {
|
||||
return undefined;
|
||||
}
|
||||
const offset = typeof record.offset === "number" ? record.offset : undefined;
|
||||
const limit = typeof record.limit === "number" ? record.limit : undefined;
|
||||
if (offset !== undefined && limit !== undefined) {
|
||||
return `${path}:${offset}-${offset + limit}`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function resolveWriteDetail(args: unknown): string | undefined {
|
||||
if (!args || typeof args !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const record = args as Record<string, unknown>;
|
||||
const path = typeof record.path === "string" ? record.path : undefined;
|
||||
return path;
|
||||
}
|
||||
|
||||
function resolveActionSpec(
|
||||
spec: ToolDisplaySpec | undefined,
|
||||
action: string | undefined,
|
||||
): ToolDisplayActionSpec | undefined {
|
||||
if (!spec || !action) {
|
||||
return undefined;
|
||||
}
|
||||
return spec.actions?.[action] ?? undefined;
|
||||
}
|
||||
|
||||
export function resolveToolDisplay(params: {
|
||||
name?: string;
|
||||
args?: unknown;
|
||||
@@ -185,7 +64,10 @@ export function resolveToolDisplay(params: {
|
||||
|
||||
const detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];
|
||||
if (!detail && detailKeys.length > 0) {
|
||||
detail = resolveDetailFromKeys(params.args, detailKeys);
|
||||
detail = resolveDetailFromKeys(params.args, detailKeys, {
|
||||
mode: "first",
|
||||
coerce: { includeFalse: true, includeZero: true },
|
||||
});
|
||||
}
|
||||
|
||||
if (!detail && params.meta) {
|
||||
@@ -224,10 +106,3 @@ export function formatToolSummary(display: ToolDisplay): string {
|
||||
const detail = formatToolDetail(display);
|
||||
return detail ? `${display.label}: ${detail}` : display.label;
|
||||
}
|
||||
|
||||
function shortenHomeInString(input: string): string {
|
||||
if (!input) {
|
||||
return input;
|
||||
}
|
||||
return input.replace(/\/Users\/[^/]+/g, "~").replace(/\/home\/[^/]+/g, "~");
|
||||
}
|
||||
|
||||
@@ -424,222 +424,15 @@ export type SessionsPatchResult = {
|
||||
};
|
||||
};
|
||||
|
||||
export type SessionsUsageEntry = {
|
||||
key: string;
|
||||
label?: string;
|
||||
sessionId?: string;
|
||||
updatedAt?: number;
|
||||
agentId?: string;
|
||||
channel?: string;
|
||||
chatType?: string;
|
||||
origin?: {
|
||||
label?: string;
|
||||
provider?: string;
|
||||
surface?: string;
|
||||
chatType?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
accountId?: string;
|
||||
threadId?: string | number;
|
||||
};
|
||||
modelOverride?: string;
|
||||
providerOverride?: string;
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
usage: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
totalCost: number;
|
||||
inputCost?: number;
|
||||
outputCost?: number;
|
||||
cacheReadCost?: number;
|
||||
cacheWriteCost?: number;
|
||||
missingCostEntries: number;
|
||||
firstActivity?: number;
|
||||
lastActivity?: number;
|
||||
durationMs?: number;
|
||||
activityDates?: string[]; // YYYY-MM-DD dates when session had activity
|
||||
dailyBreakdown?: Array<{ date: string; tokens: number; cost: number }>;
|
||||
dailyMessageCounts?: Array<{
|
||||
date: string;
|
||||
total: number;
|
||||
user: number;
|
||||
assistant: number;
|
||||
toolCalls: number;
|
||||
toolResults: number;
|
||||
errors: number;
|
||||
}>;
|
||||
dailyLatency?: Array<{
|
||||
date: string;
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
}>;
|
||||
dailyModelUsage?: Array<{
|
||||
date: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
count: number;
|
||||
}>;
|
||||
messageCounts?: {
|
||||
total: number;
|
||||
user: number;
|
||||
assistant: number;
|
||||
toolCalls: number;
|
||||
toolResults: number;
|
||||
errors: number;
|
||||
};
|
||||
toolUsage?: {
|
||||
totalCalls: number;
|
||||
uniqueTools: number;
|
||||
tools: Array<{ name: string; count: number }>;
|
||||
};
|
||||
modelUsage?: Array<{
|
||||
provider?: string;
|
||||
model?: string;
|
||||
count: number;
|
||||
totals: SessionsUsageTotals;
|
||||
}>;
|
||||
latency?: {
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
};
|
||||
} | null;
|
||||
contextWeight?: {
|
||||
systemPrompt: { chars: number; projectContextChars: number; nonProjectContextChars: number };
|
||||
skills: { promptChars: number; entries: Array<{ name: string; blockChars: number }> };
|
||||
tools: {
|
||||
listChars: number;
|
||||
schemaChars: number;
|
||||
entries: Array<{ name: string; summaryChars: number; schemaChars: number }>;
|
||||
};
|
||||
injectedWorkspaceFiles: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
rawChars: number;
|
||||
injectedChars: number;
|
||||
truncated: boolean;
|
||||
}>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type SessionsUsageTotals = {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
totalCost: number;
|
||||
inputCost: number;
|
||||
outputCost: number;
|
||||
cacheReadCost: number;
|
||||
cacheWriteCost: number;
|
||||
missingCostEntries: number;
|
||||
};
|
||||
|
||||
export type SessionsUsageResult = {
|
||||
updatedAt: number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
sessions: SessionsUsageEntry[];
|
||||
totals: SessionsUsageTotals;
|
||||
aggregates: {
|
||||
messages: {
|
||||
total: number;
|
||||
user: number;
|
||||
assistant: number;
|
||||
toolCalls: number;
|
||||
toolResults: number;
|
||||
errors: number;
|
||||
};
|
||||
tools: {
|
||||
totalCalls: number;
|
||||
uniqueTools: number;
|
||||
tools: Array<{ name: string; count: number }>;
|
||||
};
|
||||
byModel: Array<{
|
||||
provider?: string;
|
||||
model?: string;
|
||||
count: number;
|
||||
totals: SessionsUsageTotals;
|
||||
}>;
|
||||
byProvider: Array<{
|
||||
provider?: string;
|
||||
model?: string;
|
||||
count: number;
|
||||
totals: SessionsUsageTotals;
|
||||
}>;
|
||||
byAgent: Array<{ agentId: string; totals: SessionsUsageTotals }>;
|
||||
byChannel: Array<{ channel: string; totals: SessionsUsageTotals }>;
|
||||
latency?: {
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
};
|
||||
dailyLatency?: Array<{
|
||||
date: string;
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
}>;
|
||||
modelDaily?: Array<{
|
||||
date: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
count: number;
|
||||
}>;
|
||||
daily: Array<{
|
||||
date: string;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
messages: number;
|
||||
toolCalls: number;
|
||||
errors: number;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type CostUsageDailyEntry = SessionsUsageTotals & { date: string };
|
||||
|
||||
export type CostUsageSummary = {
|
||||
updatedAt: number;
|
||||
days: number;
|
||||
daily: CostUsageDailyEntry[];
|
||||
totals: SessionsUsageTotals;
|
||||
};
|
||||
|
||||
export type SessionUsageTimePoint = {
|
||||
timestamp: number;
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
cost: number;
|
||||
cumulativeTokens: number;
|
||||
cumulativeCost: number;
|
||||
};
|
||||
|
||||
export type SessionUsageTimeSeries = {
|
||||
sessionId?: string;
|
||||
points: SessionUsageTimePoint[];
|
||||
};
|
||||
export type {
|
||||
CostUsageDailyEntry,
|
||||
CostUsageSummary,
|
||||
SessionsUsageEntry,
|
||||
SessionsUsageResult,
|
||||
SessionsUsageTotals,
|
||||
SessionUsageTimePoint,
|
||||
SessionUsageTimeSeries,
|
||||
} from "./usage-types.ts";
|
||||
|
||||
export type CronSchedule =
|
||||
| { kind: "at"; at: string }
|
||||
|
||||
216
ui/src/ui/usage-types.ts
Normal file
216
ui/src/ui/usage-types.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
export type SessionsUsageEntry = {
|
||||
key: string;
|
||||
label?: string;
|
||||
sessionId?: string;
|
||||
updatedAt?: number;
|
||||
agentId?: string;
|
||||
channel?: string;
|
||||
chatType?: string;
|
||||
origin?: {
|
||||
label?: string;
|
||||
provider?: string;
|
||||
surface?: string;
|
||||
chatType?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
accountId?: string;
|
||||
threadId?: string | number;
|
||||
};
|
||||
modelOverride?: string;
|
||||
providerOverride?: string;
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
usage: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
totalCost: number;
|
||||
inputCost?: number;
|
||||
outputCost?: number;
|
||||
cacheReadCost?: number;
|
||||
cacheWriteCost?: number;
|
||||
missingCostEntries: number;
|
||||
firstActivity?: number;
|
||||
lastActivity?: number;
|
||||
durationMs?: number;
|
||||
activityDates?: string[];
|
||||
dailyBreakdown?: Array<{ date: string; tokens: number; cost: number }>;
|
||||
dailyMessageCounts?: Array<{
|
||||
date: string;
|
||||
total: number;
|
||||
user: number;
|
||||
assistant: number;
|
||||
toolCalls: number;
|
||||
toolResults: number;
|
||||
errors: number;
|
||||
}>;
|
||||
dailyLatency?: Array<{
|
||||
date: string;
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
}>;
|
||||
dailyModelUsage?: Array<{
|
||||
date: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
count: number;
|
||||
}>;
|
||||
messageCounts?: {
|
||||
total: number;
|
||||
user: number;
|
||||
assistant: number;
|
||||
toolCalls: number;
|
||||
toolResults: number;
|
||||
errors: number;
|
||||
};
|
||||
toolUsage?: {
|
||||
totalCalls: number;
|
||||
uniqueTools: number;
|
||||
tools: Array<{ name: string; count: number }>;
|
||||
};
|
||||
modelUsage?: Array<{
|
||||
provider?: string;
|
||||
model?: string;
|
||||
count: number;
|
||||
totals: SessionsUsageTotals;
|
||||
}>;
|
||||
latency?: {
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
};
|
||||
} | null;
|
||||
contextWeight?: {
|
||||
systemPrompt: { chars: number; projectContextChars: number; nonProjectContextChars: number };
|
||||
skills: { promptChars: number; entries: Array<{ name: string; blockChars: number }> };
|
||||
tools: {
|
||||
listChars: number;
|
||||
schemaChars: number;
|
||||
entries: Array<{ name: string; summaryChars: number; schemaChars: number }>;
|
||||
};
|
||||
injectedWorkspaceFiles: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
rawChars: number;
|
||||
injectedChars: number;
|
||||
truncated: boolean;
|
||||
}>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type SessionsUsageTotals = {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
totalCost: number;
|
||||
inputCost: number;
|
||||
outputCost: number;
|
||||
cacheReadCost: number;
|
||||
cacheWriteCost: number;
|
||||
missingCostEntries: number;
|
||||
};
|
||||
|
||||
export type SessionsUsageResult = {
|
||||
updatedAt: number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
sessions: SessionsUsageEntry[];
|
||||
totals: SessionsUsageTotals;
|
||||
aggregates: {
|
||||
messages: {
|
||||
total: number;
|
||||
user: number;
|
||||
assistant: number;
|
||||
toolCalls: number;
|
||||
toolResults: number;
|
||||
errors: number;
|
||||
};
|
||||
tools: {
|
||||
totalCalls: number;
|
||||
uniqueTools: number;
|
||||
tools: Array<{ name: string; count: number }>;
|
||||
};
|
||||
byModel: Array<{
|
||||
provider?: string;
|
||||
model?: string;
|
||||
count: number;
|
||||
totals: SessionsUsageTotals;
|
||||
}>;
|
||||
byProvider: Array<{
|
||||
provider?: string;
|
||||
model?: string;
|
||||
count: number;
|
||||
totals: SessionsUsageTotals;
|
||||
}>;
|
||||
byAgent: Array<{ agentId: string; totals: SessionsUsageTotals }>;
|
||||
byChannel: Array<{ channel: string; totals: SessionsUsageTotals }>;
|
||||
latency?: {
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
};
|
||||
dailyLatency?: Array<{
|
||||
date: string;
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
}>;
|
||||
modelDaily?: Array<{
|
||||
date: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
count: number;
|
||||
}>;
|
||||
daily: Array<{
|
||||
date: string;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
messages: number;
|
||||
toolCalls: number;
|
||||
errors: number;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type CostUsageDailyEntry = SessionsUsageTotals & { date: string };
|
||||
|
||||
export type CostUsageSummary = {
|
||||
updatedAt: number;
|
||||
days: number;
|
||||
daily: CostUsageDailyEntry[];
|
||||
totals: SessionsUsageTotals;
|
||||
};
|
||||
|
||||
export type SessionUsageTimePoint = {
|
||||
timestamp: number;
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
cost: number;
|
||||
cumulativeTokens: number;
|
||||
cumulativeCost: number;
|
||||
};
|
||||
|
||||
export type SessionUsageTimeSeries = {
|
||||
sessionId?: string;
|
||||
points: SessionUsageTimePoint[];
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { SkillStatusEntry, SkillStatusReport } from "../types.ts";
|
||||
import type { SkillGroup } from "./skills-grouping.ts";
|
||||
import { normalizeToolName } from "../../../../src/agents/tool-policy.js";
|
||||
import {
|
||||
isAllowedByPolicy,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
resolveToolProfile,
|
||||
TOOL_SECTIONS,
|
||||
} from "./agents-utils.ts";
|
||||
import { groupSkills } from "./skills-grouping.ts";
|
||||
|
||||
export function renderAgentTools(params: {
|
||||
agentId: string;
|
||||
@@ -242,45 +244,6 @@ export function renderAgentTools(params: {
|
||||
`;
|
||||
}
|
||||
|
||||
type SkillGroup = {
|
||||
id: string;
|
||||
label: string;
|
||||
skills: SkillStatusEntry[];
|
||||
};
|
||||
|
||||
const SKILL_SOURCE_GROUPS: Array<{ id: string; label: string; sources: string[] }> = [
|
||||
{ id: "workspace", label: "Workspace Skills", sources: ["openclaw-workspace"] },
|
||||
{ id: "built-in", label: "Built-in Skills", sources: ["openclaw-bundled"] },
|
||||
{ id: "installed", label: "Installed Skills", sources: ["openclaw-managed"] },
|
||||
{ id: "extra", label: "Extra Skills", sources: ["openclaw-extra"] },
|
||||
];
|
||||
|
||||
function groupSkills(skills: SkillStatusEntry[]): SkillGroup[] {
|
||||
const groups = new Map<string, SkillGroup>();
|
||||
for (const def of SKILL_SOURCE_GROUPS) {
|
||||
groups.set(def.id, { id: def.id, label: def.label, skills: [] });
|
||||
}
|
||||
const builtInGroup = SKILL_SOURCE_GROUPS.find((group) => group.id === "built-in");
|
||||
const other: SkillGroup = { id: "other", label: "Other Skills", skills: [] };
|
||||
for (const skill of skills) {
|
||||
const match = skill.bundled
|
||||
? builtInGroup
|
||||
: SKILL_SOURCE_GROUPS.find((group) => group.sources.includes(skill.source));
|
||||
if (match) {
|
||||
groups.get(match.id)?.skills.push(skill);
|
||||
} else {
|
||||
other.skills.push(skill);
|
||||
}
|
||||
}
|
||||
const ordered = SKILL_SOURCE_GROUPS.map((group) => groups.get(group.id)).filter(
|
||||
(group): group is SkillGroup => Boolean(group && group.skills.length > 0),
|
||||
);
|
||||
if (other.skills.length > 0) {
|
||||
ordered.push(other);
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
export function renderAgentSkills(params: {
|
||||
agentId: string;
|
||||
report: SkillStatusReport | null;
|
||||
|
||||
40
ui/src/ui/views/skills-grouping.ts
Normal file
40
ui/src/ui/views/skills-grouping.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { SkillStatusEntry } from "../types.ts";
|
||||
|
||||
export type SkillGroup = {
|
||||
id: string;
|
||||
label: string;
|
||||
skills: SkillStatusEntry[];
|
||||
};
|
||||
|
||||
const SKILL_SOURCE_GROUPS: Array<{ id: string; label: string; sources: string[] }> = [
|
||||
{ id: "workspace", label: "Workspace Skills", sources: ["openclaw-workspace"] },
|
||||
{ id: "built-in", label: "Built-in Skills", sources: ["openclaw-bundled"] },
|
||||
{ id: "installed", label: "Installed Skills", sources: ["openclaw-managed"] },
|
||||
{ id: "extra", label: "Extra Skills", sources: ["openclaw-extra"] },
|
||||
];
|
||||
|
||||
export function groupSkills(skills: SkillStatusEntry[]): SkillGroup[] {
|
||||
const groups = new Map<string, SkillGroup>();
|
||||
for (const def of SKILL_SOURCE_GROUPS) {
|
||||
groups.set(def.id, { id: def.id, label: def.label, skills: [] });
|
||||
}
|
||||
const builtInGroup = SKILL_SOURCE_GROUPS.find((group) => group.id === "built-in");
|
||||
const other: SkillGroup = { id: "other", label: "Other Skills", skills: [] };
|
||||
for (const skill of skills) {
|
||||
const match = skill.bundled
|
||||
? builtInGroup
|
||||
: SKILL_SOURCE_GROUPS.find((group) => group.sources.includes(skill.source));
|
||||
if (match) {
|
||||
groups.get(match.id)?.skills.push(skill);
|
||||
} else {
|
||||
other.skills.push(skill);
|
||||
}
|
||||
}
|
||||
const ordered = SKILL_SOURCE_GROUPS.map((group) => groups.get(group.id)).filter(
|
||||
(group): group is SkillGroup => Boolean(group && group.skills.length > 0),
|
||||
);
|
||||
if (other.skills.length > 0) {
|
||||
ordered.push(other);
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
@@ -2,45 +2,7 @@ import { html, nothing } from "lit";
|
||||
import type { SkillMessageMap } from "../controllers/skills.ts";
|
||||
import type { SkillStatusEntry, SkillStatusReport } from "../types.ts";
|
||||
import { clampText } from "../format.ts";
|
||||
|
||||
type SkillGroup = {
|
||||
id: string;
|
||||
label: string;
|
||||
skills: SkillStatusEntry[];
|
||||
};
|
||||
|
||||
const SKILL_SOURCE_GROUPS: Array<{ id: string; label: string; sources: string[] }> = [
|
||||
{ id: "workspace", label: "Workspace Skills", sources: ["openclaw-workspace"] },
|
||||
{ id: "built-in", label: "Built-in Skills", sources: ["openclaw-bundled"] },
|
||||
{ id: "installed", label: "Installed Skills", sources: ["openclaw-managed"] },
|
||||
{ id: "extra", label: "Extra Skills", sources: ["openclaw-extra"] },
|
||||
];
|
||||
|
||||
function groupSkills(skills: SkillStatusEntry[]): SkillGroup[] {
|
||||
const groups = new Map<string, SkillGroup>();
|
||||
for (const def of SKILL_SOURCE_GROUPS) {
|
||||
groups.set(def.id, { id: def.id, label: def.label, skills: [] });
|
||||
}
|
||||
const builtInGroup = SKILL_SOURCE_GROUPS.find((group) => group.id === "built-in");
|
||||
const other: SkillGroup = { id: "other", label: "Other Skills", skills: [] };
|
||||
for (const skill of skills) {
|
||||
const match = skill.bundled
|
||||
? builtInGroup
|
||||
: SKILL_SOURCE_GROUPS.find((group) => group.sources.includes(skill.source));
|
||||
if (match) {
|
||||
groups.get(match.id)?.skills.push(skill);
|
||||
} else {
|
||||
other.skills.push(skill);
|
||||
}
|
||||
}
|
||||
const ordered = SKILL_SOURCE_GROUPS.map((group) => groups.get(group.id)).filter(
|
||||
(group): group is SkillGroup => Boolean(group && group.skills.length > 0),
|
||||
);
|
||||
if (other.skills.length > 0) {
|
||||
ordered.push(other);
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
import { groupSkills } from "./skills-grouping.ts";
|
||||
|
||||
export type SkillsProps = {
|
||||
loading: boolean;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { html } from "lit";
|
||||
import { buildUsageAggregateTail } from "../../../../src/shared/usage-aggregates.js";
|
||||
import { UsageSessionEntry, UsageTotals, UsageAggregates } from "./usageTypes.ts";
|
||||
|
||||
const CHARS_PER_TOKEN = 4;
|
||||
@@ -494,6 +495,14 @@ const buildAggregatesFromSessions = (
|
||||
}
|
||||
}
|
||||
|
||||
const tail = buildUsageAggregateTail({
|
||||
byChannelMap: channelMap,
|
||||
latencyTotals,
|
||||
dailyLatencyMap,
|
||||
modelDailyMap,
|
||||
dailyMap,
|
||||
});
|
||||
|
||||
return {
|
||||
messages,
|
||||
tools: {
|
||||
@@ -512,33 +521,7 @@ const buildAggregatesFromSessions = (
|
||||
byAgent: Array.from(agentMap.entries())
|
||||
.map(([agentId, totals]) => ({ agentId, totals }))
|
||||
.toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
|
||||
byChannel: Array.from(channelMap.entries())
|
||||
.map(([channel, totals]) => ({ channel, totals }))
|
||||
.toSorted((a, b) => b.totals.totalCost - a.totals.totalCost),
|
||||
latency:
|
||||
latencyTotals.count > 0
|
||||
? {
|
||||
count: latencyTotals.count,
|
||||
avgMs: latencyTotals.sum / latencyTotals.count,
|
||||
minMs: latencyTotals.min === Number.POSITIVE_INFINITY ? 0 : latencyTotals.min,
|
||||
maxMs: latencyTotals.max,
|
||||
p95Ms: latencyTotals.p95Max,
|
||||
}
|
||||
: undefined,
|
||||
dailyLatency: Array.from(dailyLatencyMap.values())
|
||||
.map((entry) => ({
|
||||
date: entry.date,
|
||||
count: entry.count,
|
||||
avgMs: entry.count ? entry.sum / entry.count : 0,
|
||||
minMs: entry.min === Number.POSITIVE_INFINITY ? 0 : entry.min,
|
||||
maxMs: entry.max,
|
||||
p95Ms: entry.p95Max,
|
||||
}))
|
||||
.toSorted((a, b) => a.date.localeCompare(b.date)),
|
||||
modelDaily: Array.from(modelDailyMap.values()).toSorted(
|
||||
(a, b) => a.date.localeCompare(b.date) || b.cost - a.cost,
|
||||
),
|
||||
daily: Array.from(dailyMap.values()).toSorted((a, b) => a.date.localeCompare(b.date)),
|
||||
...tail,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,188 +1,15 @@
|
||||
export type UsageSessionEntry = {
|
||||
key: string;
|
||||
label?: string;
|
||||
sessionId?: string;
|
||||
updatedAt?: number;
|
||||
agentId?: string;
|
||||
channel?: string;
|
||||
chatType?: string;
|
||||
origin?: {
|
||||
label?: string;
|
||||
provider?: string;
|
||||
surface?: string;
|
||||
chatType?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
accountId?: string;
|
||||
threadId?: string | number;
|
||||
};
|
||||
modelOverride?: string;
|
||||
providerOverride?: string;
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
usage: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
totalCost: number;
|
||||
inputCost?: number;
|
||||
outputCost?: number;
|
||||
cacheReadCost?: number;
|
||||
cacheWriteCost?: number;
|
||||
missingCostEntries: number;
|
||||
firstActivity?: number;
|
||||
lastActivity?: number;
|
||||
durationMs?: number;
|
||||
activityDates?: string[]; // YYYY-MM-DD dates when session had activity
|
||||
dailyBreakdown?: Array<{ date: string; tokens: number; cost: number }>; // Per-day breakdown
|
||||
dailyMessageCounts?: Array<{
|
||||
date: string;
|
||||
total: number;
|
||||
user: number;
|
||||
assistant: number;
|
||||
toolCalls: number;
|
||||
toolResults: number;
|
||||
errors: number;
|
||||
}>;
|
||||
dailyLatency?: Array<{
|
||||
date: string;
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
}>;
|
||||
dailyModelUsage?: Array<{
|
||||
date: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
count: number;
|
||||
}>;
|
||||
messageCounts?: {
|
||||
total: number;
|
||||
user: number;
|
||||
assistant: number;
|
||||
toolCalls: number;
|
||||
toolResults: number;
|
||||
errors: number;
|
||||
};
|
||||
toolUsage?: {
|
||||
totalCalls: number;
|
||||
uniqueTools: number;
|
||||
tools: Array<{ name: string; count: number }>;
|
||||
};
|
||||
modelUsage?: Array<{
|
||||
provider?: string;
|
||||
model?: string;
|
||||
count: number;
|
||||
totals: UsageTotals;
|
||||
}>;
|
||||
latency?: {
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
};
|
||||
} | null;
|
||||
contextWeight?: {
|
||||
systemPrompt: { chars: number; projectContextChars: number; nonProjectContextChars: number };
|
||||
skills: { promptChars: number; entries: Array<{ name: string; blockChars: number }> };
|
||||
tools: {
|
||||
listChars: number;
|
||||
schemaChars: number;
|
||||
entries: Array<{ name: string; summaryChars: number; schemaChars: number }>;
|
||||
};
|
||||
injectedWorkspaceFiles: Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
rawChars: number;
|
||||
injectedChars: number;
|
||||
truncated: boolean;
|
||||
}>;
|
||||
} | null;
|
||||
};
|
||||
import type {
|
||||
CostUsageDailyEntry,
|
||||
SessionsUsageEntry,
|
||||
SessionsUsageResult,
|
||||
SessionsUsageTotals,
|
||||
SessionUsageTimePoint,
|
||||
} from "../usage-types.ts";
|
||||
|
||||
export type UsageTotals = {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
totalCost: number;
|
||||
inputCost: number;
|
||||
outputCost: number;
|
||||
cacheReadCost: number;
|
||||
cacheWriteCost: number;
|
||||
missingCostEntries: number;
|
||||
};
|
||||
|
||||
export type CostDailyEntry = UsageTotals & { date: string };
|
||||
|
||||
export type UsageAggregates = {
|
||||
messages: {
|
||||
total: number;
|
||||
user: number;
|
||||
assistant: number;
|
||||
toolCalls: number;
|
||||
toolResults: number;
|
||||
errors: number;
|
||||
};
|
||||
tools: {
|
||||
totalCalls: number;
|
||||
uniqueTools: number;
|
||||
tools: Array<{ name: string; count: number }>;
|
||||
};
|
||||
byModel: Array<{
|
||||
provider?: string;
|
||||
model?: string;
|
||||
count: number;
|
||||
totals: UsageTotals;
|
||||
}>;
|
||||
byProvider: Array<{
|
||||
provider?: string;
|
||||
model?: string;
|
||||
count: number;
|
||||
totals: UsageTotals;
|
||||
}>;
|
||||
byAgent: Array<{ agentId: string; totals: UsageTotals }>;
|
||||
byChannel: Array<{ channel: string; totals: UsageTotals }>;
|
||||
latency?: {
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
};
|
||||
dailyLatency?: Array<{
|
||||
date: string;
|
||||
count: number;
|
||||
avgMs: number;
|
||||
p95Ms: number;
|
||||
minMs: number;
|
||||
maxMs: number;
|
||||
}>;
|
||||
modelDaily?: Array<{
|
||||
date: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
count: number;
|
||||
}>;
|
||||
daily: Array<{
|
||||
date: string;
|
||||
tokens: number;
|
||||
cost: number;
|
||||
messages: number;
|
||||
toolCalls: number;
|
||||
errors: number;
|
||||
}>;
|
||||
};
|
||||
export type UsageSessionEntry = SessionsUsageEntry;
|
||||
export type UsageTotals = SessionsUsageTotals;
|
||||
export type CostDailyEntry = CostUsageDailyEntry;
|
||||
export type UsageAggregates = SessionsUsageResult["aggregates"];
|
||||
|
||||
export type UsageColumnId =
|
||||
| "channel"
|
||||
@@ -194,17 +21,7 @@ export type UsageColumnId =
|
||||
| "errors"
|
||||
| "duration";
|
||||
|
||||
export type TimeSeriesPoint = {
|
||||
timestamp: number;
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
cost: number;
|
||||
cumulativeTokens: number;
|
||||
cumulativeCost: number;
|
||||
};
|
||||
export type TimeSeriesPoint = SessionUsageTimePoint;
|
||||
|
||||
export type UsageProps = {
|
||||
loading: boolean;
|
||||
|
||||
Reference in New Issue
Block a user