feat: pct_create action, ToolCallGroup collapse+animation, inline chat approval
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

- Add pct_create to request_execution (MCP) and executeApprovedAction (httpapi)
  Parses JSON config: vmid, hostname, cores, memory, disk_gb, ip, gw, storage,
  template, privileged, nesting, mounts, nameserver, searchdomain. Creates
  entity (state=provisioning), hosts relationship, entity_status on success.
  Fixes action string parsing to use Index instead of SplitN (colons in JSON).

- Rewrite ToolCallGroup.svelte: bits-ui Collapsible replaces native <details>.
  Collapsed by default. Animated header shows live tool count + running tool
  name while streaming. Auto-expands during streaming, auto-collapses on done.

- Add InlineApproval component: parses 'execution UUID queued' from agent
  response, renders Approve/Deny buttons inline in chat, calls decideApproval.

- Document pct_create in nomos/SOUL.md with params, risk class, and approval flow.

- Add session-review skill at .agents/skills/session-review/SKILL.md.

- Add plan: 2026-07-09-session-execution-and-ux-fixes.md.
This commit is contained in:
2026-07-09 11:15:28 +02:00
parent 0d29b1db81
commit ea62d744ed
8 changed files with 531 additions and 54 deletions

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import { decideApproval } from '$lib/api'
import { Button } from '$lib/components/ui/button'
import CheckIcon from '@lucide/svelte/icons/check'
import XIcon from '@lucide/svelte/icons/x'
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 pending = $state(false)
let done = $state<'approved' | 'denied' | null>(null)
async function approve() {
if (!match) return
pending = true
const result = await decideApproval(match[1], 'approve')
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'
}
$effect(() => {
done = null
pending = false
void text
})
</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'}
<CheckIcon class="size-4" />
<span>Approved. The action is running.</span>
{:else}
<XIcon class="size-4" />
<span>Denied.</span>
{/if}
</div>
{/if}