Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/ag-ui/angular/src/app/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@
<h1>AG-UI Chat</h1>
<p>The Threadplane chat UI over the AG-UI transport.</p>
</header>
@if (agent.interrupt && agent.interrupt()) {
<div class="ag-ui-demo__interrupt" role="region" aria-label="Approval required">
<chat-interrupt-panel [agent]="agent" (action)="onInterruptAction($event)" />
</div>
}
<chat main [agent]="agent" [views]="catalog" class="ag-ui-demo__chat" />
</main>
47 changes: 45 additions & 2 deletions examples/ag-ui/angular/src/app/app.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<void> {
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 });
}
}
1 change: 1 addition & 0 deletions examples/ag-ui/angular/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Loading