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