fix: structured approvals + ToolCallGroup reactivity
- 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.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { PendingApproval } from '$lib/stores/chat'
|
||||
import { decideApproval } from '$lib/api'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import CheckIcon from '@lucide/svelte/icons/check'
|
||||
@@ -6,62 +7,57 @@
|
||||
import ShieldCheckIcon from '@lucide/svelte/icons/shield-check'
|
||||
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
||||
|
||||
let { text }: { text: string } = $props()
|
||||
|
||||
const RE = /\bexec[uecution]*\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/i
|
||||
const match = $derived(text.match(RE))
|
||||
let { approvals }: { approvals: PendingApproval[] } = $props()
|
||||
|
||||
let pending = $state(false)
|
||||
let done = $state<'approved' | 'denied' | null>(null)
|
||||
let done = $state<string | null>(null)
|
||||
let doneId = $state('')
|
||||
|
||||
async function approve() {
|
||||
if (!match) return
|
||||
async function decide(approval: PendingApproval, decision: 'approve' | 'deny') {
|
||||
pending = true
|
||||
const result = await decideApproval(match[1], 'approve')
|
||||
doneId = approval.executionId
|
||||
const result = await decideApproval(approval.executionId, decision)
|
||||
pending = false
|
||||
done = result ? 'approved' : 'denied'
|
||||
}
|
||||
|
||||
async function deny() {
|
||||
if (!match) return
|
||||
pending = true
|
||||
const result = await decideApproval(match[1], 'deny')
|
||||
pending = false
|
||||
done = result ? 'denied' : 'denied'
|
||||
done = result ? decision : 'failed'
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
done = null
|
||||
doneId = ''
|
||||
pending = false
|
||||
void text
|
||||
void approvals
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if match && !done}
|
||||
<div class="my-2 flex items-center gap-2 rounded-lg border border-warning/40 bg-warning/5 px-3 py-2">
|
||||
<ShieldCheckIcon class="size-4 shrink-0 text-warning" />
|
||||
<span class="flex-1 text-xs text-muted-foreground">This action requires approval</span>
|
||||
{#if pending}
|
||||
<LoaderCircleIcon class="size-4 animate-spin text-muted-foreground" />
|
||||
{:else}
|
||||
<Button size="sm" variant="default" class="h-7 px-2.5 text-xs" onclick={approve}>
|
||||
<CheckIcon class="size-3" />
|
||||
<span class="ml-1">Approve</span>
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" class="h-7 px-2.5 text-xs" onclick={deny}>
|
||||
<XIcon class="size-3" />
|
||||
<span class="ml-1">Deny</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if done}
|
||||
<div class="my-2 flex items-center gap-2 rounded-lg border px-3 py-2 text-xs {done === 'approved' ? 'border-success/40 bg-success/5 text-success' : 'border-destructive/40 bg-destructive/5 text-destructive'}">
|
||||
{#if done === 'approved'}
|
||||
{#each approvals.filter(a => !done || a.executionId !== doneId) as approval (approval.executionId)}
|
||||
{#if !done || approval.executionId !== doneId}
|
||||
<div class="my-2 flex items-center gap-2 rounded-lg border border-warning/40 bg-warning/5 px-3 py-2">
|
||||
<ShieldCheckIcon class="size-4 shrink-0 text-warning" />
|
||||
<span class="flex-1 text-xs text-muted-foreground">
|
||||
{approval.action} on {approval.target} requires approval
|
||||
</span>
|
||||
{#if pending && doneId === approval.executionId}
|
||||
<LoaderCircleIcon class="size-4 animate-spin text-muted-foreground" />
|
||||
{:else}
|
||||
<Button size="sm" variant="default" class="h-7 px-2.5 text-xs" onclick={() => decide(approval, 'approve')}>
|
||||
<CheckIcon class="size-3" />
|
||||
<span class="ml-1">Approve</span>
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" class="h-7 px-2.5 text-xs" onclick={() => decide(approval, 'deny')}>
|
||||
<XIcon class="size-3" />
|
||||
<span class="ml-1">Deny</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if done === 'approved'}
|
||||
<div class="my-2 flex items-center gap-2 rounded-lg border border-success/40 bg-success/5 px-3 py-2 text-xs text-success">
|
||||
<CheckIcon class="size-4" />
|
||||
<span>Approved. The action is running.</span>
|
||||
{:else}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="my-2 flex items-center gap-2 rounded-lg border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive">
|
||||
<XIcon class="size-4" />
|
||||
<span>Denied.</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
Reference in New Issue
Block a user