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; }