Skip to content
Open
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
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/001-stabilize-agent-startup"
"feature_directory": "specs/002-rancher-usage-panel"
}
56 changes: 32 additions & 24 deletions admin/slices/agent/agent/components/agent/chat/Tab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ const emit = defineEmits<{ restart: []; toggleRunning: [] }>();

const authStore = useAuthStore();

// Side-by-side logs panel — the only logs surface on this page. Open by
// default; closing collapses it into a page-level "Logs" button next to the
// chat's top-right corner (the chat widget itself stays uniform across hosts
// — no host-specific controls in its header). The panel is only mounted
// while open, so its 5s polling stops the moment it's collapsed.
// Side stack next to the chat: logs on top, usage panel below, each
// independently collapsible into a compact button. Logs stay the primary
// operational surface (open by default); collapsing one hands its height to
// the other via flex. The logs panel is only mounted while open, so its 5s
// polling stops the moment it's collapsed. The usage panel manages its own
// collapsed state (`collapsible` prop).
const showSideLogs = ref(true);

// One "restart is underway" signal for every surface (bridle header status,
Expand Down Expand Up @@ -150,28 +151,35 @@ watch(
</Transition>
</div>
<div
v-if="showSideLogs"
class="h-[calc(100vh-15.5rem)] min-h-120 w-full max-w-200 basis-1/2 overflow-hidden"
class="flex h-[calc(100vh-15.5rem)] min-h-120 w-full max-w-200 basis-1/2 flex-col gap-3"
>
<AgentLogsPanel
<div v-if="showSideLogs" class="min-h-0 flex-1 overflow-hidden">
<AgentLogsPanel
:agent-id="agent.id"
closable
:restarting="restartUnderway"
:first-start="agent.launchContext === 'initial'"
class="h-full min-w-100"
@close="showSideLogs = false"
/>
</div>
<Button
v-else
variant="outline"
size="sm"
class="self-start"
title="Show pod logs"
@click="showSideLogs = true"
>
<IconFileText class="size-4" />
Logs
</Button>

<UsagePanel
:agent-id="agent.id"
closable
:restarting="restartUnderway"
:first-start="agent.launchContext === 'initial'"
class="h-full min-w-100"
@close="showSideLogs = false"
collapsible
class="min-h-0 flex-1"
/>
</div>
<Button
v-else
variant="outline"
size="sm"
class="self-start"
title="Show pod logs"
@click="showSideLogs = true"
>
<IconFileText class="size-4" />
Logs
</Button>
</div>
</template>
69 changes: 4 additions & 65 deletions admin/slices/agent/agent/components/agent/overview/UsageCard.vue
Original file line number Diff line number Diff line change
@@ -1,70 +1,9 @@
<script setup lang="ts">
const props = defineProps<{ agentId: string }>();

const usageStore = useUsageStore();

const { data: usage, pending } = useAsyncData(
`admin-agent-usage-${props.agentId}`,
() => usageStore.fetchForAgent(props.agentId),
{ lazy: true },
);
// Thin wrapper: the Overview tab renders the same shared usage panel as the
// chat tab's side stack and the Rancher page, so the surfaces cannot drift.
defineProps<{ agentId: string }>();
</script>

<template>
<Card>
<CardHeader>
<CardTitle>Usage</CardTitle>
<CardDescription>
Tokens reported by the agent runtime (<code>usage.json</code>).
Today first, then 30-day totals across all models.
</CardDescription>
</CardHeader>
<CardContent>
<div v-if="pending && !usage" class="grid grid-cols-1 gap-4 sm:grid-cols-4">
<div v-for="i in 8" :key="i" class="space-y-2">
<Skeleton class="h-3 w-20" />
<Skeleton class="h-4 w-24" />
</div>
</div>
<div v-else-if="!usage || usage.totals.callCount === 0" class="text-sm text-muted-foreground">
No usage reported yet.
</div>
<dl v-else class="grid grid-cols-1 gap-4 sm:grid-cols-4">
<div>
<dt class="text-xs text-muted-foreground">Today · model</dt>
<dd class="mt-1 font-mono text-sm">{{ usage.today.model ?? '—' }}</dd>
</div>
<div>
<dt class="text-xs text-muted-foreground">Today · in / out</dt>
<dd class="mt-1 font-mono text-sm">
{{ formatCount(usage.today.inputTokens) }} / {{ formatCount(usage.today.outputTokens) }}
</dd>
</div>
<div>
<dt class="text-xs text-muted-foreground">Today · calls</dt>
<dd class="mt-1 font-mono text-sm">{{ usage.today.callCount }}</dd>
</div>
<div>
<dt class="text-xs text-muted-foreground">30d · cost</dt>
<dd class="mt-1 font-mono text-sm">{{ formatUsd(usage.totals.costUsd) }}</dd>
</div>
<div>
<dt class="text-xs text-muted-foreground">30d · top model</dt>
<dd class="mt-1 font-mono text-sm">{{ usage.topModel ?? '—' }}</dd>
</div>
<div>
<dt class="text-xs text-muted-foreground">30d · input</dt>
<dd class="mt-1 font-mono text-sm">{{ formatCount(usage.totals.inputTokens) }}</dd>
</div>
<div>
<dt class="text-xs text-muted-foreground">30d · output</dt>
<dd class="mt-1 font-mono text-sm">{{ formatCount(usage.totals.outputTokens) }}</dd>
</div>
<div>
<dt class="text-xs text-muted-foreground">30d · calls</dt>
<dd class="mt-1 font-mono text-sm">{{ usage.totals.callCount }}</dd>
</div>
</dl>
</CardContent>
</Card>
<UsagePanel :agent-id="agentId" />
</template>
Loading
Loading