- activityLog now detects 'requires approval' in tool results - Adds approval entries with shield icon + description + execution ID - Works for both run and remaining approval paths
110 lines
4.6 KiB
Svelte
110 lines
4.6 KiB
Svelte
<script lang="ts">
|
|
import { activityLog, type ActivityEntry } from '$lib/stores/activity'
|
|
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
|
import CircleCheckIcon from '@lucide/svelte/icons/circle-check'
|
|
import CircleXIcon from '@lucide/svelte/icons/circle-x'
|
|
import TargetIcon from '@lucide/svelte/icons/target'
|
|
import ListTodoIcon from '@lucide/svelte/icons/list-todo'
|
|
import SparklesIcon from '@lucide/svelte/icons/sparkles'
|
|
import HelpCircleIcon from '@lucide/svelte/icons/help-circle'
|
|
import FlagIcon from '@lucide/svelte/icons/flag'
|
|
import ShieldIcon from '@lucide/svelte/icons/shield'
|
|
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'
|
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
|
|
|
let expanded = $state(new Set<string>())
|
|
|
|
function toggle(id: string) {
|
|
if (expanded.has(id)) expanded.delete(id)
|
|
else expanded.add(id)
|
|
expanded = new Set(expanded)
|
|
}
|
|
|
|
function typeIcon(type: ActivityEntry['type']) {
|
|
switch (type) {
|
|
case 'goal': return TargetIcon
|
|
case 'plan': return ListTodoIcon
|
|
case 'step_running': case 'step_done': case 'step_failed':
|
|
case 'tool_running': case 'tool_done': case 'tool_error':
|
|
return null // use status icon instead
|
|
case 'knowledge': return SparklesIcon
|
|
case 'complete': return FlagIcon
|
|
case 'question': return HelpCircleIcon
|
|
case 'approval': return ShieldIcon
|
|
default: return null
|
|
}
|
|
}
|
|
|
|
function statusColor(status: ActivityEntry['status']) {
|
|
if (status === 'running') return 'text-primary'
|
|
if (status === 'failed') return 'text-destructive'
|
|
return 'text-success'
|
|
}
|
|
</script>
|
|
|
|
<div class="flex h-full flex-col">
|
|
<div class="flex-1 overflow-y-auto">
|
|
{#if $activityLog.length === 0}
|
|
<div class="flex flex-col items-center gap-2 px-3 py-6 text-center">
|
|
<div class="flex items-center gap-0.5">
|
|
<div class="size-1 rounded-full bg-muted-foreground/20 animate-pulse" style="animation-delay:0s" />
|
|
<div class="size-1 rounded-full bg-muted-foreground/30 animate-pulse" style="animation-delay:0.15s" />
|
|
<div class="size-1 rounded-full bg-muted-foreground/20 animate-pulse" style="animation-delay:0.3s" />
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Waiting for activity…</p>
|
|
</div>
|
|
{:else}
|
|
<div class="flex flex-col py-1">
|
|
{#each $activityLog as entry, i (entry.id)}
|
|
{@const isLast = i === $activityLog.length - 1}
|
|
{@const hasDetail = !!entry.detail}
|
|
{@const icon = typeIcon(entry.type)}
|
|
<div class="relative">
|
|
<!-- connector line -->
|
|
{#if !isLast}
|
|
<div class="absolute left-[17px] top-6 bottom-0 w-px bg-border"></div>
|
|
{/if}
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-start gap-2 {entry.indent ? 'pl-7' : 'px-3'} py-1.5 text-left text-xs hover:bg-muted/30 {hasDetail ? 'cursor-pointer' : 'cursor-default'}"
|
|
onclick={() => hasDetail && toggle(entry.id)}
|
|
>
|
|
<!-- status icon -->
|
|
<span class="relative mt-0.5 flex size-3.5 shrink-0 items-center justify-center rounded-full {statusColor(entry.status)}">
|
|
{#if entry.status === 'running'}
|
|
<LoaderCircleIcon class="size-3.5 animate-spin" />
|
|
{:else if entry.status === 'failed'}
|
|
<CircleXIcon class="size-3.5" />
|
|
{:else if icon}
|
|
<svelte:component this={icon} class="size-3" />
|
|
{:else}
|
|
<CircleCheckIcon class="size-3" />
|
|
{/if}
|
|
</span>
|
|
<!-- description -->
|
|
<span class="min-w-0 flex-1 leading-snug {entry.status === 'done' ? 'text-muted-foreground' : ''}">
|
|
{entry.description}
|
|
</span>
|
|
{#if hasDetail}
|
|
<span class="mt-0.5 shrink-0 text-muted-foreground">
|
|
{#if expanded.has(entry.id)}
|
|
<ChevronDownIcon class="size-3" />
|
|
{:else}
|
|
<ChevronRightIcon class="size-3" />
|
|
{/if}
|
|
</span>
|
|
{/if}
|
|
</button>
|
|
<!-- detail (collapsed) -->
|
|
{#if hasDetail && expanded.has(entry.id)}
|
|
<div class="pl-8 pr-3 pb-1">
|
|
<pre class="whitespace-pre-wrap break-all rounded bg-muted/50 p-2 font-mono text-[10px] text-muted-foreground">{entry.detail}</pre>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|