@artooi/ag-ui-web-component 0.29.0 → 0.30.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 +199 -1
- package/README.md +196 -33
- package/dist/ag-ui-web-component.bundle.js +211 -28
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +60 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +25 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +18 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +578 -35
- package/dist/index.js.map +4 -4
- package/dist/ui/message_actions.d.ts +14 -4
- package/dist/ui/message_actions.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/subagent_panel.d.ts +92 -0
- package/dist/ui/subagent_panel.d.ts.map +1 -0
- package/dist/ui/subagent_update.d.ts +19 -0
- package/dist/ui/subagent_update.d.ts.map +1 -0
- package/dist/ui/tool_call_card.d.ts +73 -1
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +10 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +63 -1
- package/src/core/ag_ui_chat.ts +180 -26
- package/src/core/agui_client.ts +26 -2
- package/src/index.ts +4 -0
- package/src/ui/message_actions.ts +20 -8
- package/src/ui/styles.ts +183 -0
- package/src/ui/subagent_panel.ts +213 -0
- package/src/ui/subagent_update.ts +80 -0
- package/src/ui/tool_call_card.ts +129 -3
- package/src/ui/ui_strings.ts +15 -0
- package/src/version.ts +1 -1
|
@@ -8,11 +8,16 @@ export interface MessageActionsOptions {
|
|
|
8
8
|
/** Localized strings. */
|
|
9
9
|
strings: UiStrings;
|
|
10
10
|
/**
|
|
11
|
-
* The text Copy puts on the clipboard.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The text Copy puts on the clipboard. Absent means no copy button.
|
|
12
|
+
*
|
|
13
|
+
* A function rather than a string because a bubble's content is rewritten
|
|
14
|
+
* while it streams, and the bar is attached to the element rather than to a
|
|
15
|
+
* snapshot of it. Optional for the same reason `onFeedback` is: what a button
|
|
16
|
+
* needs to do its job is also the statement that the button belongs here, so
|
|
17
|
+
* there is no second flag saying the same thing and no way for the two to
|
|
18
|
+
* disagree.
|
|
14
19
|
*/
|
|
15
|
-
text
|
|
20
|
+
text?: () => string;
|
|
16
21
|
/**
|
|
17
22
|
* Report a rating for this message. Absent means no feedback buttons.
|
|
18
23
|
*
|
|
@@ -35,13 +40,21 @@ export interface MessageActionsOptions {
|
|
|
35
40
|
*
|
|
36
41
|
* Idempotent -- a bubble already given a bar is skipped, so a re-render or a
|
|
37
42
|
* second call cannot stack rows.
|
|
43
|
+
*
|
|
44
|
+
* Each button is present because the option it needs was passed: `text` for
|
|
45
|
+
* Copy, `onFeedback` for the rating pair. Passing neither builds an empty row,
|
|
46
|
+
* which is a caller's mistake rather than a state to guard against -- the
|
|
47
|
+
* element skips the call entirely when a host has turned both off.
|
|
38
48
|
*/
|
|
39
49
|
export function attachMessageActions(bubble: HTMLElement, options: MessageActionsOptions): void {
|
|
40
50
|
if (existingBar(bubble) !== null) {
|
|
41
51
|
return;
|
|
42
52
|
}
|
|
43
53
|
const bar = messageActionBar(bubble, options.strings);
|
|
44
|
-
|
|
54
|
+
const text = options.text;
|
|
55
|
+
if (text !== undefined) {
|
|
56
|
+
bar.appendChild(copyButton(options.strings, text));
|
|
57
|
+
}
|
|
45
58
|
if (options.onFeedback !== undefined) {
|
|
46
59
|
bar.append(
|
|
47
60
|
feedbackButton("up", options.strings.feedbackUp, options.onFeedback),
|
|
@@ -104,11 +117,10 @@ export function messageActionButton(
|
|
|
104
117
|
return button;
|
|
105
118
|
}
|
|
106
119
|
|
|
107
|
-
function copyButton(
|
|
108
|
-
const { strings } = options;
|
|
120
|
+
function copyButton(strings: UiStrings, text: () => string): HTMLButtonElement {
|
|
109
121
|
const button = messageActionButton("copy", strings.copyMessage, "⎘");
|
|
110
122
|
button.addEventListener("click", () => {
|
|
111
|
-
void navigator.clipboard.writeText(
|
|
123
|
+
void navigator.clipboard.writeText(text()).then(
|
|
112
124
|
() => flash(button, strings.copied, strings.copyMessage),
|
|
113
125
|
// A denied clipboard permission is the common case, not an exception:
|
|
114
126
|
// say so on the button rather than throwing into an unhandled rejection.
|
package/src/ui/styles.ts
CHANGED
|
@@ -1195,6 +1195,21 @@ export const STYLES = `
|
|
|
1195
1195
|
color: var(--_fg);
|
|
1196
1196
|
}
|
|
1197
1197
|
|
|
1198
|
+
/* A region a host formatter took over, marked by the card. Preformatted
|
|
1199
|
+
whitespace is what makes the built-in block read as written, and it is the one
|
|
1200
|
+
thing a host cannot want: a table inherits it as mangled cell spacing, and a
|
|
1201
|
+
sentence as line breaks nobody typed.
|
|
1202
|
+
|
|
1203
|
+
Whitespace only. The card's own face, frame, padding and scroll cap stay,
|
|
1204
|
+
because the card is one visual object -- the head row and the status pill are
|
|
1205
|
+
monospaced too -- and a region that dropped the family would be the only part
|
|
1206
|
+
of it wearing a different one. A host that wants that restyles the
|
|
1207
|
+
tool-card-result part, which does not need the formatter at all. */
|
|
1208
|
+
.tool-call-args[data-formatted],
|
|
1209
|
+
.tool-call-result[data-formatted] {
|
|
1210
|
+
white-space: normal;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1198
1213
|
/* Display modes are pure visibility over one DOM shape, selected from the host
|
|
1199
1214
|
attribute rather than a value stamped on the card at build time, so flipping
|
|
1200
1215
|
data-tool-display re-styles cards already on screen. See ToolCallCard.
|
|
@@ -1259,6 +1274,174 @@ export const STYLES = `
|
|
|
1259
1274
|
display: flex;
|
|
1260
1275
|
}
|
|
1261
1276
|
|
|
1277
|
+
/* A delegated sub-agent's progress, inside the card that delegated. Empty on
|
|
1278
|
+
every card that delegated nothing, so it collapses rather than adding a gap
|
|
1279
|
+
to each one -- the same shape the approval slot uses. */
|
|
1280
|
+
.tool-call-subagent:empty {
|
|
1281
|
+
display: none;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.tool-call-subagent {
|
|
1285
|
+
display: flex;
|
|
1286
|
+
flex-direction: column;
|
|
1287
|
+
gap: 4px;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
.subagent {
|
|
1291
|
+
display: flex;
|
|
1292
|
+
flex-direction: column;
|
|
1293
|
+
gap: 4px;
|
|
1294
|
+
min-width: 0;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/* The collapsed row is the status and the expander at once, which is what keeps
|
|
1298
|
+
a ten-step child one row until somebody opens it. Full width and left-aligned,
|
|
1299
|
+
because it is a line of the card rather than a button on it. */
|
|
1300
|
+
.subagent-row {
|
|
1301
|
+
display: flex;
|
|
1302
|
+
align-items: center;
|
|
1303
|
+
gap: 6px;
|
|
1304
|
+
width: 100%;
|
|
1305
|
+
box-sizing: border-box;
|
|
1306
|
+
padding: 2px 0;
|
|
1307
|
+
border: none;
|
|
1308
|
+
background: none;
|
|
1309
|
+
font: inherit;
|
|
1310
|
+
text-align: left;
|
|
1311
|
+
color: var(--_muted);
|
|
1312
|
+
cursor: pointer;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/* Nothing behind the row yet -- a delegation that failed before calling
|
|
1316
|
+
anything. Drop the affordances rather than offer a control that expands onto
|
|
1317
|
+
an empty region, which is the refusal the card's own toggle already makes. */
|
|
1318
|
+
.subagent-row:disabled {
|
|
1319
|
+
cursor: default;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
.subagent-row::after {
|
|
1323
|
+
content: "▸";
|
|
1324
|
+
flex: none;
|
|
1325
|
+
margin-left: auto;
|
|
1326
|
+
color: var(--_accent);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
.subagent-row[aria-expanded="true"]::after {
|
|
1330
|
+
content: "▾";
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
.subagent-row:disabled::after {
|
|
1334
|
+
display: none;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/* Empty in the DOM; the glyph is drawn here from the panel's data-phase, so a
|
|
1338
|
+
host re-themes it through the same tool-icon custom properties the card uses. */
|
|
1339
|
+
.subagent-icon {
|
|
1340
|
+
flex: none;
|
|
1341
|
+
box-sizing: border-box;
|
|
1342
|
+
display: inline-flex;
|
|
1343
|
+
align-items: center;
|
|
1344
|
+
justify-content: center;
|
|
1345
|
+
width: 10px;
|
|
1346
|
+
height: 10px;
|
|
1347
|
+
font-size: 10px;
|
|
1348
|
+
line-height: 1;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/* Anything that is not a terminal phase is the child still working. Selected by
|
|
1352
|
+
what it is not, so a phase this client has not heard of still spins rather
|
|
1353
|
+
than rendering as a blank. */
|
|
1354
|
+
.subagent[data-phase]:not([data-phase="finished"]):not([data-phase="failed"]) .subagent-icon {
|
|
1355
|
+
border: 2px solid var(--_muted);
|
|
1356
|
+
border-top-color: transparent;
|
|
1357
|
+
border-radius: 50%;
|
|
1358
|
+
animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.subagent[data-phase="finished"] .subagent-icon::before {
|
|
1362
|
+
content: var(--_tool-icon-done);
|
|
1363
|
+
color: var(--_success);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
.subagent[data-phase="failed"] .subagent-icon::before {
|
|
1367
|
+
content: var(--_tool-icon-error);
|
|
1368
|
+
color: var(--_danger);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1372
|
+
.subagent .subagent-icon {
|
|
1373
|
+
animation: none;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/* The server's own pre-rendered line. Shrinks and wraps rather than pushing the
|
|
1378
|
+
chevron out of the card, which is what a fixed-width sibling in a flex row
|
|
1379
|
+
does to a panel at sidebar width. */
|
|
1380
|
+
.subagent-status {
|
|
1381
|
+
flex: 1 1 auto;
|
|
1382
|
+
min-width: 0;
|
|
1383
|
+
overflow-wrap: anywhere;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/* The child's own calls. Indented and ruled, so the nesting is visible without
|
|
1387
|
+
a second card frame around it. */
|
|
1388
|
+
.subagent-steps {
|
|
1389
|
+
display: flex;
|
|
1390
|
+
flex-direction: column;
|
|
1391
|
+
gap: 2px;
|
|
1392
|
+
margin-left: 4px;
|
|
1393
|
+
padding-left: 10px;
|
|
1394
|
+
border-left: 1px solid var(--_border);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
.subagent-steps[hidden] {
|
|
1398
|
+
display: none;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
.subagent-step {
|
|
1402
|
+
display: flex;
|
|
1403
|
+
align-items: center;
|
|
1404
|
+
gap: 6px;
|
|
1405
|
+
min-width: 0;
|
|
1406
|
+
color: var(--_muted);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
.subagent-step-icon {
|
|
1410
|
+
flex: none;
|
|
1411
|
+
box-sizing: border-box;
|
|
1412
|
+
display: inline-flex;
|
|
1413
|
+
align-items: center;
|
|
1414
|
+
justify-content: center;
|
|
1415
|
+
width: 8px;
|
|
1416
|
+
height: 8px;
|
|
1417
|
+
font-size: 9px;
|
|
1418
|
+
line-height: 1;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/* No outcome yet: the wire says null while the call is in flight, and the
|
|
1422
|
+
absence of the attribute is how that arrives here. A hollow ring, not a
|
|
1423
|
+
spinner -- several can be on screen at once and the row above already spins. */
|
|
1424
|
+
.subagent-step:not([data-ok]) .subagent-step-icon {
|
|
1425
|
+
border: 1px solid var(--_muted);
|
|
1426
|
+
border-radius: 50%;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
.subagent-step[data-ok="true"] .subagent-step-icon::before {
|
|
1430
|
+
content: var(--_tool-icon-done);
|
|
1431
|
+
color: var(--_success);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
.subagent-step[data-ok="false"] .subagent-step-icon::before {
|
|
1435
|
+
content: var(--_tool-icon-error);
|
|
1436
|
+
color: var(--_danger);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.subagent-step-name {
|
|
1440
|
+
flex: 1 1 auto;
|
|
1441
|
+
min-width: 0;
|
|
1442
|
+
overflow-wrap: anywhere;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1262
1445
|
.tool-call-toggle {
|
|
1263
1446
|
align-self: flex-start;
|
|
1264
1447
|
border: none;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { SUBAGENT_PHASE } from "../constants.js";
|
|
2
|
+
import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
3
|
+
|
|
4
|
+
/** One phase of a delegation's life, as the server spells it. */
|
|
5
|
+
export type SubAgentPhase = (typeof SUBAGENT_PHASE)[keyof typeof SUBAGENT_PHASE];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One tool call the child made, as the two tool phases report it.
|
|
9
|
+
*
|
|
10
|
+
* `ok` is a tri-state and stays one here rather than collapsing to a boolean:
|
|
11
|
+
* `null` is the call in flight, `true` a result the child accepted, `false` a
|
|
12
|
+
* result that came back to it. Flattening `null` into `false` would draw a
|
|
13
|
+
* running call as a failed one for as long as it runs.
|
|
14
|
+
*/
|
|
15
|
+
export interface SubAgentTool {
|
|
16
|
+
readonly toolCallId: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly ok: boolean | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One narrowed progress announcement about a delegation.
|
|
23
|
+
*
|
|
24
|
+
* Every field but `phase` and `delegationId` is nullable because the wire's
|
|
25
|
+
* `value` is `unknown` and a malformed announcement must not take the run down
|
|
26
|
+
* with it. `null` means "said nothing about this", never "said nothing was
|
|
27
|
+
* there" — the panel leaves what it already shows alone.
|
|
28
|
+
*/
|
|
29
|
+
export interface SubAgentUpdate {
|
|
30
|
+
/** The **parent's** `delegate_task` tool-call id, not the child's run id. */
|
|
31
|
+
readonly delegationId: string;
|
|
32
|
+
/** The child agent's name, for a host that wants to style or select by it. */
|
|
33
|
+
readonly agent: string | null;
|
|
34
|
+
readonly phase: SubAgentPhase;
|
|
35
|
+
/** The server's pre-rendered line. The collapsed row needs nothing else. */
|
|
36
|
+
readonly status: string | null;
|
|
37
|
+
/** Present on the two tool phases only. */
|
|
38
|
+
readonly tool: SubAgentTool | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The nested surface for one delegation: a collapsed status row that expands
|
|
43
|
+
* onto the child agent's own tool calls.
|
|
44
|
+
*
|
|
45
|
+
* ## Where it goes and why
|
|
46
|
+
*
|
|
47
|
+
* Into {@link ToolCallCard.subagentSlot} — the card the parent's own
|
|
48
|
+
* `delegate_task` call already drew. The wire keys progress on the *parent's*
|
|
49
|
+
* tool-call id, so the thing being narrated is already on screen; a floating
|
|
50
|
+
* element would have duplicated its identity and then had to explain the
|
|
51
|
+
* relationship. Attaching instead means the delegation reuses how tool cards
|
|
52
|
+
* already behave, and there is no second visual language to learn.
|
|
53
|
+
*
|
|
54
|
+
* ## The shape
|
|
55
|
+
*
|
|
56
|
+
* One row per delegation, live, carrying nothing but the server's own `status`
|
|
57
|
+
* line — which is what makes a ten-step child cost one row until somebody opens
|
|
58
|
+
* it. Two alternatives were rejected on the way here and both are worth naming:
|
|
59
|
+
* a bare status line is cheaper and gives up the detail entirely, and inline
|
|
60
|
+
* child cards in the transcript interleave parent and child with nothing marking
|
|
61
|
+
* whose is whose, in an order the persisted transcript will not reproduce.
|
|
62
|
+
*
|
|
63
|
+
* A child's steps are keyed by the child's own `toolCallId`, so the `tool_call`
|
|
64
|
+
* that opens one and the `tool_result` that settles it are the same row updated
|
|
65
|
+
* in place rather than two rows stacked.
|
|
66
|
+
*
|
|
67
|
+
* ## What it never does
|
|
68
|
+
*
|
|
69
|
+
* It never renders failure text. A `failed` phase carries none, on purpose; the
|
|
70
|
+
* detail arrives on the ordinary `TOOL_CALL_RESULT` and lands in the same card's
|
|
71
|
+
* result region, a few pixels below. Anything invented here would be this
|
|
72
|
+
* component guessing at words the server declined to send.
|
|
73
|
+
*
|
|
74
|
+
* Nothing here is persisted: the events ride the imperative carrier, so a thread
|
|
75
|
+
* restore rebuilds the tool card and not the delegation under it.
|
|
76
|
+
*
|
|
77
|
+
* Pure DOM, like the other widgets: the host appends {@link element}, and all
|
|
78
|
+
* chrome text comes from {@link UiStrings}. The status line is server text and
|
|
79
|
+
* is set with `textContent`, never parsed as markup.
|
|
80
|
+
*/
|
|
81
|
+
export class SubAgentPanel {
|
|
82
|
+
/** The panel's root; append this into the delegating card's slot. */
|
|
83
|
+
readonly element: HTMLDivElement;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The collapsed row, which is the expander as well as the status.
|
|
87
|
+
*
|
|
88
|
+
* Disabled while the child has called nothing, so a delegation that failed
|
|
89
|
+
* before it started offers no control that expands onto an empty region —
|
|
90
|
+
* the same refusal the card's own Details toggle already makes.
|
|
91
|
+
*/
|
|
92
|
+
readonly #row: HTMLButtonElement;
|
|
93
|
+
readonly #status: HTMLSpanElement;
|
|
94
|
+
readonly #steps: HTMLDivElement;
|
|
95
|
+
/** The child's tool calls, keyed by the child's own call id. */
|
|
96
|
+
readonly #stepRows = new Map<string, HTMLDivElement>();
|
|
97
|
+
|
|
98
|
+
constructor(strings: UiStrings = DEFAULT_UI_STRINGS) {
|
|
99
|
+
this.element = document.createElement("div");
|
|
100
|
+
this.element.className = "subagent";
|
|
101
|
+
this.element.setAttribute("part", "subagent");
|
|
102
|
+
|
|
103
|
+
this.#row = document.createElement("button");
|
|
104
|
+
this.#row.type = "button";
|
|
105
|
+
this.#row.className = "subagent-row";
|
|
106
|
+
this.#row.setAttribute("part", "subagent-row");
|
|
107
|
+
this.#row.setAttribute("aria-expanded", "false");
|
|
108
|
+
this.#row.disabled = true;
|
|
109
|
+
|
|
110
|
+
// Left empty in the DOM, like the tool card's: the shadow CSS draws a
|
|
111
|
+
// spinner or a settled mark from the panel's data-phase, so a host themes
|
|
112
|
+
// the glyph without either side reaching into the other's stylesheet.
|
|
113
|
+
const icon = document.createElement("span");
|
|
114
|
+
icon.className = "subagent-icon";
|
|
115
|
+
icon.setAttribute("part", "subagent-icon");
|
|
116
|
+
icon.setAttribute("aria-hidden", "true");
|
|
117
|
+
|
|
118
|
+
this.#status = document.createElement("span");
|
|
119
|
+
this.#status.className = "subagent-status";
|
|
120
|
+
this.#status.setAttribute("part", "subagent-status");
|
|
121
|
+
// Seeded rather than left blank: an announcement whose status field is
|
|
122
|
+
// unusable must still leave a readable row, since the row is the control.
|
|
123
|
+
this.#status.textContent = strings.subAgentWorking;
|
|
124
|
+
|
|
125
|
+
this.#row.append(icon, this.#status);
|
|
126
|
+
|
|
127
|
+
this.#steps = document.createElement("div");
|
|
128
|
+
this.#steps.className = "subagent-steps";
|
|
129
|
+
this.#steps.setAttribute("part", "subagent-steps");
|
|
130
|
+
this.#steps.setAttribute("role", "list");
|
|
131
|
+
this.#steps.setAttribute("aria-label", strings.subAgentSteps);
|
|
132
|
+
this.#steps.hidden = true;
|
|
133
|
+
|
|
134
|
+
// The attribute is the state, as it is on the tool card: one place holds
|
|
135
|
+
// whether the region is open, and it is the one a screen reader reads.
|
|
136
|
+
this.#row.addEventListener("click", () => {
|
|
137
|
+
this.#setExpanded(this.#row.getAttribute("aria-expanded") !== "true");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
this.element.append(this.#row, this.#steps);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Fold one announcement in.
|
|
145
|
+
*
|
|
146
|
+
* Every field is applied only when the update actually carried it, so a phase
|
|
147
|
+
* that says nothing about the agent or the status leaves both as they stand.
|
|
148
|
+
* That is what lets `finished` be two keys wide on the wire without blanking
|
|
149
|
+
* the row it closes.
|
|
150
|
+
*/
|
|
151
|
+
report(update: SubAgentUpdate): void {
|
|
152
|
+
this.element.setAttribute("data-phase", update.phase);
|
|
153
|
+
if (update.agent !== null) {
|
|
154
|
+
this.element.setAttribute("data-agent", update.agent);
|
|
155
|
+
}
|
|
156
|
+
if (update.status !== null) {
|
|
157
|
+
this.#status.textContent = update.status;
|
|
158
|
+
}
|
|
159
|
+
if (update.tool !== null) {
|
|
160
|
+
this.#recordStep(update.tool);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Open or settle one of the child's calls, keyed by its own id.
|
|
166
|
+
*
|
|
167
|
+
* The absence of `data-ok` is what "still running" looks like, mirroring the
|
|
168
|
+
* wire's `null` rather than inventing a third value for it — so the attribute
|
|
169
|
+
* is removed on the way in and written on the way out.
|
|
170
|
+
*/
|
|
171
|
+
#recordStep(tool: SubAgentTool): void {
|
|
172
|
+
const row = this.#stepRows.get(tool.toolCallId) ?? this.#createStep(tool);
|
|
173
|
+
if (tool.ok === null) {
|
|
174
|
+
row.removeAttribute("data-ok");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
row.setAttribute("data-ok", String(tool.ok));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#createStep(tool: SubAgentTool): HTMLDivElement {
|
|
181
|
+
const row = document.createElement("div");
|
|
182
|
+
row.className = "subagent-step";
|
|
183
|
+
row.setAttribute("part", "subagent-step");
|
|
184
|
+
row.setAttribute("role", "listitem");
|
|
185
|
+
row.setAttribute("data-tool-call-id", tool.toolCallId);
|
|
186
|
+
|
|
187
|
+
const icon = document.createElement("span");
|
|
188
|
+
icon.className = "subagent-step-icon";
|
|
189
|
+
icon.setAttribute("part", "subagent-step-icon");
|
|
190
|
+
icon.setAttribute("aria-hidden", "true");
|
|
191
|
+
|
|
192
|
+
const name = document.createElement("span");
|
|
193
|
+
name.className = "subagent-step-name";
|
|
194
|
+
name.setAttribute("part", "subagent-step-name");
|
|
195
|
+
// The child's raw tool name. Not prettified and not looked up in the tool
|
|
196
|
+
// catalog: a sub-agent's tools are its own and never reached the browser's
|
|
197
|
+
// schema, and the status line above quotes the same raw name, so relabelling
|
|
198
|
+
// here would make the two lines disagree about one call.
|
|
199
|
+
name.textContent = tool.name;
|
|
200
|
+
|
|
201
|
+
row.append(icon, name);
|
|
202
|
+
this.#steps.appendChild(row);
|
|
203
|
+
this.#stepRows.set(tool.toolCallId, row);
|
|
204
|
+
// There is something behind the row now, so it becomes a control.
|
|
205
|
+
this.#row.disabled = false;
|
|
206
|
+
return row;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#setExpanded(expanded: boolean): void {
|
|
210
|
+
this.#steps.hidden = !expanded;
|
|
211
|
+
this.#row.setAttribute("aria-expanded", String(expanded));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow one `ag_ui.subagent` payload into a {@link SubAgentUpdate}.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the panel for the reason `chartSpecFrom` is kept out of the
|
|
5
|
+
* renderer: the panel's job is drawing, and a value that reaches it has already
|
|
6
|
+
* been vouched for.
|
|
7
|
+
*
|
|
8
|
+
* Defensive about the payload, not about the name. A `CUSTOM` event's `value` is
|
|
9
|
+
* `unknown` by the protocol, so a server can put anything there, and a malformed
|
|
10
|
+
* announcement must not take a run down with it — the same rule the invalidation
|
|
11
|
+
* channel applies to the same field. What is refused here is only what cannot be
|
|
12
|
+
* rendered at all: without a `delegationId` there is no card to attach to, and
|
|
13
|
+
* without a known `phase` there is no state to be in. Everything else degrades to
|
|
14
|
+
* `null`, which the panel reads as "said nothing about this".
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { SUBAGENT_PHASE } from "../constants.js";
|
|
18
|
+
import type { SubAgentPhase, SubAgentTool, SubAgentUpdate } from "./subagent_panel.js";
|
|
19
|
+
|
|
20
|
+
const PHASES: readonly string[] = Object.values(SUBAGENT_PHASE);
|
|
21
|
+
|
|
22
|
+
/** A record view of `value`, or `null` for anything that is not an object. */
|
|
23
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
24
|
+
// `typeof null` is "object", and an array is one too — neither carries the
|
|
25
|
+
// keys below, and both arrive from a JSON decoder without any warning.
|
|
26
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return value as Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A non-empty string, or `null`. */
|
|
33
|
+
function asText(value: unknown): string | null {
|
|
34
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The `tool` record the two tool phases carry.
|
|
39
|
+
*
|
|
40
|
+
* All-or-nothing: the contract states all three keys on every tool phase, so a
|
|
41
|
+
* partial record is a payload this client does not understand rather than a step
|
|
42
|
+
* to draw half of. A step row keyed by an empty id would also collide with the
|
|
43
|
+
* next one, silently merging two of the child's calls into one row.
|
|
44
|
+
*/
|
|
45
|
+
function asTool(value: unknown): SubAgentTool | null {
|
|
46
|
+
const record = asRecord(value);
|
|
47
|
+
if (record === null) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const toolCallId = asText(record["toolCallId"]);
|
|
51
|
+
const name = asText(record["name"]);
|
|
52
|
+
const ok = record["ok"];
|
|
53
|
+
if (toolCallId === null || name === null) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (ok !== null && typeof ok !== "boolean") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return { toolCallId, name, ok };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Narrow an `ag_ui.subagent` `CUSTOM` value, or `null` if it cannot be drawn. */
|
|
63
|
+
export function subAgentUpdate(value: unknown): SubAgentUpdate | null {
|
|
64
|
+
const record = asRecord(value);
|
|
65
|
+
if (record === null) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const delegationId = asText(record["delegationId"]);
|
|
69
|
+
const phase = record["phase"];
|
|
70
|
+
if (delegationId === null || typeof phase !== "string" || !PHASES.includes(phase)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
delegationId,
|
|
75
|
+
phase: phase as SubAgentPhase,
|
|
76
|
+
agent: asText(record["agent"]),
|
|
77
|
+
status: asText(record["status"]),
|
|
78
|
+
tool: asTool(record["tool"]),
|
|
79
|
+
};
|
|
80
|
+
}
|