feat(web): collapse chat tool calls into one agent trace, reverse the activity rail
The chat rendered one card per tool call, so a 20-call turn buried the answer under 20 stacked cards. Merge them with the "thinking" indicator into a single collapsible strip above the answer: - collapsed: the live activity while running, a count once finished - expanded: the turn's work in humanized language (reuses the activity log's toolActivityLabel, so ten identical "run · target: host:strong" rows now read as what they actually did) - per row: the raw args/result, one more click in Also flip the Activity rail to newest-first with the current step on top: - follow-mode/auto-scroll re-anchored to the top to match, or it would jump to the oldest entry on every new event - pending plan steps park at the tail rather than sorting above the running step and pushing it off the top; the goal anchors the bottom Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Check, ChevronRight, Loader2, Wrench, X } from '@lucide/svelte'
|
||||
// One tool call inside AgentTrace's expanded list. Renders as a borderless
|
||||
// row (the trace supplies the container/border) whose own click reveals the
|
||||
// raw args/result — so the trace stays a readable thinking log by default
|
||||
// and the JSON is one more click away, not stacked inline.
|
||||
import { Check, ChevronRight, Loader2, X } from '@lucide/svelte'
|
||||
import type { ToolCallResult } from '$lib/types'
|
||||
import { toolActivityLabel } from '$lib/stores/activity'
|
||||
|
||||
let { tool }: { tool: ToolCallResult } = $props()
|
||||
let expanded = $state(false)
|
||||
@@ -11,11 +16,7 @@
|
||||
return 'done'
|
||||
})
|
||||
|
||||
const statusColor = $derived.by(() => {
|
||||
if (status === 'running') return 'text-primary'
|
||||
if (status === 'error') return 'text-destructive'
|
||||
return 'text-primary'
|
||||
})
|
||||
const label = $derived(toolActivityLabel(tool))
|
||||
|
||||
const argsSummary = $derived.by(() => {
|
||||
if (!tool.args) return ''
|
||||
@@ -25,61 +26,62 @@
|
||||
const val = typeof first[1] === 'string' ? first[1] : JSON.stringify(first[1])
|
||||
return `${first[0]}: ${val.length > 60 ? val.slice(0, 60) + '…' : val}`
|
||||
})
|
||||
|
||||
const hasDetail = $derived(
|
||||
!!tool.args || (tool.result !== undefined && tool.result !== null) || !!tool.error
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="tool-card rounded-lg border border-border/60 bg-card/40 overflow-hidden transition-all">
|
||||
<div class="tool-row">
|
||||
<button
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/40 transition-colors"
|
||||
class="flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-muted/40 disabled:cursor-default"
|
||||
onclick={() => (expanded = !expanded)}
|
||||
aria-expanded={expanded}
|
||||
disabled={!hasDetail}
|
||||
>
|
||||
<ChevronRight class="size-3 shrink-0 text-muted-foreground transition-transform {expanded ? 'rotate-90' : ''}" />
|
||||
<Wrench class="size-3.5 shrink-0 text-muted-foreground" />
|
||||
<span class="font-mono text-xs font-medium text-foreground/80">{tool.name}</span>
|
||||
{#if argsSummary}
|
||||
<span class="ml-1 truncate text-[11px] text-muted-foreground/70">{argsSummary}</span>
|
||||
{/if}
|
||||
<span class="ml-auto shrink-0 {statusColor}">
|
||||
<span class="mt-px shrink-0 {status === 'error' ? 'text-destructive' : 'text-primary'}">
|
||||
{#if status === 'running'}
|
||||
<Loader2 class="size-3.5 animate-spin" />
|
||||
<Loader2 class="size-3 animate-spin" />
|
||||
{:else if status === 'error'}
|
||||
<X class="size-3.5" />
|
||||
<X class="size-3" />
|
||||
{:else}
|
||||
<Check class="size-3.5" />
|
||||
<Check class="size-3" />
|
||||
{/if}
|
||||
</span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-xs text-foreground/90">{label}</span>
|
||||
{#if argsSummary}
|
||||
<span class="block truncate font-mono text-[10px] text-muted-foreground/60">{argsSummary}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="shrink-0 font-mono text-[10px] text-muted-foreground/50">{tool.name}</span>
|
||||
{#if hasDetail}
|
||||
<ChevronRight
|
||||
class="mt-px size-3 shrink-0 text-muted-foreground/50 transition-transform {expanded ? 'rotate-90' : ''}"
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if expanded}
|
||||
<div class="border-t border-border/40 px-3 py-2 space-y-2">
|
||||
<div class="space-y-2 px-2 pb-2 pl-7">
|
||||
{#if tool.args}
|
||||
<div>
|
||||
<div class="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mb-1">Args</div>
|
||||
<pre class="tool-pre rounded-md bg-muted/60 p-2 text-[11px] overflow-x-auto max-h-48">{JSON.stringify(tool.args, null, 2)}</pre>
|
||||
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Args</div>
|
||||
<pre class="max-h-48 overflow-x-auto rounded-md bg-muted/60 p-2 text-[11px]">{JSON.stringify(tool.args, null, 2)}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
{#if tool.result !== undefined && tool.result !== null}
|
||||
<div>
|
||||
<div class="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mb-1">Result</div>
|
||||
<pre class="tool-pre rounded-md bg-muted/60 p-2 text-[11px] overflow-x-auto max-h-48">{JSON.stringify(tool.result, null, 2)}</pre>
|
||||
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Result</div>
|
||||
<pre class="max-h-48 overflow-x-auto rounded-md bg-muted/60 p-2 text-[11px]">{JSON.stringify(tool.result, null, 2)}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
{#if tool.error}
|
||||
<div>
|
||||
<div class="text-[10px] font-semibold uppercase tracking-wider text-destructive mb-1">Error</div>
|
||||
<pre class="tool-pre rounded-md bg-destructive/5 border border-destructive/20 p-2 text-[11px] text-destructive overflow-x-auto max-h-48">{tool.error}</pre>
|
||||
<div class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-destructive">Error</div>
|
||||
<pre class="max-h-48 overflow-x-auto rounded-md border border-destructive/20 bg-destructive/5 p-2 text-[11px] text-destructive">{tool.error}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tool-card {
|
||||
animation: tool-in 0.2s ease-out;
|
||||
}
|
||||
@keyframes tool-in {
|
||||
from { opacity: 0; transform: translateY(-2px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user