@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.
Files changed (126) hide show
  1. package/dist/components/actions/menu-item.d.ts +2 -0
  2. package/dist/components/actions/menu-item.js +4 -0
  3. package/dist/components/collections/card.js +6 -0
  4. package/dist/components/collections/check-list.d.ts +10 -0
  5. package/dist/components/collections/check-list.js +8 -0
  6. package/dist/components/collections/document-list.d.ts +10 -0
  7. package/dist/components/collections/document-list.js +8 -0
  8. package/dist/components/collections/fact-list.d.ts +10 -0
  9. package/dist/components/collections/fact-list.js +8 -0
  10. package/dist/components/collections/result-blocks.d.ts +54 -0
  11. package/dist/components/collections/result-blocks.js +74 -0
  12. package/dist/components/feedback/alert.d.ts +2 -0
  13. package/dist/components/feedback/alert.js +17 -13
  14. package/dist/components/feedback/progress-bar.d.ts +9 -0
  15. package/dist/components/feedback/progress-bar.js +29 -3
  16. package/dist/components/feedback/status-icon.d.ts +10 -0
  17. package/dist/components/feedback/status-icon.js +26 -0
  18. package/dist/components/feedback/toast.d.ts +10 -1
  19. package/dist/components/feedback/toast.js +69 -27
  20. package/dist/components/forms/number-input.d.ts +1 -0
  21. package/dist/components/forms/number-input.js +9 -6
  22. package/dist/components/forms/search-field.js +16 -0
  23. package/dist/components/overlays/context-menu.d.ts +2 -0
  24. package/dist/components/overlays/context-menu.js +7 -0
  25. package/dist/components/overlays/dialog.js +19 -3
  26. package/dist/components/overlays/popover.js +6 -1
  27. package/dist/element-maps.d.ts +49 -19
  28. package/dist/entries/agent-workspace.d.ts +1 -0
  29. package/dist/entries/agent-workspace.js +1 -0
  30. package/dist/entries/call-console.d.ts +1 -0
  31. package/dist/entries/call-console.js +1 -0
  32. package/dist/entries/check-list.d.ts +1 -0
  33. package/dist/entries/check-list.js +1 -0
  34. package/dist/entries/document-list.d.ts +1 -0
  35. package/dist/entries/document-list.js +1 -0
  36. package/dist/entries/fact-list.d.ts +1 -0
  37. package/dist/entries/fact-list.js +1 -0
  38. package/dist/entries/result-blocks.d.ts +1 -0
  39. package/dist/entries/result-blocks.js +1 -0
  40. package/dist/entries/run-summary.d.ts +1 -0
  41. package/dist/entries/run-summary.js +1 -0
  42. package/dist/entries/status-icon.d.ts +1 -0
  43. package/dist/entries/status-icon.js +1 -0
  44. package/dist/foundations/a11y/announcer.d.ts +12 -0
  45. package/dist/foundations/a11y/announcer.js +75 -0
  46. package/dist/foundations/a11y/focus.js +46 -25
  47. package/dist/foundations/a11y/index.d.ts +2 -0
  48. package/dist/foundations/a11y/index.js +1 -0
  49. package/dist/foundations/icons/box-iconography.d.ts +472 -472
  50. package/dist/foundations/icons/box-iconography.generated.d.ts +1151 -679
  51. package/dist/foundations/icons/box-iconography.generated.js +1006 -3588
  52. package/dist/foundations/icons/glyphs/index.d.ts +1 -0
  53. package/dist/foundations/icons/glyphs/index.js +2 -0
  54. package/dist/foundations/icons/glyphs/package.json +1 -0
  55. package/dist/foundations/motion/behaviors.d.ts +8 -0
  56. package/dist/foundations/motion/behaviors.js +24 -0
  57. package/dist/foundations/motion/index.d.ts +2 -0
  58. package/dist/foundations/motion/index.js +2 -0
  59. package/dist/foundations/motion/scroll-pin.d.ts +19 -0
  60. package/dist/foundations/motion/scroll-pin.js +60 -0
  61. package/dist/foundations/motion/tokens.d.ts +2 -0
  62. package/dist/foundations/motion/tokens.js +2 -0
  63. package/dist/foundations/overlay/positioning.js +7 -0
  64. package/dist/foundations/profiles/registry.d.ts +3 -0
  65. package/dist/foundations/profiles/registry.js +3 -0
  66. package/dist/foundations/profiles/types.d.ts +3 -0
  67. package/dist/foundations/status/index.d.ts +6 -0
  68. package/dist/foundations/status/index.js +35 -0
  69. package/dist/foundations/tokens/box-dark.js +4 -1
  70. package/dist/foundations/tokens/box-defaults.js +3 -0
  71. package/dist/foundations/tokens/registry.d.ts +3 -0
  72. package/dist/foundations/tokens/registry.js +3 -0
  73. package/dist/foundations/tokens/types.d.ts +3 -0
  74. package/dist/index.d.ts +12 -0
  75. package/dist/index.js +12 -0
  76. package/dist/patterns/agent-chat/agent-chat.d.ts +9 -0
  77. package/dist/patterns/agent-chat/agent-chat.js +146 -24
  78. package/dist/patterns/agent-chat/controller.d.ts +3 -1
  79. package/dist/patterns/agent-chat/controller.js +92 -16
  80. package/dist/patterns/agent-chat/index.d.ts +1 -0
  81. package/dist/patterns/agent-chat/index.js +1 -0
  82. package/dist/patterns/agent-chat/ndjson.d.ts +5 -0
  83. package/dist/patterns/agent-chat/ndjson.js +41 -0
  84. package/dist/patterns/agent-chat/types.d.ts +44 -1
  85. package/dist/patterns/agent-workspace/agent-workspace.d.ts +28 -0
  86. package/dist/patterns/agent-workspace/agent-workspace.js +215 -0
  87. package/dist/patterns/agent-workspace/controller.d.ts +38 -0
  88. package/dist/patterns/agent-workspace/controller.js +65 -0
  89. package/dist/patterns/agent-workspace/index.d.ts +2 -0
  90. package/dist/patterns/agent-workspace/index.js +2 -0
  91. package/dist/patterns/call-console/call-console.d.ts +20 -0
  92. package/dist/patterns/call-console/call-console.js +201 -0
  93. package/dist/patterns/call-console/controller.d.ts +23 -0
  94. package/dist/patterns/call-console/controller.js +50 -0
  95. package/dist/patterns/call-console/http-transport.d.ts +3 -0
  96. package/dist/patterns/call-console/http-transport.js +26 -0
  97. package/dist/patterns/call-console/index.d.ts +4 -0
  98. package/dist/patterns/call-console/index.js +4 -0
  99. package/dist/patterns/call-console/types.d.ts +32 -0
  100. package/dist/patterns/call-console/types.js +8 -0
  101. package/dist/patterns/content-explorer/adapters/action-menu.js +7 -5
  102. package/dist/patterns/content-explorer/adapters/toolbar.js +6 -2
  103. package/dist/patterns/content-uploader/content-uploader.js +2 -1
  104. package/dist/patterns/file-request/file-request-builder.js +3 -0
  105. package/dist/patterns/flow-builder/flow-builder.d.ts +48 -0
  106. package/dist/patterns/flow-builder/flow-builder.js +200 -0
  107. package/dist/patterns/flow-builder/index.d.ts +3 -0
  108. package/dist/patterns/flow-builder/index.js +3 -0
  109. package/dist/patterns/flow-builder/model.d.ts +33 -0
  110. package/dist/patterns/flow-builder/model.js +28 -0
  111. package/dist/patterns/flow-builder/primitives.d.ts +47 -0
  112. package/dist/patterns/flow-builder/primitives.js +192 -0
  113. package/dist/patterns/lineage/lineage-graph.js +26 -6
  114. package/dist/patterns/notifications/notification-inbox.js +19 -3
  115. package/dist/patterns/preview/annotation-inspector.js +2 -2
  116. package/dist/patterns/run/index.d.ts +2 -0
  117. package/dist/patterns/run/index.js +2 -0
  118. package/dist/patterns/run/progress.d.ts +24 -0
  119. package/dist/patterns/run/progress.js +32 -0
  120. package/dist/patterns/run/run-summary.d.ts +21 -0
  121. package/dist/patterns/run/run-summary.js +82 -0
  122. package/dist/patterns/run/run-trace.d.ts +1 -0
  123. package/dist/patterns/run/run-trace.js +14 -3
  124. package/dist/patterns/undo/index.d.ts +19 -0
  125. package/dist/patterns/undo/index.js +106 -0
  126. 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="proposal-action"]:hover {
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.teardownController();
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 sent = await this.controller?.send(text);
459
- if (sent && body === undefined && this.inputEl) {
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)}">Approve</button>
571
- <button type="button" part="proposal-action" data-action="reject" data-proposal-id="${escapeHtml(proposal.id)}">Reject</button>
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" data-proposal-id="${escapeHtml(proposal.id)}"${proposal.decision ? ` data-decision="${escapeHtml(proposal.decision)}"` : ""}>
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 citations = message.citations
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
- <p part="body">${escapeHtml(message.body)}${message.status === "streaming" ? `<span part="caret" aria-hidden="true"></span>` : ""}</p>
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
- proposals: message.proposals.map(({ id, title, summary, params, decision, note }) => ({
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}</style>
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
- <textarea part="input" rows="2"></textarea>
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 atBottom = this.threadEl.scrollHeight - this.threadEl.scrollTop - this.threadEl.clientHeight < 40;
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
- if (atBottom) {
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.activeAbortController?.abort();
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.activeAbortController?.abort();
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
- patchAgentMessage({ body: current.body + event.text });
119
+ patch = { body: current.body + event.text };
99
120
  }
100
121
  else if (event.kind === "citation") {
101
- patchAgentMessage({ citations: [...current.citations, event.citation] });
122
+ patch = { citations: upsert(current.citations, event.citation) };
102
123
  }
103
- else {
104
- patchAgentMessage({ proposals: [...current.proposals, event.proposal] });
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
- patchAgentMessage(patch);
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
- this.setState({ ...this.state, error: message });
133
- this.emit("sendFailed", { message });
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 => message.proposals.some(proposal => proposal.id === proposalId));
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
- if (!this.state.connected || !holder || !pending || pending.decision) {
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 resolved = await transport.resolveAction({
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 ? { ...proposal, ...resolved } : proposal),
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
  }
@@ -1,3 +1,4 @@
1
1
  export * from "./types.js";
2
2
  export * from "./controller.js";
3
3
  export * from "./agent-chat.js";
4
+ export * from "./ndjson.js";
@@ -4,3 +4,4 @@ export * from "./types.js";
4
4
  export * from "./controller.js";
5
5
  // Composed conversation surface
6
6
  export * from "./agent-chat.js";
7
+ export * from "./ndjson.js";
@@ -0,0 +1,5 @@
1
+ /** Incremental UTF-8 decoding with a bounded line buffer and early-reader cleanup. */
2
+ export declare function readNdjson<T = unknown>(stream: ReadableStream<Uint8Array>, options?: {
3
+ maxLineLength?: number;
4
+ signal?: AbortSignal;
5
+ }): AsyncGenerator<T>;