@artooi/ag-ui-web-component 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +417 -1
- package/README.md +371 -5
- package/dist/ag-ui-web-component.bundle.js +308 -50
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +69 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +207 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +38 -0
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1516 -76
- package/dist/index.js.map +4 -4
- package/dist/ui/approval_card.d.ts +18 -0
- package/dist/ui/approval_card.d.ts.map +1 -1
- package/dist/ui/checkpoint_menu.d.ts +10 -0
- package/dist/ui/checkpoint_menu.d.ts.map +1 -1
- package/dist/ui/confirmation_card.d.ts +16 -0
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/message_actions.d.ts +46 -0
- package/dist/ui/message_actions.d.ts.map +1 -0
- package/dist/ui/page_quote_offer.d.ts +33 -0
- package/dist/ui/page_quote_offer.d.ts.map +1 -0
- package/dist/ui/quote_selection.d.ts +66 -0
- package/dist/ui/quote_selection.d.ts.map +1 -0
- package/dist/ui/relative_time.d.ts +10 -0
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/stick_to_bottom.d.ts +55 -0
- package/dist/ui/stick_to_bottom.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/suggestion_chips.d.ts +29 -0
- package/dist/ui/suggestion_chips.d.ts.map +1 -0
- package/dist/ui/thread_drawer.d.ts +10 -0
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +8 -0
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +40 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +75 -0
- package/src/core/ag_ui_chat.ts +927 -73
- package/src/core/agui_client.ts +63 -0
- package/src/index.ts +39 -0
- package/src/ui/approval_card.ts +90 -2
- package/src/ui/checkpoint_menu.ts +22 -5
- package/src/ui/confirmation_card.ts +29 -1
- package/src/ui/message_actions.ts +158 -0
- package/src/ui/page_quote_offer.ts +215 -0
- package/src/ui/quote_selection.ts +345 -0
- package/src/ui/relative_time.ts +11 -0
- package/src/ui/stick_to_bottom.ts +126 -0
- package/src/ui/styles.ts +227 -0
- package/src/ui/suggestion_chips.ts +73 -0
- package/src/ui/thread_drawer.ts +22 -2
- package/src/ui/tool_call_card.ts +9 -0
- package/src/ui/ui_strings.ts +60 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,421 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.29.0] — 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Quote a selection into the composer.** Selecting text in the transcript now
|
|
15
|
+
floats a **Quote** offer beside it; taking it drops the selection in as a
|
|
16
|
+
markdown blockquote and leaves the caret on a fresh line under it. Nothing is
|
|
17
|
+
sent -- a quotation is how a question narrows to one part of an answer, so the
|
|
18
|
+
question still has to be written. Quoting appends, so a second quotation is a
|
|
19
|
+
second thing being asked about. Long selections cap at 500 characters.
|
|
20
|
+
`data-quote-selection="false"` turns the offer off; the `quote-selection`
|
|
21
|
+
`::part()` styles it.
|
|
22
|
+
|
|
23
|
+
**The half worth having is the one the transcript cannot reach.** A chat
|
|
24
|
+
mounted beside a table, a diff or a report sits in the surface the user
|
|
25
|
+
actually works in, and a selection made *there* is one no hosted chat can see.
|
|
26
|
+
`offerQuoteInPage()` extends the same select-then-offer gesture to the host's
|
|
27
|
+
own page, or to one region of it, and `quote(text)` is the seam underneath for
|
|
28
|
+
a deliberate trigger like a per-row "ask about this" button.
|
|
29
|
+
|
|
30
|
+
**The page half is a method rather than a documented recipe, and that is the
|
|
31
|
+
correction, not the design.** It shipped first as four lines in the README --
|
|
32
|
+
quote every settled selection -- which appends to the composer on every drag
|
|
33
|
+
the user made to *read*, to copy, or to fix a typo. Worse, it cannot tell a
|
|
34
|
+
selection in the page's prose from one inside the user's own half-typed
|
|
35
|
+
`<input>`: Chrome reports a field's internal selection through
|
|
36
|
+
`document.getSelection()` as an ordinary range over the field's **wrapper**,
|
|
37
|
+
so the text reads back perfectly and nothing about the range says where it
|
|
38
|
+
came from. The only signal is `document.activeElement`. That guard, plus
|
|
39
|
+
skipping the widget's own transcript -- which needs the *event path*, since
|
|
40
|
+
`Node.contains` is false across a shadow boundary -- plus retiring a
|
|
41
|
+
fixed-position affordance on scroll, is three non-obvious guards, and three
|
|
42
|
+
guards is a feature rather than a snippet. `attachQuoteOffer` is exported for
|
|
43
|
+
a host that wants it without the element.
|
|
44
|
+
|
|
45
|
+
**A selection across several elements is not a paragraph, and was treated as
|
|
46
|
+
one twice over.** The offer was hung off the selection's *bounding box*, whose
|
|
47
|
+
centre belongs to no line -- a drag from a form's left column down to a
|
|
48
|
+
full-width line running under the chat panel put the offer on the panel,
|
|
49
|
+
pointing at a line the user had never looked at. It now hangs off the line the
|
|
50
|
+
gesture ended on, or the first line for a keyboard selection. And the text was
|
|
51
|
+
read with `Range.toString()`, which concatenates text nodes and asks nothing
|
|
52
|
+
about CSS: quoting a form returned the values of every `<option>` in a closed
|
|
53
|
+
`<select>`, the markup's own indentation on every line, and a blank `>` for
|
|
54
|
+
every gap between elements -- twenty-four lines of which twelve were empty.
|
|
55
|
+
The read is now what the engine says is rendered (`checkVisibility`), with the
|
|
56
|
+
whitespace a collapsing `white-space` collapses, and preformatted text passed
|
|
57
|
+
through so a quoted code block keeps its shape. Four leading spaces inside a
|
|
58
|
+
blockquote is a markdown code block, so this was a rendering defect and not
|
|
59
|
+
only an untidy one.
|
|
60
|
+
|
|
61
|
+
Reading a selection out of a shadow tree is the part that takes care, and the
|
|
62
|
+
component now does it properly: engines disagree about what
|
|
63
|
+
`document.getSelection()` reports for a selection made inside a shadow root,
|
|
64
|
+
and `getComposedRanges` is used where the engine has it, with the direct read
|
|
65
|
+
behind it. `quotableSelection`, `asQuote` and `MAX_QUOTE_CHARS` are exported
|
|
66
|
+
for a host with the same problem in its own component.
|
|
67
|
+
|
|
68
|
+
- **`approveWithEdits` — edit a gated call's arguments before approving it.**
|
|
69
|
+
AG-UI's resume payload carries `editedArgs` and the protocol gates it on the
|
|
70
|
+
agent's `approveWithEdits` capability; the approval card could not offer it.
|
|
71
|
+
It now shows the call's arguments as editable JSON.
|
|
72
|
+
|
|
73
|
+
**Off by default, and an assertion about the server rather than a
|
|
74
|
+
negotiation.** Capabilities are not on the wire this component reads, so it
|
|
75
|
+
cannot check — and turned on against a server that ignores `editedArgs`, a
|
|
76
|
+
user would edit arguments it silently discards, which is worse than not
|
|
77
|
+
offering.
|
|
78
|
+
|
|
79
|
+
`editedArgs` rides the payload **only when something actually changed**, so a
|
|
80
|
+
server can tell "approved as proposed" from "approved, but like this" without
|
|
81
|
+
diffing what it sent. Unparseable JSON, or JSON that is not an object, keeps
|
|
82
|
+
the card open with the reason on it rather than approving the original behind
|
|
83
|
+
the user's back. Offered only for an interrupt naming a call this component
|
|
84
|
+
holds a card for, since the card is where the arguments still are.
|
|
85
|
+
|
|
86
|
+
New `::part()`s `approval-edit`, `approval-args`, `approval-error`, and three
|
|
87
|
+
strings.
|
|
88
|
+
|
|
89
|
+
- **`formatRelativeTime` — replace the drawer and checkpoint timestamps.** There
|
|
90
|
+
is no `Intl` anywhere in this component, and the locale-neutral `"5m ago"` is
|
|
91
|
+
deliberate: guessing a locale would disagree with the host page, and being
|
|
92
|
+
wrong in a second language is worse than being neutral in one. That is a good
|
|
93
|
+
default and a bad requirement, and a host previously could not even reach the
|
|
94
|
+
formatter to replace it.
|
|
95
|
+
|
|
96
|
+
`relativeTime` and the `RelativeTimeFormatter` type are now exported, and the
|
|
97
|
+
formatter is read at render rather than at connect, so setting it after mount
|
|
98
|
+
works.
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
- **Server-pushed follow-up suggestions.** A `suggestions` activity draws its
|
|
102
|
+
prompts as chips; clicking one sends it as the user's message. Registered
|
|
103
|
+
skill chips are static and host-configured, so they can say "summarize this"
|
|
104
|
+
but never "want me to update the shipping address too?" after a tool has run.
|
|
105
|
+
|
|
106
|
+
Rides the activity envelope charts already use rather than a `CUSTOM` event,
|
|
107
|
+
which buys persistence for nothing: chips are content, so a reload puts them
|
|
108
|
+
back, and a set pushed under an id already on screen replaces that row.
|
|
109
|
+
|
|
110
|
+
Bounded at 4 prompts of 120 characters, mirroring django-ag-ui's
|
|
111
|
+
`suggestions_activity()`. The producer *raises* past those bounds while this
|
|
112
|
+
side silently drops — deliberate asymmetry, because the producer can report
|
|
113
|
+
the problem and the client cannot. Both numbers live on both sides for the
|
|
114
|
+
reason the chart bounds do: mirroring only some of them leaves exactly the
|
|
115
|
+
silent-drop hole they exist to close.
|
|
116
|
+
|
|
117
|
+
New `::part()`s `suggestions` / `suggestion-chip`, new
|
|
118
|
+
`SUGGESTIONS_ACTIVITY_TYPE`, and `renderSuggestionChips` / `suggestionPrompts`
|
|
119
|
+
exported for a host drawing its own.
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
- **A message action row — copy, retry, thumbs up/down — under every finished
|
|
123
|
+
assistant message.** There were **zero** message-level actions before this;
|
|
124
|
+
`attachCopyButtons` handled fenced code and nothing else.
|
|
125
|
+
|
|
126
|
+
**Retry is the item that earns it.** History truncates to the most recent user
|
|
127
|
+
message inclusive and the run repeats, so the agent answers the question it was
|
|
128
|
+
asked rather than being told its last answer was wrong. It sits on the **last**
|
|
129
|
+
answer only: re-running an older turn is branching, and for a page-driving
|
|
130
|
+
agent editing a past turn is not neutral, because those turns clicked buttons.
|
|
131
|
+
`retryLastTurn()` is public for a host driving its own message UI.
|
|
132
|
+
|
|
133
|
+
A retried turn **re-runs its tools**, which the previous attempt already ran.
|
|
134
|
+
Confirmation still applies, so a destructive tool asks again unless the user
|
|
135
|
+
waived it this session.
|
|
136
|
+
|
|
137
|
+
Ratings fire `ag-ui-feedback` and **store nothing**: a rating belongs to
|
|
138
|
+
whatever the host already uses for product signal, and a write-only table
|
|
139
|
+
inside a chat widget is a schema nobody reads.
|
|
140
|
+
|
|
141
|
+
New `::part()`s `message-actions` / `message-action` (plus
|
|
142
|
+
`message-action-retry`, `-copy`, `-up`, `-down`), new `FEEDBACK_EVENT` and
|
|
143
|
+
`FeedbackDetail`, and `attachMessageActions` / `messageActionBar` exported for
|
|
144
|
+
a host assembling its own transcript.
|
|
145
|
+
|
|
146
|
+
- **A dropped run has a way back.** `ConnectionLostError` rendered a dead
|
|
147
|
+
"Connection lost" bubble; only *uploads* had retry. The failed bubble now
|
|
148
|
+
carries the same action row, with Retry and Copy and no rating — error text is
|
|
149
|
+
what people paste into a bug report, while "the connection dropped" is not a
|
|
150
|
+
statement about answer quality and mixing it into feedback makes that signal
|
|
151
|
+
say less.
|
|
152
|
+
|
|
153
|
+
**Kept as an error rather than demoted to a run notice**, which is what was
|
|
154
|
+
originally proposed. `renderRunNotice`'s contract is that a notice "never
|
|
155
|
+
settles, takes no action, and carries no controls" and is "distinct from an
|
|
156
|
+
error, which is a failure". This is a failure that now needs a control, so the
|
|
157
|
+
taxonomy already had the answer.
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
- **"Always allow" on the confirmation card — a session-scoped waiver, per tool
|
|
161
|
+
name.** Confirmation was binary and permanent: `autoConfirm` is
|
|
162
|
+
all-or-nothing and `confirmPredicate` has no memory, so a tool the user
|
|
163
|
+
approves every single time keeps asking every single time.
|
|
164
|
+
|
|
165
|
+
A prompt approved nearly every time is not a decision, it is a speed bump, and
|
|
166
|
+
the reflex it trains is what makes the rare refusal easy to miss. Anthropic
|
|
167
|
+
published that users approve **~93%** of Claude Code permission prompts
|
|
168
|
+
manually and called interactive confirmation *"behaviorally unreliable as a
|
|
169
|
+
sole safety mechanism"* on exactly that basis. The waiver exists so the
|
|
170
|
+
prompts that remain still mean something.
|
|
171
|
+
|
|
172
|
+
**The button appears only where the `x-destructive` default is what gated the
|
|
173
|
+
call.** `confirmPredicate` is documented as authoritative, so letting one
|
|
174
|
+
click retire it would silently defeat a host policy — and because the offer
|
|
175
|
+
and the allowlist sit on the same path, there is no dead button either. The
|
|
176
|
+
waiver is per tool name and per element, held in memory and never persisted: a
|
|
177
|
+
session decision that outlived the tab would be a permanent grant made by one
|
|
178
|
+
click, which is what `autoConfirm` already exists to say deliberately.
|
|
179
|
+
|
|
180
|
+
New `::part()` `confirm-always`, new string `confirmAlways`, and
|
|
181
|
+
`requestConfirmation` gains an `onAlwaysAllow` option — its presence is what
|
|
182
|
+
renders the button, so the affordance can never appear with nothing listening.
|
|
183
|
+
The card still resolves `true`: the waiver is *in addition to* approving this
|
|
184
|
+
call, not instead of it.
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
- **`ag-ui-invalidate` and `RunFinishedDetail.invalidated` — the agent tells your
|
|
188
|
+
page what it moved.** The agent writes and the page still shows the old list.
|
|
189
|
+
`ag-ui-run-finished` already said *something* moved, so this is **precision on
|
|
190
|
+
a channel that ships**: the server names the resources and a host refetches
|
|
191
|
+
only those.
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
chat.addEventListener("ag-ui-invalidate", (e) => {
|
|
195
|
+
if (formIsDirty()) return showBanner("This data changed.", e.detail.keys);
|
|
196
|
+
refetch(e.detail.keys);
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Do not reload on this.** The user was probably typing, and an
|
|
201
|
+
agent-triggered reload into a live form destroys unsaved input with no
|
|
202
|
+
explanation the user can see. The component never reloads by itself and offers
|
|
203
|
+
no option that would.
|
|
204
|
+
|
|
205
|
+
**Two dispatches, deliberately.** The live event fires as each announcement
|
|
206
|
+
arrives, which is what makes a long multi-step run feel live -- the list
|
|
207
|
+
refreshes as the third of eight writes lands. `invalidated` on the existing
|
|
208
|
+
run-finished detail carries the same keys de-duplicated at the end, so a host
|
|
209
|
+
already listening there upgrades by reading one extra field:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
if (detail.invalidated.length > 0) refetchOnly(detail.invalidated);
|
|
213
|
+
else if (detail.tools.some((t) => t.side === "server")) refetchEverything();
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
That `else` is the whole compatibility story. A new server with an old client
|
|
217
|
+
has its `CUSTOM` event ignored and still gets the coarse refetch; an old server
|
|
218
|
+
with a new client leaves `invalidated` empty and falls through to the same
|
|
219
|
+
branch. Nothing negotiates, which is what makes this shippable across repos
|
|
220
|
+
with independent release cadences.
|
|
221
|
+
|
|
222
|
+
**Keys are opaque and matching is exact.** `orders/42` does not imply `orders`
|
|
223
|
+
-- a prefix rule would be this component guessing at a scheme it does not own,
|
|
224
|
+
and `orders/1` would match `orders/11`. A server that wants the collection
|
|
225
|
+
refreshed names it. Hosts may match hierarchically in their own vocabulary,
|
|
226
|
+
where the scheme is known.
|
|
227
|
+
|
|
228
|
+
Built on the `CUSTOM` carrier rather than `ACTIVITY_SNAPSHOT`, because an
|
|
229
|
+
invalidation is an imperative: activities are materialised into messages,
|
|
230
|
+
persisted and replayed on every thread restore, and an invalidation replayed on
|
|
231
|
+
every thread load is a refetch storm. Nothing is rendered or persisted, and
|
|
232
|
+
that absence is asserted. Every other `CUSTOM` name still arrives on
|
|
233
|
+
`ag-ui-custom` unchanged, and an invalidation does **not** also fire it -- a
|
|
234
|
+
host listening to both would otherwise refetch twice for one announcement.
|
|
235
|
+
|
|
236
|
+
Requires django-ag-ui 0.51 to have anything to receive.
|
|
237
|
+
|
|
238
|
+
- **`registerActivityRenderer` — `activityType` is an open set now, not two
|
|
239
|
+
branches.** AG-UI leaves exactly two payload names an open string the protocol
|
|
240
|
+
does not enumerate, and the component treated neither as open: `chart` and
|
|
241
|
+
`compaction` were handled and everything else fell through a bare `return`,
|
|
242
|
+
with no host seam and no record that anything had arrived. The server could
|
|
243
|
+
only say things the client had been compiled to understand, in a protocol
|
|
244
|
+
designed so it can say more.
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
chat.registerActivityRenderer({
|
|
248
|
+
type: "build_status",
|
|
249
|
+
render: (content) => {
|
|
250
|
+
const el = document.createElement("div");
|
|
251
|
+
el.textContent = `Build ${content.status}`;
|
|
252
|
+
return el;
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`render` carries the same contract as a client tool's `render`, and for the
|
|
258
|
+
same reason rather than by analogy: activities are materialised into
|
|
259
|
+
`role: "activity"` messages, persisted with the transcript and re-fired on
|
|
260
|
+
restore, so a renderer that writes to the page instead of returning DOM fires
|
|
261
|
+
again on every thread load.
|
|
262
|
+
|
|
263
|
+
**Both built-ins go through the registry**, which is the test that the seam is
|
|
264
|
+
real — a built-in needing a privileged branch would mean the seam cannot
|
|
265
|
+
express what the component itself needs. Registering either name replaces it.
|
|
266
|
+
|
|
267
|
+
Compaction gains two things by going through the seam: a reload puts the
|
|
268
|
+
notice back (it is content, and content replays), and a server redrawing under
|
|
269
|
+
the same `messageId` replaces it instead of leaving two notices standing for
|
|
270
|
+
one event.
|
|
271
|
+
|
|
272
|
+
- **`unhandledActivityTypes`** — the activity types that arrived with nobody
|
|
273
|
+
registered to draw them. Deliberately the only trace: ignoring an unknown name
|
|
274
|
+
is the protocol's own answer, and a warning would fire on every
|
|
275
|
+
forward-compatible server, but "nothing happened and nothing was said" is
|
|
276
|
+
impossible to debug. Note `chart` is listed until `enableCharts(["activity"])`
|
|
277
|
+
is called, which is the honest answer to "I pushed a chart and nothing
|
|
278
|
+
happened".
|
|
279
|
+
|
|
280
|
+
- **`ag-ui-custom`** (`CUSTOM_AGENT_EVENT`, detail `CustomAgentDetail`) — the
|
|
281
|
+
other open carrier, which had no implementation at all. An AG-UI `CUSTOM`
|
|
282
|
+
event is forwarded to the host page whole and uninterpreted, `bubbles` and
|
|
283
|
+
`composed` like every other event the element dispatches.
|
|
284
|
+
|
|
285
|
+
**It is deliberately not rendered, persisted or replayed.** That asymmetry is
|
|
286
|
+
the rule for choosing between the two carriers: `ACTIVITY_SNAPSHOT` is content
|
|
287
|
+
and has a place in the conversation, `CUSTOM` is an imperative with no meaning
|
|
288
|
+
once acted on, and replaying "refetch the board" on every thread load would be
|
|
289
|
+
a bug rather than a feature.
|
|
290
|
+
|
|
291
|
+
**Note:** pydantic-ai emits its own compaction activity under
|
|
292
|
+
`pydantic_ai_compaction`, by a different route than the harness sink this
|
|
293
|
+
package renders as `compaction`. The registry deliberately does **not** answer
|
|
294
|
+
to both names — doing so would give a deployment running both two notices for
|
|
295
|
+
one event. It shows up in `unhandledActivityTypes` instead, so a host that
|
|
296
|
+
wants it can register it and decide about duplication itself.
|
|
297
|
+
|
|
298
|
+
### Fixed
|
|
299
|
+
|
|
300
|
+
- **The message action row sat closer to the block below it than to the message
|
|
301
|
+
it acts on.** The answer group is a flex column with a 10px gap, so the row's
|
|
302
|
+
`margin-top: 6px` **added** to that gap instead of tightening it: 16px above,
|
|
303
|
+
10px below, and the buttons read as belonging to whatever card followed. A
|
|
304
|
+
negative margin pulls the row back inside the gap — 4px above, 10px below.
|
|
305
|
+
|
|
306
|
+
Found by driving the demo, not by any assertion: every existing check was
|
|
307
|
+
satisfied by the broken spacing. Ordered correctly, contained correctly,
|
|
308
|
+
visible — and grouped with the wrong thing. The new browser case fails against
|
|
309
|
+
the previous stylesheet with `expected 16 to be less than 10`.
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
- **The action row is a sibling of the message bubble, not a child.** Inside, the
|
|
313
|
+
buttons join the bubble's `textContent` — which is what Copy reads, what
|
|
314
|
+
history persists, and what every existing assertion about a message's text
|
|
315
|
+
compares against. An answer would have been copied back carrying the glyphs of
|
|
316
|
+
the buttons that copied it. Caught by twelve existing tests failing at once,
|
|
317
|
+
which is the check working.
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
- **The confirmation card's action row could push a button outside the card.**
|
|
321
|
+
It was a `justify-content: flex-end` flex line with no wrap, built when there
|
|
322
|
+
were two buttons. Measured in Chromium at a 260px panel: the three buttons want
|
|
323
|
+
71 + 107 + 81 plus two 8px gaps against a 200px row, and the overflow went off
|
|
324
|
+
the **left** edge — Cancel rendered, styled and reporting its label, 27px
|
|
325
|
+
outside the box the user can see and hit.
|
|
326
|
+
|
|
327
|
+
`flex-wrap: wrap` lets the row take a second line instead, as the checkpoint
|
|
328
|
+
row already does. Found by measuring rather than by review: happy-dom lays out
|
|
329
|
+
no boxes and answers 0 for every width, so it called the overflowing row and
|
|
330
|
+
the fitting one the same pass. The new browser test fails against the previous
|
|
331
|
+
stylesheet at that width and passes at the two wider ones, which is the honest
|
|
332
|
+
shape of the bug.
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
- **A server that replaced the conversation did it in silence.** AG-UI's
|
|
336
|
+
`MESSAGES_SNAPSHOT` is applied by `@ag-ui/client` before any subscriber runs,
|
|
337
|
+
so `agent.messages` **is** the server's list by the time the host sees
|
|
338
|
+
anything — and the run loop persists `agent.messages`. The replacement
|
|
339
|
+
therefore reached the conversation store either way, while the DOM was
|
|
340
|
+
untouched. Nothing looked wrong until a reload, in a later session, served a
|
|
341
|
+
transcript the user had never seen, with no event that could be correlated to
|
|
342
|
+
it. That is not reported as a bug; it is reported as "the chat lost my
|
|
343
|
+
messages".
|
|
344
|
+
|
|
345
|
+
The store still follows the server, deliberately — the server is authoritative
|
|
346
|
+
about what the conversation *is*, and it would follow it regardless. What
|
|
347
|
+
changes is that the replacement is now announced in the transcript.
|
|
348
|
+
|
|
349
|
+
**Re-rendering from the snapshot was the other candidate and is declined.** A
|
|
350
|
+
snapshot can land mid-run, and rebuilding the transcript then destroys the
|
|
351
|
+
in-flight run's own state: the streaming bubble, the open answer group, and
|
|
352
|
+
every tool card keyed by call id, some still waiting on results. This is the
|
|
353
|
+
same answer the same question already got for compaction.
|
|
354
|
+
|
|
355
|
+
- **One string-valued point silently dropped a whole chart that was on screen.**
|
|
356
|
+
`chartSpecFrom` returns `null` for the entire spec when any point is not a
|
|
357
|
+
finite number — not the offending series, the whole chart — and the pushed
|
|
358
|
+
activity path then removed any chart already rendered and returned, with no
|
|
359
|
+
`console` call anywhere on that path. The triggering shape is not exotic: a
|
|
360
|
+
Django `Sum` over a `DecimalField` serialises as a JSON string, and money is
|
|
361
|
+
the most common chart input there is.
|
|
362
|
+
|
|
363
|
+
**Removing it is still right** and is unchanged: leaving retracted numbers on
|
|
364
|
+
screen reading as current is worse, and a reload drops the chart anyway
|
|
365
|
+
because the *stored* content is the version that could not be drawn. Live and
|
|
366
|
+
reload should agree. What was wrong was doing it silently. The path now warns
|
|
367
|
+
on the console naming the likely cause, and posts a notice in the transcript
|
|
368
|
+
when a chart that had been drawn is taken away.
|
|
369
|
+
|
|
370
|
+
Nothing is coerced, client-side or server-side. `django-ag-ui` already raises
|
|
371
|
+
at construction and names `Decimal` on purpose, and guessing whether
|
|
372
|
+
`"1234.50"` lost precision upstream is not a favour worth doing.
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
- **A screen reader re-announced the whole answer tens of times per turn.** The
|
|
376
|
+
transcript carried both `role="log"` and an explicit `aria-live="polite"`,
|
|
377
|
+
and the streaming bubble's `innerHTML` is replaced inside it on every
|
|
378
|
+
animation frame. `role="log"` already implies polite announcement and the
|
|
379
|
+
default `aria-relevant` includes text additions, so every frame was a fresh
|
|
380
|
+
announcement of the answer so far. That is not merely unhelpful; it is
|
|
381
|
+
hostile.
|
|
382
|
+
|
|
383
|
+
The transcript is demoted out of live-region duty with an explicit
|
|
384
|
+
`aria-live="off"`, which overrides the value the role implies. **The role
|
|
385
|
+
stays** -- the log semantics are what let the transcript be navigated as one,
|
|
386
|
+
and only the announcing was the defect.
|
|
387
|
+
|
|
388
|
+
A separate visually-hidden status region takes over, and roughly four short
|
|
389
|
+
statuses land per turn: responding, answered, a decision is waiting and how
|
|
390
|
+
many, stopped, failed. Five new `strings` keys (`announceResponding`,
|
|
391
|
+
`announceAnswerReady`, `announceAwaitingDecision`, `announceStopped`,
|
|
392
|
+
`announceFailed`) make all of them translatable. The answer's own words never
|
|
393
|
+
reach it, and neither does an exception's.
|
|
394
|
+
|
|
395
|
+
- **The transcript could not be read while anything streamed.** Eleven separate
|
|
396
|
+
sites assigned `scrollTop = scrollHeight` unconditionally, and nothing in the
|
|
397
|
+
element listened for a `scroll` event -- so nothing knew the reader had
|
|
398
|
+
scrolled up, and scrolling back during a run was undone by the next token.
|
|
399
|
+
|
|
400
|
+
All eleven now follow the foot only while the reader is already there. A
|
|
401
|
+
**jump-to-latest** button (`jump-latest` part, `jumpToLatest` string) appears
|
|
402
|
+
once they have scrolled away *and* have since missed something; scrolling up
|
|
403
|
+
through a settled transcript is not a reason to nag. A user's own message
|
|
404
|
+
still goes to the foot -- pressing Send is as deliberate as pressing the
|
|
405
|
+
button. `overflow-anchor: none` stops the browser's own scroll anchoring
|
|
406
|
+
competing for the same job.
|
|
407
|
+
|
|
408
|
+
- **A strict-CSP host got an unstyled widget.** The stylesheet was injected as
|
|
409
|
+
an inline `<style>`, which a host with a strict `style-src` and no
|
|
410
|
+
`'unsafe-inline'` drops silently: the component mounted, functioned, and
|
|
411
|
+
rendered with no styling at all, and nothing in the console pointed at why.
|
|
412
|
+
It is attached with `adoptedStyleSheets` instead, which carries no
|
|
413
|
+
inline-style origin.
|
|
414
|
+
|
|
415
|
+
The sheet is per instance rather than shared at module scope. A shared one
|
|
416
|
+
would also stop re-parsing the stylesheet once per mount, but a module-level
|
|
417
|
+
singleton is what this package forbids, and per instance is no worse than the
|
|
418
|
+
`<style>` it replaces.
|
|
419
|
+
|
|
420
|
+
All three came out of a **survey of how other products build chat**, not a
|
|
421
|
+
review. Two review passes and a full audit wave went over this component
|
|
422
|
+
without surfacing any of them, because each is invisible unless you ask how
|
|
423
|
+
everyone else does it.
|
|
424
|
+
|
|
10
425
|
## [0.28.0] — 2026-08-26
|
|
11
426
|
|
|
12
427
|
### Added
|
|
@@ -1950,7 +2365,8 @@ hosts that both arrange the page the way it expects.
|
|
|
1950
2365
|
### Notes
|
|
1951
2366
|
- First release — exercising the automated npm OIDC publish pipeline end-to-end.
|
|
1952
2367
|
|
|
1953
|
-
[Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.
|
|
2368
|
+
[Unreleased]: https://github.com/Artui/ag-ui-web-component/compare/v0.29.0...HEAD
|
|
2369
|
+
[0.29.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.28.0...v0.29.0
|
|
1954
2370
|
[0.28.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.27.0...v0.28.0
|
|
1955
2371
|
[0.27.0]: https://github.com/Artui/ag-ui-web-component/compare/v0.26.1...v0.27.0
|
|
1956
2372
|
[0.26.1]: https://github.com/Artui/ag-ui-web-component/compare/v0.26.0...v0.26.1
|