Files
hermes-ui/packages/client/src/components/hermes/usage/AgentBreakdown.vue
T
toor 8c1ddf415f Phase 003: remove the coding-agent and ekko-agent runtimes
Every chat session now runs through the Hermes bridge, so the three chat
chains become feature-equivalent instead of three partially-maintained
dispatch paths. Keeping the coding-agent and in-house agent runtimes alive
meant every bridge change had to be mirrored into two more runtimes that
had already drifted.

Historical rows are read, not migrated: sessions stored with the retired
source normalize to cli on load, workflow graphs naming the retired agents
keep loading and run on the bridge, and the visible-source check plus
SOURCE_LABELS still recognize the old value so those sessions stay listed.

The public surface follows the code: the OpenAPI generator and the MCP
chat-run tool schema drop the six retired entry fields so external callers
are not told about runtimes that no longer exist.

harness-check gains a RETIRED_RUNTIME scan so the residual check is a
mechanical gate instead of a manual grep, with the two surviving read-side
compat sites recorded with reasons.

Refs: docs/phases/phase_003.md
2026-10-16 13:27:50 +08:00

170 lines
4.3 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useUsageStore } from '@/stores/hermes/usage'
const { t } = useI18n()
const usageStore = useUsageStore()
const maxAgentTokens = computed(() => Math.max(...usageStore.agentUsage.map(agent => agent.visualTokens), 1))
const agentLabels: Record<string, string> = {
hermes: 'hermes',
}
function agentName(agent: string): string {
return t(`usage.agents.${agentLabels[agent] || 'unknown'}`)
}
function formatTokens(n: number): string {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'
if (n >= 1000) return (n / 1000).toFixed(1) + 'K'
return String(n)
}
function cacheHitRate(agent: { inputTokens: number; cacheTokens: number }): string {
const total = agent.inputTokens + agent.cacheTokens
if (total === 0) return '--'
return ((agent.cacheTokens / total) * 100).toFixed(1) + '%'
}
</script>
<template>
<div class="agent-breakdown">
<h3 class="section-title">{{ t('usage.agentBreakdown') }}</h3>
<div class="agent-legend" aria-label="Token type legend">
<div class="legend-item"><span class="legend-swatch input" />{{ t('usage.inputTokens') }}</div>
<div class="legend-item"><span class="legend-swatch output" />{{ t('usage.outputTokens') }}</div>
<div class="legend-item"><span class="legend-swatch cache" />{{ t('usage.cacheRead') }}</div>
</div>
<div class="agent-list">
<div v-for="agent in usageStore.agentUsage" :key="agent.agent" class="agent-row">
<span class="agent-swatch" :style="{ background: agent.color }" />
<span class="agent-name" :title="agentName(agent.agent)">{{ agentName(agent.agent) }}</span>
<div class="agent-bar-wrap">
<div class="agent-bar" :style="{ width: (agent.visualTokens / maxAgentTokens * 100) + '%' }">
<div v-if="agent.inputTokens > 0" class="agent-bar-segment input" :style="{ width: agent.inputPercent + '%' }" />
<div v-if="agent.outputTokens > 0" class="agent-bar-segment output" :style="{ width: agent.outputPercent + '%' }" />
<div v-if="agent.cacheTokens > 0" class="agent-bar-segment cache" :style="{ width: agent.cachePercent + '%' }" />
</div>
</div>
<span
class="agent-tokens"
:title="`${t('usage.inputTokens')}: ${formatTokens(agent.inputTokens)} · ${t('usage.outputTokens')}: ${formatTokens(agent.outputTokens)} · ${t('usage.cacheRead')}: ${formatTokens(agent.cacheTokens)} · ${t('usage.cacheHitRate')}: ${cacheHitRate(agent)}`"
>
{{ formatTokens(agent.totalTokens) }}
<small v-if="agent.cacheTokens > 0">+{{ formatTokens(agent.cacheTokens) }}</small>
</span>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@use '@/styles/variables' as *;
.agent-breakdown {
background: $bg-card;
border: 1px solid $border-color;
border-radius: $radius-md;
padding: 16px;
margin-bottom: 20px;
}
.section-title {
font-size: 13px;
font-weight: 600;
color: $text-secondary;
margin: 0 0 12px;
}
.agent-legend {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
margin: 0 0 12px;
color: $text-muted;
font-size: 11px;
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 5px;
}
.legend-swatch,
.agent-swatch {
width: 8px;
height: 8px;
border-radius: 2px;
flex-shrink: 0;
}
.legend-swatch.input,
.agent-bar-segment.input { background: #5c6bc0; }
.legend-swatch.output,
.agent-bar-segment.output { background: #26a69a; }
.legend-swatch.cache,
.agent-bar-segment.cache { background: #f6ad55; }
.agent-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.agent-row {
display: flex;
align-items: center;
gap: 10px;
}
.agent-name {
font-size: 12px;
color: $text-secondary;
width: 140px;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-bar-wrap {
flex: 1;
height: 16px;
background: $bg-secondary;
border-radius: 3px;
overflow: hidden;
}
.agent-bar {
height: 100%;
border-radius: 3px;
min-width: 2px;
transition: width 0.3s ease;
display: flex;
overflow: hidden;
}
.agent-bar-segment {
height: 100%;
min-width: 0;
}
.agent-tokens {
font-size: 12px;
color: $text-muted;
width: 86px;
text-align: right;
flex-shrink: 0;
small {
color: #f6ad55;
margin-left: 4px;
font-size: 10px;
}
}
</style>