Ops "Executions" tab showed raw target UUIDs, alphabetical (not
recency) order, and a stale status vocabulary from an earlier schema
iteration — never actually usable as a live "what's happening" view.
Replaced with a new recency-ordered /api/v1/activity/recent endpoint
and matching table (human-readable action summaries, risk/status
badges, duration, inline error preview).
Also added /api/v1/activity/session/{id} + a collapsible SessionDigest
panel in the chat rail, answering "what did this session actually do"
(executions by status, entities touched, knowledge written) — the
missing piece for proactive outcome reporting to be visible in the UI,
not just in the chat transcript.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
101 lines
3.7 KiB
Svelte
101 lines
3.7 KiB
Svelte
<script lang="ts">
|
||
import { fetchSessionDigest, type SessionDigest } from '$lib/api'
|
||
import { currentSession, streaming } from '$lib/stores/chat'
|
||
import { Badge } from '$lib/components/ui/badge'
|
||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'
|
||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
||
import SparklesIcon from '@lucide/svelte/icons/sparkles'
|
||
|
||
let digest = $state<SessionDigest | null>(null)
|
||
let open = $state(false)
|
||
let loadedFor = $state<string | null>(null)
|
||
|
||
// Reload the digest whenever the session changes or a stream finishes —
|
||
// "what did this session actually do" is only meaningful once executions
|
||
// have had a chance to land.
|
||
$effect(() => {
|
||
const sid = $currentSession
|
||
const busy = $streaming
|
||
if (!sid || busy) return
|
||
if (loadedFor === sid) return
|
||
loadedFor = sid
|
||
fetchSessionDigest(sid).then((d) => (digest = d))
|
||
})
|
||
|
||
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||
if (['failed', 'denied', 'revoked', 'cancelled'].includes(status)) return 'destructive'
|
||
if (status === 'completed') return 'default'
|
||
if (['running', 'approved'].includes(status)) return 'secondary'
|
||
return 'outline'
|
||
}
|
||
</script>
|
||
|
||
{#if digest && digest.total_executions > 0}
|
||
<div class="border-b">
|
||
<button
|
||
type="button"
|
||
class="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-xs font-medium hover:bg-muted/50"
|
||
onclick={() => (open = !open)}
|
||
>
|
||
<span class="flex items-center gap-1.5">
|
||
{#if open}<ChevronDownIcon class="size-3.5" />{:else}<ChevronRightIcon class="size-3.5" />{/if}
|
||
This session
|
||
</span>
|
||
<span class="flex items-center gap-1.5 text-muted-foreground">
|
||
{digest.total_executions} action{digest.total_executions === 1 ? '' : 's'}
|
||
{#if digest.knowledge_created.length}
|
||
<span class="flex items-center gap-0.5 text-primary">
|
||
<SparklesIcon class="size-3" />{digest.knowledge_created.length}
|
||
</span>
|
||
{/if}
|
||
</span>
|
||
</button>
|
||
|
||
{#if open}
|
||
<div class="flex flex-col gap-3 px-3 pb-3 text-xs">
|
||
<div class="flex flex-wrap gap-1">
|
||
{#each Object.entries(digest.by_status) as [status, count]}
|
||
<Badge variant={statusVariant(status)}>{status} × {count}</Badge>
|
||
{/each}
|
||
</div>
|
||
|
||
{#if digest.entities_touched.length}
|
||
<div>
|
||
<div class="mb-1 text-muted-foreground">Entities touched</div>
|
||
<div class="flex flex-wrap gap-1">
|
||
{#each digest.entities_touched as target}
|
||
<span class="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px]">{target}</span>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<div class="flex flex-col gap-1">
|
||
{#each digest.executions as ex}
|
||
<div class="flex items-start justify-between gap-2 rounded border px-2 py-1">
|
||
<div class="min-w-0">
|
||
<div class="font-mono text-[11px] text-muted-foreground">{ex.target}</div>
|
||
<div class="truncate">{ex.summary || ex.verb}</div>
|
||
</div>
|
||
<Badge variant={statusVariant(ex.status)} class="shrink-0">{ex.status}</Badge>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
|
||
{#if digest.knowledge_created.length}
|
||
<div>
|
||
<div class="mb-1 flex items-center gap-1 text-primary">
|
||
<SparklesIcon class="size-3" />Learned this session
|
||
</div>
|
||
<ul class="list-inside list-disc">
|
||
{#each digest.knowledge_created as title}
|
||
<li>{title}</li>
|
||
{/each}
|
||
</ul>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{/if}
|