Skip to content

Commit 3fd150f

Browse files
bloveclaudegithub-actions[bot]
authored
fix: close AG-UI audit gaps F1–F4 + F6 (composer clear, light scheme, graceful stop, json-render values, markdown tracking) (#663)
* docs(ag-ui): correct F3 — stray 'Success' was a truncated delta, real gap is the error banner Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(ag-ui): Phase 3 gap-closure implementation plan Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(examples/ag-ui): set data-color-scheme so light mode reaches the page chrome (F2) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(chat): chat-input clears the textarea after submit (F1) ngModel does not write a programmatic clear back to the view under zoneless + OnPush, so sent text stayed in the composer. Bind [value]/(input) directly and drop FormsModule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(chat): restore textarea name attribute + test convention cleanups (review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ag-ui): treat stop()-induced aborts as graceful cancellation (F3) abortRun() makes the AG-UI client report onRunFailed('BodyStreamBuffer was aborted'), which rendered a red error banner for a user-initiated stop. Track abort intent and settle the store as idle instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(chat): track markdown children by index, not identity (F6) Re-parsed markdown produces new child objects every stream delta; identity tracking re-created the DOM subtree per chunk (NG0956). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(chat): resolve json-render statePath object values to scalars (F4) The json-render schema prompt (examples/*/python/src/schemas/json_render.py) documents state-bound props as { statePath: "/path" } with spec.state as the initial value model, but @json-render/core only resolves $state/ $bindState expressions — the raw object fell through to the a2ui catalog components and interpolated as the literal string "[object Object]" (KPI Text values, Slider labels, TextField values, Overview metrics). chat-generative-ui now (a) normalizes { statePath: p } props to the engine-native { $bindState: p } plus a _bindings map (mirroring what surfaceToSpec does for A2UI path refs, so user input writes back), and (b) seeds spec.state into the provided store — the chat composition's shared store starts empty and render-spec only self-seeds when no store input is given. User-modified paths are never clobbered on spec re-emits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(chat): isolate json-render stores per surface + seed-effect tracking (review) Passing the conversation-wide internal store into every generative-ui surface made same-key dashboards collide across messages (regenerate, multi-dashboard, thread switch). Match a2ui isolation: only an explicit consumer store is shared; otherwise render-spec self-seeds per instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cockpit): pass explicit stores to json-render dashboards + pin composition isolation (review) The store-isolation fix means backend-state sync reaches json-render surfaces only via an explicit consumer store; the two cockpit dashboard capabilities want exactly those shared live semantics, so they now opt in. Adds a composition-level test pinning the isolation binding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(chat): drop unused @angular/forms peer dependency chat-input no longer uses ngModel (F1); dependency-checks lint flagged the stale peer. Consumer-visible: @threadplane/chat no longer requires @angular/forms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ag-ui): also settle abort-shaped RUN_ERROR events on stop (F3) The AG-UI client surfaces a user abort through the event stream as RUN_ERROR in addition to onRunFailed; the event path bypassed the graceful-stop guard, so the error banner still appeared in live smoke. Introduces an abortSettled flag alongside abortRequested: the first delivery (event or onRunFailed) settles the store as idle and sets abortSettled; any subsequent delivery for the same abort is swallowed by settleIfAborted() without re-touching the store. Both flags are reset together at the top of submit() so the next run starts clean and genuine failures after a previous stop are never suppressed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(ag-ui): Phase 3 closure status — F1-F4 + main F6 closed, residuals logged Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ag-ui): reset abort flags in regenerate + settle duplicate aborts defensively (review) stop -> regenerate -> stop left the store wedged in streaming because regenerate never reset the abort flags and the duplicate-delivery guard returned without settling. Also documents the json-render store-isolation change in the chat CHANGELOG and fixes the README peer table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(docs): regenerate api docs * ci: retrigger checks after bot docs regeneration Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent b871ba2 commit 3fd150f

25 files changed

Lines changed: 1506 additions & 52 deletions

File tree

apps/website/content/docs/chat/api/api-docs.json

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2153,6 +2153,12 @@
21532153
"description": "",
21542154
"optional": false
21552155
},
2156+
{
2157+
"name": "normalizedSpec",
2158+
"type": "Signal<Spec | null>",
2159+
"description": "The bound spec with schema-documented `{ statePath }` prop refs\nrewritten to engine-native `{ $bindState }` + `_bindings` so values\nresolve against the state store instead of interpolating as\n\"[object Object]\" (F4).",
2160+
"optional": false
2161+
},
21562162
{
21572163
"name": "registry",
21582164
"type": "InputSignal<AngularRegistry | undefined>",
@@ -2393,6 +2399,19 @@
23932399
"description": "",
23942400
"params": []
23952401
},
2402+
{
2403+
"name": "onInput",
2404+
"signature": "onInput(event: Event): void",
2405+
"description": "Sync the textarea's value into the signal on user input. A direct\n [value]/(input) pair is used instead of ngModel: NgModel does not\n reliably write a programmatic clear back to the view under zoneless\n + OnPush, leaving sent text visible in the composer (audit F1).",
2406+
"params": [
2407+
{
2408+
"name": "event",
2409+
"type": "Event",
2410+
"description": "",
2411+
"optional": false
2412+
}
2413+
]
2414+
},
23962415
{
23972416
"name": "onKeydown",
23982417
"signature": "onKeydown(event: KeyboardEvent): void",
@@ -2782,6 +2801,12 @@
27822801
"description": "",
27832802
"optional": false
27842803
},
2804+
{
2805+
"name": "clientTools",
2806+
"type": "InputSignal<Readonly<Record<string, ClientToolDef>> | undefined>",
2807+
"description": "Frontend-declared client tools forwarded to the inner `<chat>`.",
2808+
"optional": false
2809+
},
27852810
{
27862811
"name": "closeOnEscape",
27872812
"type": "InputSignal<boolean>",
@@ -3277,6 +3302,12 @@
32773302
"description": "",
32783303
"optional": false
32793304
},
3305+
{
3306+
"name": "clientTools",
3307+
"type": "InputSignal<Readonly<Record<string, ClientToolDef>> | undefined>",
3308+
"description": "Frontend-declared client tools forwarded to the inner `<chat>`.",
3309+
"optional": false
3310+
},
32803311
{
32813312
"name": "closeOnEscape",
32823313
"type": "InputSignal<boolean>",
@@ -4702,7 +4733,7 @@
47024733
{
47034734
"name": "MarkdownChildrenComponent",
47044735
"kind": "class",
4705-
"description": "Recursively dispatches a parent node's children through the markdown view\nregistry. Each child's `type` is looked up in the registry; the resolved\ncomponent is rendered with `[node]` bound to that child.\n\nIdentity-preserving: `track $any(child)` keys on the JS reference of the\nchild node. Because @cacheplane/partial-markdown preserves node identity\nacross pushes, unchanged subtrees never re-render.",
4736+
"description": "Recursively dispatches a parent node's children through the markdown view\nregistry. Each child's `type` is looked up in the registry; the resolved\ncomponent is rendered with `[node]` bound to that child.\n\nPosition-stable: `track $index` avoids NG0956 re-creation warnings that\noccur when the markdown pipeline re-parses content on every stream delta,\nproducing new child object references even for unchanged nodes.",
47064737
"params": [],
47074738
"examples": [],
47084739
"properties": [

cockpit/ag-ui/json-render/angular/src/app/json-render.component.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Component } from '@angular/core';
33
import { ChatComponent, ChatWelcomeSuggestionComponent, views } from '@threadplane/chat';
44
import { injectAgent } from '@threadplane/ag-ui';
5+
import { signalStateStore } from '@threadplane/render';
56
import { ExampleChatLayoutComponent } from '@threadplane/example-layouts';
67
import { StatCardComponent } from './views/stat-card.component';
78
import { ContainerComponent } from './views/container.component';
@@ -30,7 +31,7 @@ const WELCOME_SUGGESTIONS = [
3031
imports: [ChatComponent, ChatWelcomeSuggestionComponent, ExampleChatLayoutComponent],
3132
template: `
3233
<example-chat-layout>
33-
<chat main [agent]="agent" [views]="dashboardViews" class="flex-1 min-w-0">
34+
<chat main [agent]="agent" [views]="dashboardViews" [store]="dashStore" class="flex-1 min-w-0">
3435
<div chatWelcomeSuggestions>
3536
@for (s of suggestions; track s.value) {
3637
<chat-welcome-suggestion [label]="s.label" [value]="s.value" (selected)="send($event)" />
@@ -44,5 +45,10 @@ export class JsonRenderComponent {
4445
protected readonly agent = injectAgent();
4546
protected readonly dashboardViews = dashboardViews;
4647
protected readonly suggestions = WELCOME_SUGGESTIONS;
48+
/**
49+
* Explicit shared store: backend state (STATE_SNAPSHOT) syncs into it via
50+
* the chat composition, so every dashboard surface reads live values.
51+
*/
52+
protected readonly dashStore = signalStateStore({});
4753
protected send(text: string): void { void this.agent.submit({ message: text }); }
4854
}

cockpit/ag-ui/json-render/python/docs/guide.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@ as the backend updates the data.
1010
</Summary>
1111

1212
<Prompt>
13-
Render a backend-authored dashboard with `@threadplane/chat` over the AG-UI adapter. Register your view components in the `views` map and pass it to `<chat>`. Have the agent emit a json-render spec (with `$state` bindings) as the assistant message content, and put the data the spec binds to in the LangGraph graph state so `ag-ui-langgraph` emits it as a `STATE_SNAPSHOT`. The chat composition resolves the bindings automatically.
13+
Render a backend-authored dashboard with `@threadplane/chat` over the AG-UI adapter. Register your view components in the `views` map and pass it to `<chat>`, along with an explicit `[store]` — the composition syncs incoming agent state into that store, and the spec's `$state` bindings resolve against it. Have the agent emit a json-render spec (with `$state` bindings) as the assistant message content, and put the data the spec binds to in the LangGraph graph state so `ag-ui-langgraph` emits it as a `STATE_SNAPSHOT`.
1414
</Prompt>
1515

1616
<Steps>
1717
<Step title="Register the view components">
1818

19-
Build a `views` registry keyed by the component types your spec will reference, and pass it to `<chat>`:
19+
Build a `views` registry keyed by the component types your spec will reference, and pass it to `<chat>` together with an explicit store. Without a `[store]`, each render surface seeds its own isolated store from the spec — the explicit store is what lets backend state (`STATE_SNAPSHOT`) reach the dashboard bindings:
2020

2121
```typescript
2222
// json-render.component.ts
2323
import { ChatComponent, views } from '@threadplane/chat';
2424
import { injectAgent } from '@threadplane/ag-ui';
25+
import { signalStateStore } from '@threadplane/render';
2526
import { StatCardComponent } from './views/stat-card.component';
2627
import { DashboardGridComponent } from './views/dashboard-grid.component';
2728
// …line-chart, bar-chart, data-grid, container
@@ -31,10 +32,13 @@ const dashboardViews = views({
3132
dashboard_grid: DashboardGridComponent,
3233
//
3334
});
35+
36+
// In the component class:
37+
readonly dashStore = signalStateStore({});
3438
```
3539

3640
```html
37-
<chat main [agent]="agent" [views]="dashboardViews" />
41+
<chat main [agent]="agent" [views]="dashboardViews" [store]="dashStore" />
3842
```
3943

4044
</Step>
@@ -76,8 +80,8 @@ data prop uses a `$state` binding rather than a literal:
7680
This is the AG-UI-native part. Instead of pushing data through a side channel,
7781
put it in the **graph state**`ag-ui-langgraph` emits the state object as a
7882
`STATE_SNAPSHOT`, the adapter writes it to the agent's `state` signal, and the
79-
chat composition syncs it into the render store where the `$state` bindings
80-
resolve:
83+
chat composition syncs it into the explicit `[store]` you passed, where the
84+
`$state` bindings resolve:
8185

8286
```python
8387
# graph.py — emit_state returns the accumulated tool data into state

cockpit/ag-ui/json-render/python/src/graph.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
1818
emit_state walks the message history for this turn and returns the tool
1919
results as top-level state fields — ag-ui-langgraph emits them as
20-
STATE_SNAPSHOT; the Angular chat-lib effect syncs them into the render
21-
store, where the spec's $state bindings resolve them.
20+
STATE_SNAPSHOT; the Angular chat-lib effect syncs them into the explicit
21+
[store] the app passes to <chat>, where the spec's $state bindings
22+
resolve them.
2223
"""
2324

2425
import json

cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Component } from '@angular/core';
33
import { ChatComponent, ChatWelcomeSuggestionComponent, views } from '@threadplane/chat';
44
import { injectAgent } from '@threadplane/langgraph';
5+
import { signalStateStore } from '@threadplane/render';
56
import { ExampleChatLayoutComponent } from '@threadplane/example-layouts';
67

78
import { StatCardComponent } from './views/stat-card.component';
@@ -31,7 +32,7 @@ const WELCOME_SUGGESTIONS = [
3132
imports: [ChatComponent, ChatWelcomeSuggestionComponent, ExampleChatLayoutComponent],
3233
template: `
3334
<example-chat-layout>
34-
<chat main [agent]="agent" [views]="dashboardViews" class="flex-1 min-w-0">
35+
<chat main [agent]="agent" [views]="dashboardViews" [store]="dashStore" class="flex-1 min-w-0">
3536
<div chatWelcomeSuggestions>
3637
@for (s of suggestions; track s.value) {
3738
<chat-welcome-suggestion
@@ -50,6 +51,12 @@ export class GenerativeUiComponent {
5051
protected readonly dashboardViews = dashboardViews;
5152
protected readonly suggestions = WELCOME_SUGGESTIONS;
5253

54+
/**
55+
* Explicit shared store: backend graph state syncs into it via the chat
56+
* composition, so every dashboard surface reads live values.
57+
*/
58+
protected readonly dashStore = signalStateStore({});
59+
5360
protected send(text: string): void {
5461
void this.agent.submit({ message: text });
5562
}

0 commit comments

Comments
 (0)