From 2da437914efe7ee62e1334614f1dd569f00a5797 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 9 Jun 2026 13:28:45 -0700 Subject: [PATCH] feat(examples/ag-ui): render approval card for human-in-the-loop interrupts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The minimal ag-ui example had no interrupt UI, so request_approval paused with no card (interrupt parity gap vs examples/chat). Add gated on agent.interrupt(), and resolve via AG-UI's resume path — submit({ resume }) (forwarded as forwardedProps.command.resume) — mapping the accept/edit/respond/ignore actions like the canonical demo. Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/ag-ui/angular/src/app/app.html | 5 +++ examples/ag-ui/angular/src/app/app.ts | 47 +++++++++++++++++++++++-- examples/ag-ui/angular/src/styles.css | 1 + 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/examples/ag-ui/angular/src/app/app.html b/examples/ag-ui/angular/src/app/app.html index 6f7944f09..50ed6d632 100644 --- a/examples/ag-ui/angular/src/app/app.html +++ b/examples/ag-ui/angular/src/app/app.html @@ -3,5 +3,10 @@

AG-UI Chat

The Threadplane chat UI over the AG-UI transport.

+ @if (agent.interrupt && agent.interrupt()) { +
+ +
+ } diff --git a/examples/ag-ui/angular/src/app/app.ts b/examples/ag-ui/angular/src/app/app.ts index 8c0b5d494..f48ab8a77 100644 --- a/examples/ag-ui/angular/src/app/app.ts +++ b/examples/ag-ui/angular/src/app/app.ts @@ -1,13 +1,18 @@ // SPDX-License-Identifier: MIT import { ChangeDetectionStrategy, Component } from '@angular/core'; import { injectAgent } from '@threadplane/ag-ui'; -import { ChatComponent, a2uiBasicCatalog } from '@threadplane/chat'; +import { + ChatComponent, + ChatInterruptPanelComponent, + a2uiBasicCatalog, + type InterruptAction, +} from '@threadplane/chat'; @Component({ selector: 'app-root', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ChatComponent], + imports: [ChatComponent, ChatInterruptPanelComponent], templateUrl: './app.html', }) export class App { @@ -16,4 +21,42 @@ export class App { // catalog — without it, a2ui surfaces parse but never mount and the // render_a2ui_surface tool call shows only as a tool chip (issue #616). protected readonly catalog = a2uiBasicCatalog(); + + /** + * Resolve a human-in-the-loop interrupt (request_approval). The + * chat-interrupt-panel emits a four-action vocabulary; map each to a resume + * payload and replay the run via AG-UI's resume path — `submit({ resume })`, + * which the adapter forwards as `forwardedProps.command.resume`. `edit` / + * `respond` use window.prompt as a demo affordance; a production app would + * inline a textarea editor. + */ + protected async onInterruptAction(action: InterruptAction): Promise { + const interrupt = this.agent.interrupt?.(); + if (!interrupt) return; + + let resume: unknown; + switch (action) { + case 'accept': + resume = 'approved'; + break; + case 'edit': { + const reason = (interrupt.value as { reason?: string })?.reason ?? ''; + const edited = window.prompt(`Edit your response (current proposal: "${reason}"):`, 'approved'); + if (edited == null) return; + resume = edited; + break; + } + case 'respond': { + const text = window.prompt('Respond to the agent:', ''); + if (text == null) return; + resume = text; + break; + } + case 'ignore': + resume = 'denied'; + break; + } + + await this.agent.submit({ resume }); + } } diff --git a/examples/ag-ui/angular/src/styles.css b/examples/ag-ui/angular/src/styles.css index 6d9a5bde9..e9d37df2b 100644 --- a/examples/ag-ui/angular/src/styles.css +++ b/examples/ag-ui/angular/src/styles.css @@ -97,4 +97,5 @@ html[data-theme='material-light'] { .ag-ui-demo__header { padding: 12px 16px; border-bottom: 1px solid var(--tp-border, #e5e7eb); } .ag-ui-demo__header h1 { margin: 0; font-size: 1.1rem; } .ag-ui-demo__header p { margin: 2px 0 0; font-size: 0.85rem; opacity: 0.7; } +.ag-ui-demo__interrupt { padding: 12px 16px; border-bottom: 1px solid var(--tp-border, #e5e7eb); } .ag-ui-demo__chat { flex: 1 1 auto; min-height: 0; }