@artooi/ag-ui-web-component 0.28.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 +615 -1
- package/README.md +564 -35
- package/dist/ag-ui-web-component.bundle.js +491 -50
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +129 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +232 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +56 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2081 -98
- 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 +56 -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/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/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 +81 -1
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +50 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +138 -1
- package/src/core/ag_ui_chat.ts +1081 -73
- package/src/core/agui_client.ts +89 -2
- package/src/index.ts +43 -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 +170 -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 +410 -0
- package/src/ui/subagent_panel.ts +213 -0
- package/src/ui/subagent_update.ts +80 -0
- package/src/ui/suggestion_chips.ts +73 -0
- package/src/ui/thread_drawer.ts +22 -2
- package/src/ui/tool_call_card.ts +138 -3
- package/src/ui/ui_strings.ts +75 -0
- package/src/version.ts +1 -1
package/src/ui/styles.ts
CHANGED
|
@@ -571,6 +571,7 @@ export const STYLES = `
|
|
|
571
571
|
opacity: 0;
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
+
:host([collapsed]:is([placement="embedded"], [placement="page"])) .messages-wrap,
|
|
574
575
|
:host([collapsed]:is([placement="embedded"], [placement="page"])) .messages,
|
|
575
576
|
:host([collapsed]:is([placement="embedded"], [placement="page"])) .input-row,
|
|
576
577
|
:host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-chips,
|
|
@@ -579,9 +580,113 @@ export const STYLES = `
|
|
|
579
580
|
display: none;
|
|
580
581
|
}
|
|
581
582
|
|
|
583
|
+
/* Jump-to-latest: shown only once the reader has scrolled away *and* missed
|
|
584
|
+
something. Anchored to the panel rather than the list so it does not scroll
|
|
585
|
+
with the content it is offering to scroll to. */
|
|
586
|
+
/* The transcript's own box, and the only one whose foot is the transcript's
|
|
587
|
+
foot. The panel's foot is below the composer, the chips and the footer. */
|
|
588
|
+
.messages-wrap {
|
|
589
|
+
position: relative;
|
|
590
|
+
flex: 1;
|
|
591
|
+
min-height: 0;
|
|
592
|
+
display: flex;
|
|
593
|
+
flex-direction: column;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.jump-latest {
|
|
597
|
+
position: absolute;
|
|
598
|
+
left: 50%;
|
|
599
|
+
transform: translateX(-50%);
|
|
600
|
+
bottom: var(--_pad);
|
|
601
|
+
z-index: 2;
|
|
602
|
+
display: none;
|
|
603
|
+
align-items: center;
|
|
604
|
+
gap: 0.35em;
|
|
605
|
+
padding: 0.4em 0.9em;
|
|
606
|
+
border: 1px solid var(--_border);
|
|
607
|
+
border-radius: 999px;
|
|
608
|
+
/* A raised surface, not the panel's own background. Reusing --_bg made the
|
|
609
|
+
pill the same colour as everything behind it, leaving a 1px border and a
|
|
610
|
+
shadow to carry the whole affordance -- and a dark-on-dark shadow carries
|
|
611
|
+
nothing. --_hover is the token that already means "lifted off the panel",
|
|
612
|
+
and it separates in both themes without competing with the accent the send
|
|
613
|
+
button owns. */
|
|
614
|
+
background: var(--_hover);
|
|
615
|
+
color: var(--_text);
|
|
616
|
+
font: inherit;
|
|
617
|
+
font-size: 0.85em;
|
|
618
|
+
cursor: pointer;
|
|
619
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.jump-latest[data-missed="true"] {
|
|
623
|
+
display: flex;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.jump-latest:hover {
|
|
627
|
+
border-color: var(--_accent);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* The offer to quote a selection. Positioned in script against the transcript
|
|
631
|
+
box, which is the only ancestor whose top and foot are the transcript's --
|
|
632
|
+
the same reason .jump-latest lives here. The translate is the half the
|
|
633
|
+
script does not do: script sets the point the offer hangs from, CSS decides
|
|
634
|
+
which corner of the offer that point is. */
|
|
635
|
+
.quote-selection {
|
|
636
|
+
position: absolute;
|
|
637
|
+
z-index: 2;
|
|
638
|
+
transform: translate(-50%, -100%);
|
|
639
|
+
padding: 0.25em 0.7em;
|
|
640
|
+
border: 1px solid var(--_border);
|
|
641
|
+
border-radius: 999px;
|
|
642
|
+
background: var(--_hover);
|
|
643
|
+
color: var(--_text);
|
|
644
|
+
font: inherit;
|
|
645
|
+
font-size: 0.8em;
|
|
646
|
+
line-height: 1.6;
|
|
647
|
+
white-space: nowrap;
|
|
648
|
+
cursor: pointer;
|
|
649
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/* Flipped under the selection when there was no room above it. Only the
|
|
653
|
+
vertical half of the translate changes: it still hangs from its own centre
|
|
654
|
+
horizontally. */
|
|
655
|
+
.quote-selection[data-below="true"] {
|
|
656
|
+
transform: translate(-50%, 0);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.quote-selection:hover {
|
|
660
|
+
border-color: var(--_accent);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* Screen-reader-only status region. Off-screen rather than display:none or
|
|
664
|
+
visibility:hidden, both of which take the element out of the accessibility
|
|
665
|
+
tree entirely -- a hidden live region announces nothing at all, which is the
|
|
666
|
+
classic way this pattern is written wrong.
|
|
667
|
+
|
|
668
|
+
The 1px box with clip-path, rather than width/height 0, is the shape that
|
|
669
|
+
survives: a zero-sized element is dropped from the tree by some engines. */
|
|
670
|
+
.sr-only {
|
|
671
|
+
position: absolute;
|
|
672
|
+
width: 1px;
|
|
673
|
+
height: 1px;
|
|
674
|
+
margin: -1px;
|
|
675
|
+
padding: 0;
|
|
676
|
+
border: 0;
|
|
677
|
+
overflow: hidden;
|
|
678
|
+
white-space: nowrap;
|
|
679
|
+
clip-path: inset(50%);
|
|
680
|
+
}
|
|
681
|
+
|
|
582
682
|
.messages {
|
|
583
683
|
flex: 1;
|
|
584
684
|
overflow-y: auto;
|
|
685
|
+
/* The browser's own scroll anchoring competes with the scroller for the same
|
|
686
|
+
job and wins unpredictably -- it can hold the view still exactly when we
|
|
687
|
+
want to follow. Turned off so following is decided in one place. Safari
|
|
688
|
+
does not implement it, which is itself a reason not to depend on it. */
|
|
689
|
+
overflow-anchor: none;
|
|
585
690
|
padding: var(--_pad);
|
|
586
691
|
display: flex;
|
|
587
692
|
flex-direction: column;
|
|
@@ -1090,6 +1195,21 @@ export const STYLES = `
|
|
|
1090
1195
|
color: var(--_fg);
|
|
1091
1196
|
}
|
|
1092
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
|
+
|
|
1093
1213
|
/* Display modes are pure visibility over one DOM shape, selected from the host
|
|
1094
1214
|
attribute rather than a value stamped on the card at build time, so flipping
|
|
1095
1215
|
data-tool-display re-styles cards already on screen. See ToolCallCard.
|
|
@@ -1154,6 +1274,174 @@ export const STYLES = `
|
|
|
1154
1274
|
display: flex;
|
|
1155
1275
|
}
|
|
1156
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
|
+
|
|
1157
1445
|
.tool-call-toggle {
|
|
1158
1446
|
align-self: flex-start;
|
|
1159
1447
|
border: none;
|
|
@@ -1747,6 +2035,7 @@ export const STYLES = `
|
|
|
1747
2035
|
|
|
1748
2036
|
.confirm-actions {
|
|
1749
2037
|
display: flex;
|
|
2038
|
+
flex-wrap: wrap;
|
|
1750
2039
|
gap: 8px;
|
|
1751
2040
|
justify-content: flex-end;
|
|
1752
2041
|
}
|
|
@@ -1773,6 +2062,99 @@ export const STYLES = `
|
|
|
1773
2062
|
color: #ffffff;
|
|
1774
2063
|
}
|
|
1775
2064
|
|
|
2065
|
+
/* The session waiver. Deliberately the quietest of the three: it is the widest
|
|
2066
|
+
decision on the card, so it should be reachable without being the one the eye
|
|
2067
|
+
lands on when the user means to say yes once. */
|
|
2068
|
+
.confirm-btn--always {
|
|
2069
|
+
font-weight: 500;
|
|
2070
|
+
opacity: 0.85;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
.confirm-btn--always:hover,
|
|
2074
|
+
.confirm-btn--always:focus-visible {
|
|
2075
|
+
opacity: 1;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
/* Editable arguments on an approval card. A plain field rather than a code
|
|
2079
|
+
editor: it holds the JSON a card already displays, and the only interaction
|
|
2080
|
+
is correcting a value before letting the call run. */
|
|
2081
|
+
.approval-edit {
|
|
2082
|
+
display: flex;
|
|
2083
|
+
flex-direction: column;
|
|
2084
|
+
gap: 6px;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
.approval-args {
|
|
2088
|
+
box-sizing: border-box;
|
|
2089
|
+
width: 100%;
|
|
2090
|
+
resize: vertical;
|
|
2091
|
+
border: 1px solid var(--_border);
|
|
2092
|
+
border-radius: 8px;
|
|
2093
|
+
padding: 8px;
|
|
2094
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2095
|
+
font-size: 0.85em;
|
|
2096
|
+
background: var(--_bg);
|
|
2097
|
+
color: var(--_fg);
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
.approval-args:focus-visible {
|
|
2101
|
+
border-color: var(--_accent);
|
|
2102
|
+
outline: none;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
.approval-error {
|
|
2106
|
+
font-size: 0.85em;
|
|
2107
|
+
color: var(--_danger);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
/* Message action row. Sits under a finished assistant bubble.
|
|
2111
|
+
|
|
2112
|
+
The wrap is insurance rather than a fix: these buttons are glyph-only, so at
|
|
2113
|
+
every width tested they fit on one line and removing the wrap changes
|
|
2114
|
+
nothing. It is here because the confirmation row one release earlier did
|
|
2115
|
+
overflow when it gained a third button, off the left edge and outside its own
|
|
2116
|
+
card, and the difference between the two rows is only that this one's labels
|
|
2117
|
+
are icons today. */
|
|
2118
|
+
.message-actions {
|
|
2119
|
+
display: flex;
|
|
2120
|
+
flex-wrap: wrap;
|
|
2121
|
+
gap: 4px;
|
|
2122
|
+
/* Negative, and that is the point. The answer group is a flex column with its
|
|
2123
|
+
own gap, so a positive margin here pushes the row further from the message
|
|
2124
|
+
it acts on than the next card is below it -- the buttons then read as
|
|
2125
|
+
belonging to whatever follows. Pulling back inside the gap is what makes
|
|
2126
|
+
them the message's own. */
|
|
2127
|
+
margin-top: -6px;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
.message-action {
|
|
2131
|
+
border: none;
|
|
2132
|
+
border-radius: 6px;
|
|
2133
|
+
padding: 2px 6px;
|
|
2134
|
+
font: inherit;
|
|
2135
|
+
line-height: 1.2;
|
|
2136
|
+
cursor: pointer;
|
|
2137
|
+
background: transparent;
|
|
2138
|
+
color: var(--_muted);
|
|
2139
|
+
opacity: 0.75;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
.message-action:hover,
|
|
2143
|
+
.message-action:focus-visible {
|
|
2144
|
+
opacity: 1;
|
|
2145
|
+
background: var(--_border);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
.message-action[aria-pressed="true"] {
|
|
2149
|
+
opacity: 1;
|
|
2150
|
+
color: var(--_accent);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
.message-action--confirmed {
|
|
2154
|
+
opacity: 1;
|
|
2155
|
+
color: var(--_accent);
|
|
2156
|
+
}
|
|
2157
|
+
|
|
1776
2158
|
/* Approval card — the server-side-tool gate (approve/deny an interrupt). */
|
|
1777
2159
|
.approval {
|
|
1778
2160
|
align-self: stretch;
|
|
@@ -1917,6 +2299,34 @@ export const STYLES = `
|
|
|
1917
2299
|
border-color: var(--_accent);
|
|
1918
2300
|
}
|
|
1919
2301
|
|
|
2302
|
+
/* Follow-up suggestion chips. Deliberately the skill chips' shape rather than a
|
|
2303
|
+
second chip vocabulary -- both are "a question you could ask", and the only
|
|
2304
|
+
difference is who chose it. Inside the transcript, so they scroll with the
|
|
2305
|
+
answer they follow instead of hovering above the composer. */
|
|
2306
|
+
.suggestions {
|
|
2307
|
+
display: flex;
|
|
2308
|
+
flex-wrap: wrap;
|
|
2309
|
+
gap: 6px;
|
|
2310
|
+
align-self: stretch;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
.suggestion-chip {
|
|
2314
|
+
border: 1px solid var(--_border);
|
|
2315
|
+
border-radius: 999px;
|
|
2316
|
+
padding: 4px 12px;
|
|
2317
|
+
font: inherit;
|
|
2318
|
+
font-size: 0.9em;
|
|
2319
|
+
text-align: left;
|
|
2320
|
+
cursor: pointer;
|
|
2321
|
+
background: var(--_assistant-bg);
|
|
2322
|
+
color: var(--_fg);
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
.suggestion-chip:hover,
|
|
2326
|
+
.suggestion-chip:focus-visible {
|
|
2327
|
+
border-color: var(--_accent);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
1920
2330
|
.skill-palette {
|
|
1921
2331
|
margin: 8px 12px 0;
|
|
1922
2332
|
display: flex;
|
|
@@ -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
|
+
}
|