fix: structured approvals + ToolCallGroup reactivity
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

- 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:
2026-07-09 11:35:30 +02:00
parent ea62d744ed
commit d9683cfe29
4 changed files with 88 additions and 52 deletions

View File

@@ -2,11 +2,38 @@ import { writable, get } from 'svelte/store'
import { streamChat, fetchSessions, fetchMessages, deleteSession as apiDeleteSession } from '$lib/api'
import type { ChatEvent, Session, Message } from '$lib/api'
export interface PendingApproval {
executionId: string
action: string
target: string
}
export interface ChatMessage {
id: string
role: 'user' | 'assistant'
text: string
tools: ToolCallResult[]
pendingApprovals: PendingApproval[]
}
const APPROVAL_RE = /execution\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
function extractApprovals(tools: ToolCallResult[]): PendingApproval[] {
const out: PendingApproval[] = []
for (const t of tools) {
if (t.name !== 'request_execution' || t.type !== 'tool_result') continue
const text = typeof t.result === 'string' ? t.result : JSON.stringify(t.result ?? '')
if (!text.includes('requires approval')) continue
const m = text.match(APPROVAL_RE)
if (m) {
out.push({
executionId: m[1],
action: t.args?.action ?? 'unknown',
target: t.args?.target ?? 'unknown'
})
}
}
return out
}
export interface ToolCallResult {
@@ -57,12 +84,16 @@ export async function loadSessionMessages(sessionId: string) {
currentSession.set(sessionId)
const msgs = await fetchMessages(sessionId)
sessionMessages.set(msgs)
const chatMsgs: ChatMessage[] = msgs.map((m) => ({
id: m.id,
role: m.role as 'user' | 'assistant',
text: m.content?.text ?? (typeof m.content === 'string' ? m.content : ''),
tools: mergeToolCalls(m.content?.tool_calls)
}))
const chatMsgs: ChatMessage[] = msgs.map((m) => {
const tools = mergeToolCalls(m.content?.tool_calls)
return {
id: m.id,
role: m.role as 'user' | 'assistant',
text: m.content?.text ?? (typeof m.content === 'string' ? m.content : ''),
tools,
pendingApprovals: extractApprovals(tools)
}
})
messages.set(chatMsgs)
}
@@ -74,7 +105,8 @@ export function sendMessage(text: string) {
id: mid(),
role: 'user',
text,
tools: []
tools: [],
pendingApprovals: []
}
messages.update((ms) => [...ms, userMsg])
@@ -82,7 +114,8 @@ export function sendMessage(text: string) {
id: mid(),
role: 'assistant',
text: '',
tools: []
tools: [],
pendingApprovals: []
}
messages.update((ms) => [...ms, assistantMsg])
@@ -147,6 +180,13 @@ export function sendMessage(text: string) {
return [...ms]
})
} else if (ev.type === 'done') {
messages.update((ms) => {
const last = ms[ms.length - 1]
if (last && last.role === 'assistant') {
last.pendingApprovals = extractApprovals(last.tools)
}
return [...ms]
})
currentSession.set(ev.data?.session_id ?? ev.session_id)
} else if (ev.type === 'error') {
error.set(ev.data)