@unofficialbox/box-open-elements 0.19.0 → 0.21.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/dist/components/actions/menu-item.d.ts +2 -0
- package/dist/components/actions/menu-item.js +4 -0
- package/dist/components/collections/card.js +6 -0
- package/dist/components/collections/check-list.d.ts +10 -0
- package/dist/components/collections/check-list.js +8 -0
- package/dist/components/collections/document-list.d.ts +10 -0
- package/dist/components/collections/document-list.js +8 -0
- package/dist/components/collections/fact-list.d.ts +10 -0
- package/dist/components/collections/fact-list.js +8 -0
- package/dist/components/collections/result-blocks.d.ts +54 -0
- package/dist/components/collections/result-blocks.js +74 -0
- package/dist/components/feedback/alert.d.ts +2 -0
- package/dist/components/feedback/alert.js +17 -13
- package/dist/components/feedback/progress-bar.d.ts +9 -0
- package/dist/components/feedback/progress-bar.js +29 -3
- package/dist/components/feedback/status-icon.d.ts +10 -0
- package/dist/components/feedback/status-icon.js +26 -0
- package/dist/components/feedback/toast.d.ts +10 -1
- package/dist/components/feedback/toast.js +69 -27
- package/dist/components/forms/number-input.d.ts +1 -0
- package/dist/components/forms/number-input.js +9 -6
- package/dist/components/forms/search-field.js +16 -0
- package/dist/components/overlays/context-menu.d.ts +2 -0
- package/dist/components/overlays/context-menu.js +7 -0
- package/dist/components/overlays/dialog.js +19 -3
- package/dist/components/overlays/popover.js +6 -1
- package/dist/element-maps.d.ts +49 -19
- package/dist/entries/agent-workspace.d.ts +1 -0
- package/dist/entries/agent-workspace.js +1 -0
- package/dist/entries/call-console.d.ts +1 -0
- package/dist/entries/call-console.js +1 -0
- package/dist/entries/check-list.d.ts +1 -0
- package/dist/entries/check-list.js +1 -0
- package/dist/entries/document-list.d.ts +1 -0
- package/dist/entries/document-list.js +1 -0
- package/dist/entries/fact-list.d.ts +1 -0
- package/dist/entries/fact-list.js +1 -0
- package/dist/entries/result-blocks.d.ts +1 -0
- package/dist/entries/result-blocks.js +1 -0
- package/dist/entries/run-summary.d.ts +1 -0
- package/dist/entries/run-summary.js +1 -0
- package/dist/entries/status-icon.d.ts +1 -0
- package/dist/entries/status-icon.js +1 -0
- package/dist/foundations/a11y/announcer.d.ts +12 -0
- package/dist/foundations/a11y/announcer.js +75 -0
- package/dist/foundations/a11y/focus.js +46 -25
- package/dist/foundations/a11y/index.d.ts +2 -0
- package/dist/foundations/a11y/index.js +1 -0
- package/dist/foundations/icons/box-iconography.d.ts +472 -472
- package/dist/foundations/icons/box-iconography.generated.d.ts +1151 -679
- package/dist/foundations/icons/box-iconography.generated.js +1006 -3588
- package/dist/foundations/icons/glyphs/index.d.ts +1 -0
- package/dist/foundations/icons/glyphs/index.js +2 -0
- package/dist/foundations/icons/glyphs/package.json +1 -0
- package/dist/foundations/motion/behaviors.d.ts +8 -0
- package/dist/foundations/motion/behaviors.js +24 -0
- package/dist/foundations/motion/index.d.ts +2 -0
- package/dist/foundations/motion/index.js +2 -0
- package/dist/foundations/motion/scroll-pin.d.ts +19 -0
- package/dist/foundations/motion/scroll-pin.js +60 -0
- package/dist/foundations/motion/tokens.d.ts +2 -0
- package/dist/foundations/motion/tokens.js +2 -0
- package/dist/foundations/overlay/positioning.js +7 -0
- package/dist/foundations/profiles/registry.d.ts +3 -0
- package/dist/foundations/profiles/registry.js +3 -0
- package/dist/foundations/profiles/types.d.ts +3 -0
- package/dist/foundations/status/index.d.ts +6 -0
- package/dist/foundations/status/index.js +35 -0
- package/dist/foundations/tokens/box-dark.js +4 -1
- package/dist/foundations/tokens/box-defaults.js +3 -0
- package/dist/foundations/tokens/registry.d.ts +3 -0
- package/dist/foundations/tokens/registry.js +3 -0
- package/dist/foundations/tokens/types.d.ts +3 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/patterns/agent-chat/agent-chat.d.ts +9 -0
- package/dist/patterns/agent-chat/agent-chat.js +146 -24
- package/dist/patterns/agent-chat/controller.d.ts +3 -1
- package/dist/patterns/agent-chat/controller.js +92 -16
- package/dist/patterns/agent-chat/index.d.ts +1 -0
- package/dist/patterns/agent-chat/index.js +1 -0
- package/dist/patterns/agent-chat/ndjson.d.ts +5 -0
- package/dist/patterns/agent-chat/ndjson.js +41 -0
- package/dist/patterns/agent-chat/types.d.ts +44 -1
- package/dist/patterns/agent-workspace/agent-workspace.d.ts +28 -0
- package/dist/patterns/agent-workspace/agent-workspace.js +215 -0
- package/dist/patterns/agent-workspace/controller.d.ts +38 -0
- package/dist/patterns/agent-workspace/controller.js +65 -0
- package/dist/patterns/agent-workspace/index.d.ts +2 -0
- package/dist/patterns/agent-workspace/index.js +2 -0
- package/dist/patterns/call-console/call-console.d.ts +20 -0
- package/dist/patterns/call-console/call-console.js +201 -0
- package/dist/patterns/call-console/controller.d.ts +23 -0
- package/dist/patterns/call-console/controller.js +50 -0
- package/dist/patterns/call-console/http-transport.d.ts +3 -0
- package/dist/patterns/call-console/http-transport.js +26 -0
- package/dist/patterns/call-console/index.d.ts +4 -0
- package/dist/patterns/call-console/index.js +4 -0
- package/dist/patterns/call-console/types.d.ts +32 -0
- package/dist/patterns/call-console/types.js +8 -0
- package/dist/patterns/content-explorer/adapters/action-menu.js +7 -5
- package/dist/patterns/content-explorer/adapters/toolbar.js +6 -2
- package/dist/patterns/content-uploader/content-uploader.js +2 -1
- package/dist/patterns/file-request/file-request-builder.js +3 -0
- package/dist/patterns/flow-builder/flow-builder.d.ts +48 -0
- package/dist/patterns/flow-builder/flow-builder.js +200 -0
- package/dist/patterns/flow-builder/index.d.ts +3 -0
- package/dist/patterns/flow-builder/index.js +3 -0
- package/dist/patterns/flow-builder/model.d.ts +33 -0
- package/dist/patterns/flow-builder/model.js +28 -0
- package/dist/patterns/flow-builder/primitives.d.ts +47 -0
- package/dist/patterns/flow-builder/primitives.js +192 -0
- package/dist/patterns/lineage/lineage-graph.js +26 -6
- package/dist/patterns/notifications/notification-inbox.js +19 -3
- package/dist/patterns/preview/annotation-inspector.js +2 -2
- package/dist/patterns/run/index.d.ts +2 -0
- package/dist/patterns/run/index.js +2 -0
- package/dist/patterns/run/progress.d.ts +24 -0
- package/dist/patterns/run/progress.js +32 -0
- package/dist/patterns/run/run-summary.d.ts +21 -0
- package/dist/patterns/run/run-summary.js +82 -0
- package/dist/patterns/run/run-trace.d.ts +1 -0
- package/dist/patterns/run/run-trace.js +14 -3
- package/dist/patterns/undo/index.d.ts +19 -0
- package/dist/patterns/undo/index.js +106 -0
- package/package.json +23 -2
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { AgentChatController } from "./controller.js";
|
|
2
2
|
import { isSafeHref } from "../internal/safe-href.js";
|
|
3
3
|
import { BaseElement } from "../../core/index.js";
|
|
4
|
+
import { announce } from "../../foundations/a11y/index.js";
|
|
4
5
|
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
5
6
|
import { boePanel, boeRadius } from "../../foundations/geometry/index.js";
|
|
7
|
+
import { ScrollPinController, boeEntranceKeyframes, boeReducedMotionPolicy } from "../../foundations/motion/index.js";
|
|
8
|
+
import { boeStatusGlyph, boeStatusStyles } from "../../foundations/status/index.js";
|
|
9
|
+
import { ResultBlocks, resultDocumentIds } from "../../components/collections/result-blocks.js";
|
|
10
|
+
import { RunSummary } from "../run/run-summary.js";
|
|
11
|
+
let chatInstance = 0;
|
|
6
12
|
const DEFAULT_TAG_NAME = "box-agent-chat";
|
|
7
13
|
const escapeHtml = (value) => value
|
|
8
14
|
.replaceAll("&", "&")
|
|
@@ -237,7 +243,7 @@ const elementStyles = `
|
|
|
237
243
|
gap: 0.4rem;
|
|
238
244
|
}
|
|
239
245
|
|
|
240
|
-
[part="proposal-action"] {
|
|
246
|
+
[part="proposal-action"], [part="option"], [part="retry"] {
|
|
241
247
|
appearance: none;
|
|
242
248
|
font: inherit;
|
|
243
249
|
font-size: 0.78rem;
|
|
@@ -251,7 +257,9 @@ const elementStyles = `
|
|
|
251
257
|
transition: background ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
252
258
|
}
|
|
253
259
|
|
|
254
|
-
[part="
|
|
260
|
+
[part="option"], [part="retry"] { justify-self: start; min-height: 32px; text-align: left; }
|
|
261
|
+
|
|
262
|
+
[part="proposal-action"]:hover, [part="option"]:hover, [part="retry"]:hover {
|
|
255
263
|
background: var(--boe-token-surface-surface-hover, #f4f4f4);
|
|
256
264
|
}
|
|
257
265
|
|
|
@@ -265,7 +273,7 @@ const elementStyles = `
|
|
|
265
273
|
color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 74%, black 26%);
|
|
266
274
|
}
|
|
267
275
|
|
|
268
|
-
[part="proposal-action"]:focus-visible {
|
|
276
|
+
[part="proposal-action"]:focus-visible, [part="option"]:focus-visible, [part="retry"]:focus-visible {
|
|
269
277
|
outline: none;
|
|
270
278
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 18%, transparent);
|
|
271
279
|
}
|
|
@@ -365,9 +373,22 @@ const elementStyles = `
|
|
|
365
373
|
export class AgentChat extends BaseElement {
|
|
366
374
|
static tagName = DEFAULT_TAG_NAME;
|
|
367
375
|
static get observedAttributes() {
|
|
368
|
-
return ["agent-name", "heading", "placeholder", "token"];
|
|
376
|
+
return ["agent-name", "heading", "placeholder", "token", "hidden", "hide-citations"];
|
|
369
377
|
}
|
|
370
378
|
controller = null;
|
|
379
|
+
scope = `boe-chat-${++chatInstance}`;
|
|
380
|
+
composing = false;
|
|
381
|
+
scrollPin;
|
|
382
|
+
resizeObserver;
|
|
383
|
+
messageCount = 0;
|
|
384
|
+
jumpEl;
|
|
385
|
+
focusComposer() { this.inputEl?.focus({ preventScroll: true }); }
|
|
386
|
+
proposalAnchor(messageId, proposalId) { return `${this.scope}-${encodeURIComponent(messageId)}-${encodeURIComponent(proposalId)}`; }
|
|
387
|
+
focusProposal(messageId, proposalId) {
|
|
388
|
+
const card = this.shadowRoot?.getElementById(this.proposalAnchor(messageId, proposalId));
|
|
389
|
+
card?.scrollIntoView({ block: "nearest" });
|
|
390
|
+
card?.focus({ preventScroll: true });
|
|
391
|
+
}
|
|
371
392
|
ownsController = false;
|
|
372
393
|
pendingStart = false;
|
|
373
394
|
unsubscribeFns = [];
|
|
@@ -425,6 +446,8 @@ export class AgentChat extends BaseElement {
|
|
|
425
446
|
this.adoptController(value);
|
|
426
447
|
}
|
|
427
448
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
449
|
+
if (name === "hidden" && newValue === null && this.isRendered)
|
|
450
|
+
this.focusComposer();
|
|
428
451
|
if (name === "token") {
|
|
429
452
|
this.scheduleStart();
|
|
430
453
|
}
|
|
@@ -442,10 +465,26 @@ export class AgentChat extends BaseElement {
|
|
|
442
465
|
}
|
|
443
466
|
connectedCallback() {
|
|
444
467
|
super.connectedCallback();
|
|
468
|
+
if (this.controller && !this.ownsController && this.unsubscribeFns.length === 0)
|
|
469
|
+
this.subscribeToController(this.controller);
|
|
470
|
+
this.scrollPin?.connect();
|
|
471
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
472
|
+
this.resizeObserver = new ResizeObserver(() => this.scrollPin?.contentChanged());
|
|
473
|
+
this.resizeObserver.observe(this.threadEl);
|
|
474
|
+
for (const child of Array.from(this.threadEl.children))
|
|
475
|
+
this.resizeObserver.observe(child);
|
|
476
|
+
}
|
|
445
477
|
this.scheduleStart();
|
|
446
478
|
}
|
|
447
479
|
disconnectedCallback() {
|
|
448
|
-
this.
|
|
480
|
+
this.scrollPin?.disconnect();
|
|
481
|
+
this.resizeObserver?.disconnect();
|
|
482
|
+
if (this.ownsController)
|
|
483
|
+
this.teardownController();
|
|
484
|
+
else {
|
|
485
|
+
this.unsubscribeFns.forEach(fn => fn());
|
|
486
|
+
this.unsubscribeFns = [];
|
|
487
|
+
}
|
|
449
488
|
}
|
|
450
489
|
/** Send the composer's contents (or an explicit body) as a user turn. */
|
|
451
490
|
async send(body) {
|
|
@@ -455,11 +494,13 @@ export class AgentChat extends BaseElement {
|
|
|
455
494
|
}
|
|
456
495
|
// Clear only once the controller has accepted the turn, so a refused
|
|
457
496
|
// send (no controller, or a disconnected session) never loses typing.
|
|
458
|
-
const
|
|
459
|
-
|
|
497
|
+
const accepted = this.controller?.getState().connected;
|
|
498
|
+
const sending = this.controller?.send(text);
|
|
499
|
+
if (accepted && body === undefined && this.inputEl) {
|
|
460
500
|
this.inputEl.value = "";
|
|
461
501
|
this.syncComposer();
|
|
462
502
|
}
|
|
503
|
+
await sending;
|
|
463
504
|
}
|
|
464
505
|
/** Stop the in-flight generation; the partial reply is kept. */
|
|
465
506
|
stop() {
|
|
@@ -525,6 +566,11 @@ export class AgentChat extends BaseElement {
|
|
|
525
566
|
["actionResolved", "action-resolved"],
|
|
526
567
|
];
|
|
527
568
|
this.unsubscribeFns = events.map(([eventName, domEventName]) => controller.subscribe(eventName, payload => {
|
|
569
|
+
if (eventName === "actionResolved") {
|
|
570
|
+
const { proposal, decision } = payload;
|
|
571
|
+
const failed = proposal.outcome === "failed";
|
|
572
|
+
announce(`${proposal.title}: ${failed ? "Approved, but did not complete" : decision === "approved" ? "Approved" : "Rejected"}${proposal.note ? `. ${proposal.note}` : ""}`, failed ? "assertive" : "polite", this.ownerDocument);
|
|
573
|
+
}
|
|
528
574
|
this.dispatchEvent(new CustomEvent(domEventName, {
|
|
529
575
|
bubbles: true,
|
|
530
576
|
composed: true,
|
|
@@ -558,6 +604,13 @@ export class AgentChat extends BaseElement {
|
|
|
558
604
|
const canResolve = Boolean(this.controller?.config.transport.resolveAction);
|
|
559
605
|
return message.proposals
|
|
560
606
|
.map(proposal => {
|
|
607
|
+
if (proposal.decision) {
|
|
608
|
+
const failed = proposal.outcome === "failed";
|
|
609
|
+
return `<div part="proposal" aria-live="off" tabindex="-1" id="${this.proposalAnchor(message.id, proposal.id)}" data-proposal-id="${escapeHtml(proposal.id)}" data-decision="${escapeHtml(proposal.decision)}" ${failed ? 'role="alert"' : ""}>
|
|
610
|
+
<span part="decision">${boeStatusGlyph(failed ? "failed" : proposal.decision === "approved" ? "done" : "skipped")}${proposal.decision === "approved" ? failed ? "Approved · didn't complete" : "Approved" : "Rejected"} · <span part="proposal-title">${escapeHtml(proposal.title)}</span></span>
|
|
611
|
+
${proposal.note ? `<p part="proposal-note">${escapeHtml(proposal.note)}</p>` : ""}
|
|
612
|
+
${failed ? `<button part="retry" data-message-id="${escapeHtml(message.id)}">Try again</button>` : ""}</div>`;
|
|
613
|
+
}
|
|
561
614
|
const params = (proposal.params ?? [])
|
|
562
615
|
.map(param => `
|
|
563
616
|
<dt part="param-label">${escapeHtml(param.label)}</dt>
|
|
@@ -567,20 +620,22 @@ export class AgentChat extends BaseElement {
|
|
|
567
620
|
const actions = canResolve && !proposal.decision
|
|
568
621
|
? `
|
|
569
622
|
<div part="proposal-actions">
|
|
570
|
-
<button type="button" part="proposal-action" data-action="approve" data-proposal-id="${escapeHtml(proposal.id)}"
|
|
571
|
-
<button type="button" part="proposal-action" data-action="reject" data-proposal-id="${escapeHtml(proposal.id)}"
|
|
572
|
-
<button type="button" part="proposal-action" data-action="modify" data-proposal-id="${escapeHtml(proposal.id)}">Modify</button>
|
|
623
|
+
<button type="button" part="proposal-action" data-action="approve" data-proposal-id="${escapeHtml(proposal.id)}" ${proposal.resolving ? "disabled" : ""} aria-busy="${proposal.resolving === "approved"}">${proposal.resolving === "approved" ? boeStatusGlyph("active") : ""}Approve</button>
|
|
624
|
+
<button type="button" part="proposal-action" data-action="reject" data-proposal-id="${escapeHtml(proposal.id)}" ${proposal.resolving ? "disabled" : ""} aria-busy="${proposal.resolving === "rejected"}">${proposal.resolving === "rejected" ? boeStatusGlyph("active") : ""}Reject</button>
|
|
625
|
+
<button type="button" part="proposal-action" data-action="modify" data-proposal-id="${escapeHtml(proposal.id)}" ${proposal.resolving ? "disabled" : ""}>Modify</button>
|
|
573
626
|
</div>
|
|
574
627
|
`
|
|
575
628
|
: "";
|
|
576
629
|
return `
|
|
577
|
-
<div part="proposal"
|
|
630
|
+
<div part="proposal" tabindex="-1" id="${this.proposalAnchor(message.id, proposal.id)}" data-proposal-id="${escapeHtml(proposal.id)}">
|
|
631
|
+
<span>${boeStatusGlyph("pending")} Needs your approval</span>
|
|
578
632
|
<span part="proposal-title">${escapeHtml(proposal.title)}</span>
|
|
579
633
|
${proposal.summary ? `<p part="proposal-summary">${escapeHtml(proposal.summary)}</p>` : ""}
|
|
580
634
|
${params ? `<dl part="proposal-params">${params}</dl>` : ""}
|
|
581
635
|
${proposal.decision ? `<span part="decision">${proposal.decision === "approved" ? "Approved" : "Rejected"}</span>` : ""}
|
|
582
636
|
${proposal.note ? `<p part="proposal-note">${escapeHtml(proposal.note)}</p>` : ""}
|
|
583
637
|
${actions}
|
|
638
|
+
<p part="proposal-note">To change it, reply with the new values.</p>
|
|
584
639
|
</div>
|
|
585
640
|
`;
|
|
586
641
|
})
|
|
@@ -589,7 +644,8 @@ export class AgentChat extends BaseElement {
|
|
|
589
644
|
messageInnerHtml(message) {
|
|
590
645
|
const name = message.role === "user" ? "You" : (message.actor?.name ?? this.agentName);
|
|
591
646
|
const initials = message.actor?.initials ?? initialsOf(name);
|
|
592
|
-
const
|
|
647
|
+
const documentIds = resultDocumentIds(message.blocks ?? []);
|
|
648
|
+
const citations = (this.hasAttribute("hide-citations") ? [] : message.citations.filter(c => !documentIds.includes(c.id)))
|
|
593
649
|
.map(citation => this.citationHtml(message.id, citation))
|
|
594
650
|
.join("");
|
|
595
651
|
return `
|
|
@@ -597,10 +653,14 @@ export class AgentChat extends BaseElement {
|
|
|
597
653
|
<span part="avatar" aria-hidden="true">${escapeHtml(initials)}</span>
|
|
598
654
|
<span part="author">${escapeHtml(name)}</span>
|
|
599
655
|
</div>
|
|
600
|
-
|
|
656
|
+
${message.role === "agent" ? '<box-run-summary></box-run-summary>' : ""}
|
|
657
|
+
<p part="body">${escapeHtml(message.body)}${message.status === "streaming" && !message.blocks?.length ? `<span part="caret" aria-hidden="true"></span>` : ""}</p>
|
|
658
|
+
<box-result-blocks></box-result-blocks>
|
|
659
|
+
${message.completeness?.status === "incomplete" ? '<p part="incomplete" role="status">This reply may be incomplete.</p>' : ""}
|
|
601
660
|
${message.status === "error" && message.errorMessage ? `<p part="error" role="alert">${escapeHtml(message.errorMessage)}</p>` : ""}
|
|
602
661
|
${citations ? `<div part="citations">${citations}</div>` : ""}
|
|
603
662
|
${this.proposalHtml(message)}
|
|
663
|
+
${message.proposals.some(p => p.outcome === "failed") ? "" : (message.options ?? []).map((o, i) => `<button part="option" data-option="${i}">${escapeHtml(o.label)}</button>`).join("")}
|
|
604
664
|
`;
|
|
605
665
|
}
|
|
606
666
|
/**
|
|
@@ -620,7 +680,12 @@ export class AgentChat extends BaseElement {
|
|
|
620
680
|
},
|
|
621
681
|
agentName: this.agentName,
|
|
622
682
|
citations: message.citations.map(({ id, href, label }) => ({ id, href, label })),
|
|
623
|
-
|
|
683
|
+
hideCitations: this.hasAttribute("hide-citations"),
|
|
684
|
+
blocks: message.blocks,
|
|
685
|
+
completeness: message.completeness?.status,
|
|
686
|
+
options: message.options,
|
|
687
|
+
proposals: message.proposals.map(({ id, title, summary, params, decision, note, outcome, resolving }) => ({
|
|
688
|
+
outcome, resolving,
|
|
624
689
|
id,
|
|
625
690
|
title,
|
|
626
691
|
summary,
|
|
@@ -656,21 +721,38 @@ export class AgentChat extends BaseElement {
|
|
|
656
721
|
body.insertBefore(document.createTextNode(message.body), body.firstChild);
|
|
657
722
|
}
|
|
658
723
|
renderTemplate() {
|
|
724
|
+
ResultBlocks.register();
|
|
725
|
+
RunSummary.register();
|
|
659
726
|
if (!this.shadowRoot) {
|
|
660
727
|
return;
|
|
661
728
|
}
|
|
662
729
|
// The composer is part of the stable shell: a streaming reply patches
|
|
663
730
|
// only [part="thread"], so typing is never interrupted.
|
|
664
731
|
this.shadowRoot.innerHTML = `
|
|
665
|
-
<style>${elementStyles}
|
|
732
|
+
<style>${elementStyles}
|
|
733
|
+
[part="message"][data-role="agent"]{border:0;background:transparent;padding-inline:0;max-width:75ch;min-width:0}
|
|
734
|
+
[part="message"][data-role="user"]{background:var(--boe-token-surface-surface-secondary,#f4f4f4)}
|
|
735
|
+
[part="proposal"]{border:1px solid var(--boe-token-stroke-stroke,#ddd);background:var(--boe-token-surface-surface,#fff);box-shadow:var(--boe-profile-shadow-overlay,0 3px 14px #0001)}
|
|
736
|
+
[part="proposal"][data-decision]{border:0;background:transparent;box-shadow:none;padding-inline:0}
|
|
737
|
+
[part="decision"]{background:none;border-radius:0;padding:0;gap:.4rem;font:inherit}
|
|
738
|
+
[part="proposal-action"][data-action="approve"],[part="proposal-action"][aria-busy="true"]{background:var(--boe-token-surface-surface-brand,#0061d5);color:var(--boe-token-text-text-on-brand,#fff);border-color:transparent}
|
|
739
|
+
[part="proposal-action"]:disabled:not([aria-busy="true"]){opacity:.5}
|
|
740
|
+
[part="composer"]{position:relative;border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:12px;padding:.4rem}
|
|
741
|
+
[part="input"]{border:0;resize:none;max-height:180px;padding-right:5rem;box-sizing:border-box}
|
|
742
|
+
[part="composer-actions"]{position:absolute;right:.5rem;bottom:.5rem}
|
|
743
|
+
[part="jump"]{justify-self:center;border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:999px;padding:.4rem .8rem;background:var(--boe-token-surface-surface,#fff);color:inherit;cursor:pointer}
|
|
744
|
+
.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
|
|
745
|
+
${boeEntranceKeyframes}${boeStatusStyles}${boeReducedMotionPolicy}</style>
|
|
666
746
|
<section part="panel">
|
|
667
747
|
<div part="header">
|
|
668
748
|
<h2 part="title"></h2>
|
|
669
749
|
<span part="status" role="status" aria-live="polite"></span>
|
|
670
750
|
</div>
|
|
671
|
-
<ol part="thread" role="log" aria-live="polite" aria-relevant="additions"></ol>
|
|
751
|
+
<ol part="thread" role="log" aria-live="polite" aria-relevant="additions" tabindex="0"></ol>
|
|
752
|
+
<button part="jump" hidden>Jump to latest</button>
|
|
672
753
|
<div part="composer">
|
|
673
|
-
<
|
|
754
|
+
<label class="sr-only" id="${this.scope}-label">Message</label>
|
|
755
|
+
<textarea part="input" rows="2" aria-labelledby="${this.scope}-label"></textarea>
|
|
674
756
|
<div part="composer-actions">
|
|
675
757
|
<button type="button" part="stop" hidden>Stop</button>
|
|
676
758
|
<button type="button" part="send">Send</button>
|
|
@@ -685,6 +767,11 @@ export class AgentChat extends BaseElement {
|
|
|
685
767
|
this.statusEl = this.shadowRoot.querySelector('[part="status"]');
|
|
686
768
|
}
|
|
687
769
|
setupListeners() {
|
|
770
|
+
this.jumpEl = this.shadowRoot.querySelector('[part="jump"]');
|
|
771
|
+
this.scrollPin = new ScrollPinController(this.threadEl, show => { this.jumpEl.hidden = !show; });
|
|
772
|
+
this.jumpEl.addEventListener("click", () => this.scrollPin?.jump());
|
|
773
|
+
this.inputEl.addEventListener("compositionstart", () => { this.composing = true; });
|
|
774
|
+
this.inputEl.addEventListener("compositionend", () => { this.composing = false; });
|
|
688
775
|
this.sendEl.addEventListener("click", () => {
|
|
689
776
|
void this.send();
|
|
690
777
|
});
|
|
@@ -693,7 +780,7 @@ export class AgentChat extends BaseElement {
|
|
|
693
780
|
});
|
|
694
781
|
this.inputEl.addEventListener("keydown", event => {
|
|
695
782
|
// Enter sends, Shift+Enter makes a newline — the conversational default.
|
|
696
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
783
|
+
if (event.key === "Enter" && !event.shiftKey && !event.isComposing && !this.composing && event.keyCode !== 229) {
|
|
697
784
|
event.preventDefault();
|
|
698
785
|
void this.send();
|
|
699
786
|
}
|
|
@@ -703,15 +790,31 @@ export class AgentChat extends BaseElement {
|
|
|
703
790
|
});
|
|
704
791
|
this.threadEl.addEventListener("click", event => {
|
|
705
792
|
const target = event.target;
|
|
793
|
+
const messageId = target.closest('[part="message"]')?.getAttribute("data-message-id") ?? "";
|
|
794
|
+
const option = target.closest("[data-option]");
|
|
795
|
+
if (option) {
|
|
796
|
+
const prompt = this.controller?.getMessage(messageId)?.options?.[Number(option.dataset.option)]?.prompt;
|
|
797
|
+
if (prompt)
|
|
798
|
+
void this.send(prompt);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (target.closest('[part="retry"]')) {
|
|
802
|
+
const messages = this.controller?.getState().messages ?? [];
|
|
803
|
+
const index = messages.findIndex(m => m.id === messageId);
|
|
804
|
+
const request = messages.slice(0, index).reverse().find(m => m.role === "user");
|
|
805
|
+
if (request)
|
|
806
|
+
void this.send(request.body);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
706
809
|
const proposalButton = target.closest('[part="proposal-action"]');
|
|
707
810
|
if (proposalButton && this.threadEl.contains(proposalButton)) {
|
|
708
811
|
const proposalId = proposalButton.getAttribute("data-proposal-id") ?? "";
|
|
709
812
|
const action = proposalButton.getAttribute("data-action") ?? "";
|
|
710
813
|
if (action === "approve") {
|
|
711
|
-
void this.controller?.resolveAction(proposalId, "approved");
|
|
814
|
+
void this.controller?.resolveAction(proposalId, "approved", undefined, messageId);
|
|
712
815
|
}
|
|
713
816
|
else if (action === "reject") {
|
|
714
|
-
void this.controller?.resolveAction(proposalId, "rejected");
|
|
817
|
+
void this.controller?.resolveAction(proposalId, "rejected", undefined, messageId);
|
|
715
818
|
}
|
|
716
819
|
else if (action === "modify") {
|
|
717
820
|
// Modifying needs the host's own editor, so this surfaces intent.
|
|
@@ -744,6 +847,9 @@ export class AgentChat extends BaseElement {
|
|
|
744
847
|
const streaming = this.controller?.getState().streaming ?? false;
|
|
745
848
|
this.inputEl.placeholder = this.placeholder;
|
|
746
849
|
this.sendEl.disabled = streaming || this.inputEl.value.trim().length === 0;
|
|
850
|
+
this.sendEl.hidden = streaming;
|
|
851
|
+
this.inputEl.style.height = "auto";
|
|
852
|
+
this.inputEl.style.height = `${Math.min(180, Math.max(56, this.inputEl.scrollHeight))}px`;
|
|
747
853
|
this.stopEl.hidden = !streaming;
|
|
748
854
|
this.statusEl.textContent = streaming ? "Agent is responding…" : "";
|
|
749
855
|
}
|
|
@@ -762,7 +868,8 @@ export class AgentChat extends BaseElement {
|
|
|
762
868
|
: null;
|
|
763
869
|
const state = this.controller?.getState() ?? null;
|
|
764
870
|
const messages = state?.messages ?? [];
|
|
765
|
-
const
|
|
871
|
+
const newMessage = messages.length > this.messageCount;
|
|
872
|
+
this.messageCount = messages.length;
|
|
766
873
|
this.shadowRoot.querySelector('[part="title"]').textContent = this.heading;
|
|
767
874
|
if (messages.length === 0) {
|
|
768
875
|
if (!this.threadEl.querySelector('[part="empty"]')) {
|
|
@@ -793,7 +900,13 @@ export class AgentChat extends BaseElement {
|
|
|
793
900
|
else {
|
|
794
901
|
node.setAttribute("data-signature", signature);
|
|
795
902
|
node.setAttribute("data-status", message.status);
|
|
903
|
+
const summary = node.querySelector("box-run-summary");
|
|
904
|
+
const results = node.querySelector("box-result-blocks");
|
|
796
905
|
node.innerHTML = this.messageInnerHtml(message);
|
|
906
|
+
if (summary)
|
|
907
|
+
node.querySelector("box-run-summary")?.replaceWith(summary);
|
|
908
|
+
if (results)
|
|
909
|
+
node.querySelector("box-result-blocks")?.replaceWith(results);
|
|
797
910
|
}
|
|
798
911
|
}
|
|
799
912
|
else {
|
|
@@ -806,8 +919,19 @@ export class AgentChat extends BaseElement {
|
|
|
806
919
|
this.threadEl.insertBefore(node, anchor);
|
|
807
920
|
}
|
|
808
921
|
previous = node;
|
|
922
|
+
this.resizeObserver?.observe(node);
|
|
923
|
+
const blocks = node.querySelector("box-result-blocks");
|
|
924
|
+
if (blocks && blocks.blocks !== message.blocks)
|
|
925
|
+
blocks.blocks = message.blocks ?? [];
|
|
926
|
+
const summary = node.querySelector("box-run-summary");
|
|
927
|
+
if (summary) {
|
|
928
|
+
summary.toggleAttribute("failed", message.status === "error");
|
|
929
|
+
summary.turn = { steps: message.trace ?? [], todos: message.todos ?? [], startedAt: message.startedAt ?? 0,
|
|
930
|
+
...(message.endedAt !== undefined ? { endedAt: message.endedAt } : {}), incomplete: message.completeness?.status === "incomplete" };
|
|
931
|
+
}
|
|
809
932
|
}
|
|
810
933
|
for (const stale of existing.values()) {
|
|
934
|
+
this.resizeObserver?.unobserve(stale);
|
|
811
935
|
stale.remove();
|
|
812
936
|
}
|
|
813
937
|
if (focusKey?.part) {
|
|
@@ -817,9 +941,7 @@ export class AgentChat extends BaseElement {
|
|
|
817
941
|
target?.focus();
|
|
818
942
|
}
|
|
819
943
|
// Follow the stream only when the reader is already at the bottom.
|
|
820
|
-
|
|
821
|
-
this.threadEl.scrollTop = this.threadEl.scrollHeight;
|
|
822
|
-
}
|
|
944
|
+
this.scrollPin?.contentChanged(newMessage);
|
|
823
945
|
this.syncComposer();
|
|
824
946
|
}
|
|
825
947
|
}
|
|
@@ -11,6 +11,8 @@ export declare class AgentChatController extends Controller<AgentChatState, Agen
|
|
|
11
11
|
readonly config: AgentChatSessionConfig;
|
|
12
12
|
private sendCounter;
|
|
13
13
|
private activeAbortController;
|
|
14
|
+
private finishActive?;
|
|
15
|
+
private readonly resolving;
|
|
14
16
|
constructor(config: AgentChatSessionConfig);
|
|
15
17
|
connect(): void;
|
|
16
18
|
disconnect(): void;
|
|
@@ -23,5 +25,5 @@ export declare class AgentChatController extends Controller<AgentChatState, Agen
|
|
|
23
25
|
*/
|
|
24
26
|
send(body: string): Promise<AgentChatMessage | null>;
|
|
25
27
|
/** Resolve a HITL proposal through the transport capability. */
|
|
26
|
-
resolveAction(proposalId: string, decision: AgentActionDecision, note?: string): Promise<AgentActionProposal | null>;
|
|
28
|
+
resolveAction(proposalId: string, decision: AgentActionDecision, note?: string, messageId?: string): Promise<AgentActionProposal | null>;
|
|
27
29
|
}
|
|
@@ -16,6 +16,8 @@ export class AgentChatController extends Controller {
|
|
|
16
16
|
config;
|
|
17
17
|
sendCounter = 0;
|
|
18
18
|
activeAbortController = null;
|
|
19
|
+
finishActive;
|
|
20
|
+
resolving = new Set();
|
|
19
21
|
constructor(config) {
|
|
20
22
|
super(createInitialState());
|
|
21
23
|
this.config = config;
|
|
@@ -31,7 +33,7 @@ export class AgentChatController extends Controller {
|
|
|
31
33
|
if (!this.state.connected) {
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
|
-
this.
|
|
36
|
+
this.stop();
|
|
35
37
|
this.activeAbortController = null;
|
|
36
38
|
this.setState(createInitialState());
|
|
37
39
|
this.emit("disconnected", undefined);
|
|
@@ -42,6 +44,7 @@ export class AgentChatController extends Controller {
|
|
|
42
44
|
/** Abort the in-flight generation; whatever already streamed is kept. */
|
|
43
45
|
stop() {
|
|
44
46
|
this.activeAbortController?.abort();
|
|
47
|
+
this.finishActive?.();
|
|
45
48
|
}
|
|
46
49
|
/**
|
|
47
50
|
* Send a user turn and stream the agent's reply. One send at a time: a
|
|
@@ -52,7 +55,7 @@ export class AgentChatController extends Controller {
|
|
|
52
55
|
if (!this.state.connected || trimmed.length === 0) {
|
|
53
56
|
return null;
|
|
54
57
|
}
|
|
55
|
-
this.
|
|
58
|
+
this.stop();
|
|
56
59
|
const abortController = typeof AbortController === "function" ? new AbortController() : null;
|
|
57
60
|
this.activeAbortController = abortController;
|
|
58
61
|
this.sendCounter += 1;
|
|
@@ -70,6 +73,9 @@ export class AgentChatController extends Controller {
|
|
|
70
73
|
role: "agent",
|
|
71
74
|
body: "",
|
|
72
75
|
status: "streaming",
|
|
76
|
+
startedAt: Date.now(),
|
|
77
|
+
blocks: [], trace: [], todos: [], options: [],
|
|
78
|
+
completeness: { status: "streaming", missing: [] },
|
|
73
79
|
...(this.config.agentName ? { actor: { name: this.config.agentName } } : {}),
|
|
74
80
|
citations: [],
|
|
75
81
|
proposals: [],
|
|
@@ -89,37 +95,81 @@ export class AgentChatController extends Controller {
|
|
|
89
95
|
});
|
|
90
96
|
this.emit("messagesChanged", { messages: this.state.messages });
|
|
91
97
|
};
|
|
98
|
+
const sequences = new Set();
|
|
99
|
+
let highSequence = 0;
|
|
100
|
+
let done;
|
|
101
|
+
const missing = () => Array.from({ length: highSequence }, (_, i) => i + 1).filter(i => !sequences.has(i));
|
|
102
|
+
const upsert = (items, item) => item.id && items.some(i => i.id === item.id)
|
|
103
|
+
? items.map(i => i.id === item.id ? item : i) : [...items, item];
|
|
92
104
|
const onEvent = (event) => {
|
|
93
105
|
const current = this.getMessage(agentMessage.id);
|
|
94
|
-
if (!current || current.status !== "streaming") {
|
|
106
|
+
if (!current || current.status !== "streaming" || abortController?.signal.aborted || turn !== this.sendCounter || done) {
|
|
95
107
|
return;
|
|
96
108
|
}
|
|
109
|
+
if (event.seq !== undefined) {
|
|
110
|
+
if (!Number.isSafeInteger(event.seq) || event.seq < 1 || event.seq > 100000)
|
|
111
|
+
throw new Error("Invalid stream sequence");
|
|
112
|
+
if (sequences.has(event.seq))
|
|
113
|
+
return;
|
|
114
|
+
sequences.add(event.seq);
|
|
115
|
+
highSequence = Math.max(highSequence, event.seq);
|
|
116
|
+
}
|
|
117
|
+
let patch = {};
|
|
97
118
|
if (event.kind === "delta") {
|
|
98
|
-
|
|
119
|
+
patch = { body: current.body + event.text };
|
|
99
120
|
}
|
|
100
121
|
else if (event.kind === "citation") {
|
|
101
|
-
|
|
122
|
+
patch = { citations: upsert(current.citations, event.citation) };
|
|
102
123
|
}
|
|
103
|
-
else {
|
|
104
|
-
|
|
124
|
+
else if (event.kind === "proposal") {
|
|
125
|
+
patch = { proposals: upsert(current.proposals, event.proposal) };
|
|
126
|
+
}
|
|
127
|
+
else if (event.kind === "block") {
|
|
128
|
+
patch = { blocks: upsert(current.blocks ?? [], event.block) };
|
|
129
|
+
}
|
|
130
|
+
else if (event.kind === "trace") {
|
|
131
|
+
patch = { trace: upsert(current.trace ?? [], event.step) };
|
|
132
|
+
}
|
|
133
|
+
else if (event.kind === "todos") {
|
|
134
|
+
patch = { todos: event.todos };
|
|
135
|
+
}
|
|
136
|
+
else if (event.kind === "options") {
|
|
137
|
+
patch = { options: event.options };
|
|
105
138
|
}
|
|
139
|
+
else if (event.kind === "context") {
|
|
140
|
+
patch = { context: event.context };
|
|
141
|
+
}
|
|
142
|
+
else if (event.kind === "done") {
|
|
143
|
+
done = event.status;
|
|
144
|
+
}
|
|
145
|
+
patchAgentMessage({ ...patch, completeness: { status: "streaming", missing: missing() } });
|
|
106
146
|
};
|
|
147
|
+
let finished = false;
|
|
107
148
|
const finish = (patch) => {
|
|
108
|
-
|
|
149
|
+
if (finished)
|
|
150
|
+
return;
|
|
151
|
+
finished = true;
|
|
152
|
+
const gaps = missing();
|
|
153
|
+
patchAgentMessage({ ...patch, endedAt: Date.now(), completeness: {
|
|
154
|
+
status: patch.status === "error" ? "error" : !done || gaps.length ? "incomplete" : done,
|
|
155
|
+
missing: gaps,
|
|
156
|
+
} });
|
|
109
157
|
// A superseded send must not clear the newer send's streaming flag.
|
|
110
158
|
if (turn === this.sendCounter) {
|
|
111
159
|
this.setState({ ...this.state, streaming: false });
|
|
112
160
|
this.emit("streamingChanged", { streaming: false });
|
|
113
161
|
}
|
|
114
162
|
};
|
|
163
|
+
this.finishActive = () => finish({ status: "complete" });
|
|
115
164
|
try {
|
|
116
165
|
await this.config.transport.sendMessage({
|
|
117
166
|
body: trimmed,
|
|
167
|
+
messageId: agentMessage.id,
|
|
118
168
|
token: this.config.token,
|
|
119
169
|
onEvent,
|
|
120
170
|
...(abortController ? { signal: abortController.signal } : {}),
|
|
121
171
|
});
|
|
122
|
-
finish({ status: "complete" });
|
|
172
|
+
finish({ status: done === "error" ? "error" : "complete" });
|
|
123
173
|
}
|
|
124
174
|
catch (error) {
|
|
125
175
|
if (abortController?.signal.aborted) {
|
|
@@ -129,42 +179,58 @@ export class AgentChatController extends Controller {
|
|
|
129
179
|
else {
|
|
130
180
|
const message = error instanceof Error ? error.message : "The agent reply failed";
|
|
131
181
|
finish({ status: "error", errorMessage: message });
|
|
132
|
-
|
|
133
|
-
|
|
182
|
+
if (turn === this.sendCounter) {
|
|
183
|
+
this.setState({ ...this.state, error: message });
|
|
184
|
+
this.emit("sendFailed", { message });
|
|
185
|
+
}
|
|
134
186
|
}
|
|
135
187
|
}
|
|
136
188
|
finally {
|
|
137
189
|
if (this.activeAbortController === abortController) {
|
|
138
190
|
this.activeAbortController = null;
|
|
191
|
+
this.finishActive = undefined;
|
|
139
192
|
}
|
|
140
193
|
}
|
|
141
194
|
return this.getMessage(agentMessage.id) ?? null;
|
|
142
195
|
}
|
|
143
196
|
/** Resolve a HITL proposal through the transport capability. */
|
|
144
|
-
async resolveAction(proposalId, decision, note) {
|
|
197
|
+
async resolveAction(proposalId, decision, note, messageId) {
|
|
145
198
|
const transport = this.config.transport;
|
|
146
199
|
if (!transport.resolveAction) {
|
|
147
200
|
throw new Error("Agent-chat transport does not support resolveAction");
|
|
148
201
|
}
|
|
149
|
-
const holder = this.state.messages.find(message =>
|
|
202
|
+
const holder = this.state.messages.find(message => (!messageId || message.id === messageId) &&
|
|
203
|
+
message.proposals.some(proposal => proposal.id === proposalId));
|
|
150
204
|
const pending = holder?.proposals.find(proposal => proposal.id === proposalId);
|
|
151
|
-
|
|
205
|
+
const key = `${holder?.id}:${proposalId}`;
|
|
206
|
+
if (!this.state.connected || !holder || !pending || pending.decision || this.resolving.has(key)) {
|
|
152
207
|
return null;
|
|
153
208
|
}
|
|
209
|
+
const patch = (value) => {
|
|
210
|
+
this.setState({ ...this.state, messages: this.state.messages.map(m => m.id === holder.id ? {
|
|
211
|
+
...m, proposals: m.proposals.map(p => p.id === proposalId ? { ...p, ...value } : p),
|
|
212
|
+
} : m) });
|
|
213
|
+
this.emit("messagesChanged", { messages: this.state.messages });
|
|
214
|
+
};
|
|
215
|
+
this.resolving.add(key);
|
|
216
|
+
patch({ resolving: decision });
|
|
154
217
|
try {
|
|
155
218
|
// Called on the transport so class-based implementations keep `this`.
|
|
156
|
-
const
|
|
219
|
+
const response = await transport.resolveAction({
|
|
157
220
|
proposalId,
|
|
221
|
+
messageId: holder.id,
|
|
158
222
|
decision,
|
|
159
223
|
...(note !== undefined ? { note } : {}),
|
|
160
224
|
token: this.config.token,
|
|
161
225
|
});
|
|
226
|
+
const resolved = { ...pending, ...response, decision };
|
|
227
|
+
delete resolved.resolving;
|
|
162
228
|
this.setState({
|
|
163
229
|
...this.state,
|
|
164
230
|
messages: this.state.messages.map(message => message.id === holder.id
|
|
165
231
|
? {
|
|
166
232
|
...message,
|
|
167
|
-
proposals: message.proposals.map(proposal => proposal.id === proposalId ? { ...
|
|
233
|
+
proposals: message.proposals.map(proposal => proposal.id === proposalId ? { ...pending, ...resolved, decision } : proposal),
|
|
168
234
|
}
|
|
169
235
|
: message),
|
|
170
236
|
});
|
|
@@ -178,5 +244,15 @@ export class AgentChatController extends Controller {
|
|
|
178
244
|
this.emit("sendFailed", { message });
|
|
179
245
|
return null;
|
|
180
246
|
}
|
|
247
|
+
finally {
|
|
248
|
+
this.resolving.delete(key);
|
|
249
|
+
const current = this.getMessage(holder.id)?.proposals.find(p => p.id === proposalId);
|
|
250
|
+
if (current?.resolving) {
|
|
251
|
+
const clean = { ...current };
|
|
252
|
+
delete clean.resolving;
|
|
253
|
+
this.setState({ ...this.state, messages: this.state.messages.map(m => m.id === holder.id ? { ...m, proposals: m.proposals.map(p => p.id === proposalId ? clean : p) } : m) });
|
|
254
|
+
this.emit("messagesChanged", { messages: this.state.messages });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
181
257
|
}
|
|
182
258
|
}
|