- Replace text-based regex parsing in InlineApproval with structured pendingApprovals extracted from request_execution tool results. The tool result text is deterministic (not LLM-generated), making UUID extraction reliable regardless of how the LLM rephrases the response. - Fix ToolCallGroup reactivity: wasActive = active captured initial value. Now uses (active) so re-runs on prop changes. - Extract approvals in both live streaming (done event) and history loading for consistent behavior on resumed sessions.
109 lines
4.5 KiB
Svelte
109 lines
4.5 KiB
Svelte
<script lang="ts">
|
|
import type { ToolCallResult } from '$lib/stores/chat'
|
|
import * as Collapsible from '$lib/components/ui/collapsible'
|
|
import WrenchIcon from '@lucide/svelte/icons/wrench'
|
|
import CheckIcon from '@lucide/svelte/icons/check'
|
|
import XIcon from '@lucide/svelte/icons/x'
|
|
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'
|
|
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
|
|
|
let { tools, active = false }: { tools: ToolCallResult[]; active?: boolean } = $props()
|
|
|
|
let open = $state(false)
|
|
let wasActive = $state(active)
|
|
|
|
$effect(() => {
|
|
if (active && !wasActive) {
|
|
open = true
|
|
}
|
|
if (!active && wasActive) {
|
|
open = false
|
|
}
|
|
wasActive = active
|
|
})
|
|
|
|
const doneCount = $derived(tools.filter((t) => t.type === 'tool_result').length)
|
|
const hasError = $derived(tools.some((t) => t.type === 'tool_result' && t.error))
|
|
const names = $derived(tools.map((t) => t.name).join(', '))
|
|
|
|
const runningTool = $derived(
|
|
active ? tools.find((t) => t.type === 'tool_use') : undefined
|
|
)
|
|
|
|
const ariaLabel = $derived(
|
|
doneCount === tools.length
|
|
? `${tools.length} ${tools.length === 1 ? 'tool' : 'tools'} completed`
|
|
: `${doneCount}/${tools.length} ${tools.length === 1 ? 'tool' : 'tools'} done`
|
|
)
|
|
|
|
function toolSummary(args: unknown): string {
|
|
if (!args || typeof args !== 'object') return ''
|
|
return Object.entries(args as Record<string, unknown>)
|
|
.map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
.join(' ')
|
|
.slice(0, 80)
|
|
}
|
|
</script>
|
|
|
|
{#if tools.length}
|
|
<Collapsible.Root bind:open class="group w-fit max-w-full overflow-hidden rounded-lg border bg-card text-xs">
|
|
<Collapsible.Trigger class="flex w-full cursor-pointer select-none items-center gap-2 px-2.5 py-1.5 hover:bg-muted/50">
|
|
{#if active && doneCount < tools.length}
|
|
<LoaderCircleIcon class="size-3 shrink-0 animate-spin text-primary" />
|
|
{:else if hasError}
|
|
<XIcon class="size-3 shrink-0 text-destructive" />
|
|
{:else}
|
|
<CheckIcon class="size-3 shrink-0 text-success" />
|
|
{/if}
|
|
|
|
{#if active && doneCount < tools.length}
|
|
<span class="font-medium">{doneCount}/{tools.length}</span>
|
|
{#if runningTool}
|
|
<span class="max-w-48 truncate font-mono text-muted-foreground">
|
|
{runningTool.name}
|
|
<span class="animate-pulse">…</span>
|
|
</span>
|
|
{:else}
|
|
<span class="animate-pulse text-muted-foreground">working…</span>
|
|
{/if}
|
|
{:else}
|
|
<span class="font-medium">{tools.length} tool{tools.length === 1 ? '' : 's'}</span>
|
|
<span class="max-w-48 truncate font-mono text-muted-foreground">{names}</span>
|
|
{/if}
|
|
|
|
<ChevronDownIcon
|
|
class="size-3 shrink-0 text-muted-foreground transition-transform duration-200 {open ? 'rotate-180' : ''}"
|
|
aria-hidden="true"
|
|
/>
|
|
</Collapsible.Trigger>
|
|
|
|
<Collapsible.Content class="overflow-hidden transition-all duration-200 ease-out data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:slide-out-to-top-2 data-[state=open]:animate-in data-[state=open]:fade-in data-[state=open]:slide-in-from-top-2">
|
|
<div class="flex flex-col divide-y border-t">
|
|
{#each tools as tool (tool.id)}
|
|
<div class="p-2">
|
|
<div class="flex items-center gap-2">
|
|
{#if tool.type === 'tool_result' && tool.error}
|
|
<XIcon class="size-3 shrink-0 text-destructive" />
|
|
{:else if tool.type === 'tool_result'}
|
|
<CheckIcon class="size-3 shrink-0 text-success" />
|
|
{:else}
|
|
<LoaderCircleIcon class="size-3 shrink-0 animate-spin text-primary" />
|
|
{/if}
|
|
<span class="font-mono font-medium">{tool.name}</span>
|
|
<span class="max-w-64 truncate text-muted-foreground">{toolSummary(tool.args)}</span>
|
|
</div>
|
|
<div class="mt-1 max-h-48 overflow-y-auto rounded bg-background/60 p-2">
|
|
{#if tool.args}
|
|
<pre class="whitespace-pre-wrap break-all font-mono text-[11px] text-muted-foreground">{JSON.stringify(tool.args, null, 2)}</pre>
|
|
{/if}
|
|
{#if tool.type === 'tool_result'}
|
|
<pre class="mt-1 whitespace-pre-wrap break-all font-mono text-[11px] {tool.error ? 'text-destructive' : ''}">{tool.error ?? JSON.stringify(tool.result, null, 2)}</pre>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</Collapsible.Content>
|
|
</Collapsible.Root>
|
|
{/if}
|