@unofficialbox/box-open-elements 0.19.0 → 0.20.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 (79) hide show
  1. package/dist/components/collections/check-list.d.ts +10 -0
  2. package/dist/components/collections/check-list.js +8 -0
  3. package/dist/components/collections/document-list.d.ts +10 -0
  4. package/dist/components/collections/document-list.js +8 -0
  5. package/dist/components/collections/fact-list.d.ts +10 -0
  6. package/dist/components/collections/fact-list.js +8 -0
  7. package/dist/components/collections/result-blocks.d.ts +54 -0
  8. package/dist/components/collections/result-blocks.js +71 -0
  9. package/dist/components/feedback/progress-bar.d.ts +9 -0
  10. package/dist/components/feedback/progress-bar.js +29 -3
  11. package/dist/components/feedback/status-icon.d.ts +10 -0
  12. package/dist/components/feedback/status-icon.js +26 -0
  13. package/dist/element-maps.d.ts +39 -19
  14. package/dist/entries/agent-workspace.d.ts +1 -0
  15. package/dist/entries/agent-workspace.js +1 -0
  16. package/dist/entries/call-console.d.ts +1 -0
  17. package/dist/entries/call-console.js +1 -0
  18. package/dist/entries/check-list.d.ts +1 -0
  19. package/dist/entries/check-list.js +1 -0
  20. package/dist/entries/document-list.d.ts +1 -0
  21. package/dist/entries/document-list.js +1 -0
  22. package/dist/entries/fact-list.d.ts +1 -0
  23. package/dist/entries/fact-list.js +1 -0
  24. package/dist/entries/result-blocks.d.ts +1 -0
  25. package/dist/entries/result-blocks.js +1 -0
  26. package/dist/entries/run-summary.d.ts +1 -0
  27. package/dist/entries/run-summary.js +1 -0
  28. package/dist/entries/status-icon.d.ts +1 -0
  29. package/dist/entries/status-icon.js +1 -0
  30. package/dist/foundations/motion/behaviors.d.ts +8 -0
  31. package/dist/foundations/motion/behaviors.js +24 -0
  32. package/dist/foundations/motion/index.d.ts +2 -0
  33. package/dist/foundations/motion/index.js +2 -0
  34. package/dist/foundations/motion/scroll-pin.d.ts +19 -0
  35. package/dist/foundations/motion/scroll-pin.js +60 -0
  36. package/dist/foundations/motion/tokens.d.ts +2 -0
  37. package/dist/foundations/motion/tokens.js +2 -0
  38. package/dist/foundations/profiles/registry.d.ts +3 -0
  39. package/dist/foundations/profiles/registry.js +3 -0
  40. package/dist/foundations/profiles/types.d.ts +3 -0
  41. package/dist/foundations/status/index.d.ts +6 -0
  42. package/dist/foundations/status/index.js +33 -0
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +10 -0
  45. package/dist/patterns/agent-chat/agent-chat.d.ts +9 -0
  46. package/dist/patterns/agent-chat/agent-chat.js +135 -21
  47. package/dist/patterns/agent-chat/controller.d.ts +3 -1
  48. package/dist/patterns/agent-chat/controller.js +92 -16
  49. package/dist/patterns/agent-chat/index.d.ts +1 -0
  50. package/dist/patterns/agent-chat/index.js +1 -0
  51. package/dist/patterns/agent-chat/ndjson.d.ts +5 -0
  52. package/dist/patterns/agent-chat/ndjson.js +41 -0
  53. package/dist/patterns/agent-chat/types.d.ts +44 -1
  54. package/dist/patterns/agent-workspace/agent-workspace.d.ts +28 -0
  55. package/dist/patterns/agent-workspace/agent-workspace.js +215 -0
  56. package/dist/patterns/agent-workspace/controller.d.ts +38 -0
  57. package/dist/patterns/agent-workspace/controller.js +65 -0
  58. package/dist/patterns/agent-workspace/index.d.ts +2 -0
  59. package/dist/patterns/agent-workspace/index.js +2 -0
  60. package/dist/patterns/call-console/call-console.d.ts +17 -0
  61. package/dist/patterns/call-console/call-console.js +107 -0
  62. package/dist/patterns/call-console/controller.d.ts +23 -0
  63. package/dist/patterns/call-console/controller.js +50 -0
  64. package/dist/patterns/call-console/http-transport.d.ts +3 -0
  65. package/dist/patterns/call-console/http-transport.js +26 -0
  66. package/dist/patterns/call-console/index.d.ts +4 -0
  67. package/dist/patterns/call-console/index.js +4 -0
  68. package/dist/patterns/call-console/types.d.ts +32 -0
  69. package/dist/patterns/call-console/types.js +8 -0
  70. package/dist/patterns/content-uploader/content-uploader.js +2 -1
  71. package/dist/patterns/run/index.d.ts +2 -0
  72. package/dist/patterns/run/index.js +2 -0
  73. package/dist/patterns/run/progress.d.ts +24 -0
  74. package/dist/patterns/run/progress.js +32 -0
  75. package/dist/patterns/run/run-summary.d.ts +21 -0
  76. package/dist/patterns/run/run-summary.js +82 -0
  77. package/dist/patterns/run/run-trace.d.ts +1 -0
  78. package/dist/patterns/run/run-trace.js +14 -3
  79. package/package.json +13 -1
@@ -0,0 +1,24 @@
1
+ import { boeMotionDuration, boeMotionEasing } from "./tokens.js";
2
+ export const boeMotionStagger = "var(--boe-profile-motion-stagger, 50ms)";
3
+ export const boeEntranceKeyframes = `
4
+ @keyframes boe-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
5
+ @keyframes boe-pop { from { opacity: 0; transform: scale(.8); } to { opacity: 1; transform: none; } }
6
+ @keyframes boe-fade-in { from { opacity: 0; } to { opacity: 1; } }
7
+ @keyframes boe-status-check { from { stroke-dashoffset: 20; } to { stroke-dashoffset: 0; } }
8
+ @keyframes boe-status-spin { to { transform: rotate(360deg); } }
9
+ `;
10
+ export const boeEntrance = (name = "rise") => `animation: boe-${name} ${boeMotionDuration.arrival} ${boeMotionEasing.enter} both;`;
11
+ export const boeStaggerStyles = `animation-delay: calc(var(--i, 0) * ${boeMotionStagger});`;
12
+ /** Put padding on a grandchild, not the collapsible child. Pair with inert. */
13
+ export const boeDisclosureStyles = (selector = ".boe-disclosure") => `
14
+ ${selector} { display: grid; grid-template-rows: 0fr; transition: grid-template-rows ${boeMotionDuration.panel} ${boeMotionEasing.enter}; }
15
+ ${selector}[data-open] { grid-template-rows: 1fr; }
16
+ ${selector} > * { min-height: 0; overflow: hidden; padding-block: 0; }
17
+ `;
18
+ /** Include in document and shadow styles: media rules do not cross shadow roots. */
19
+ export const boeReducedMotionPolicy = `
20
+ @media (prefers-reduced-motion: reduce) {
21
+ *, *::before, *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important;
22
+ animation-delay: 0ms !important; transition-duration: 1ms !important; transition-delay: 0ms !important; scroll-behavior: auto !important; }
23
+ }
24
+ `;
@@ -1,2 +1,4 @@
1
1
  export { boeMotionDuration, boeMotionEasing, boeReducedMotionStyles, boeTransition, } from "./tokens.js";
2
2
  export type { BoeMotionDuration, BoeMotionEasing } from "./tokens.js";
3
+ export * from "./behaviors.js";
4
+ export * from "./scroll-pin.js";
@@ -1 +1,3 @@
1
1
  export { boeMotionDuration, boeMotionEasing, boeReducedMotionStyles, boeTransition, } from "./tokens.js";
2
+ export * from "./behaviors.js";
3
+ export * from "./scroll-pin.js";
@@ -0,0 +1,19 @@
1
+ /** Scroll following is changed by user intent, never by resize-induced scroll events. */
2
+ export declare class ScrollPinController {
3
+ private readonly viewport;
4
+ private readonly onChange;
5
+ pinned: boolean;
6
+ private cleanup;
7
+ private observer?;
8
+ private pointer;
9
+ private lastTop;
10
+ private touchY;
11
+ constructor(viewport: HTMLElement, onChange?: (showJump: boolean) => void);
12
+ connect(content?: Element): void;
13
+ private atBottom;
14
+ private notify;
15
+ unpin(): void;
16
+ contentChanged(newMessage?: boolean): void;
17
+ jump(): void;
18
+ disconnect(): void;
19
+ }
@@ -0,0 +1,60 @@
1
+ /** Scroll following is changed by user intent, never by resize-induced scroll events. */
2
+ export class ScrollPinController {
3
+ viewport;
4
+ onChange;
5
+ pinned = true;
6
+ cleanup = [];
7
+ observer;
8
+ pointer = false;
9
+ lastTop = 0;
10
+ touchY = 0;
11
+ constructor(viewport, onChange = () => { }) {
12
+ this.viewport = viewport;
13
+ this.onChange = onChange;
14
+ }
15
+ connect(content = this.viewport) {
16
+ this.disconnect();
17
+ const on = (kind, fn) => {
18
+ this.viewport.addEventListener(kind, fn);
19
+ this.cleanup.push(() => this.viewport.removeEventListener(kind, fn));
20
+ };
21
+ on("wheel", e => { if (e.deltaY < 0)
22
+ this.unpin(); });
23
+ on("keydown", e => { if (["ArrowUp", "PageUp", "Home"].includes(e.key))
24
+ this.unpin(); });
25
+ on("touchstart", e => { this.touchY = e.touches[0]?.clientY ?? 0; });
26
+ on("touchmove", e => { const y = e.touches[0]?.clientY ?? this.touchY; if (y > this.touchY)
27
+ this.unpin(); this.touchY = y; });
28
+ on("pointerdown", () => { this.pointer = true; this.lastTop = this.viewport.scrollTop; });
29
+ const release = () => { this.pointer = false; };
30
+ this.viewport.ownerDocument.addEventListener("pointerup", release);
31
+ this.cleanup.push(() => this.viewport.ownerDocument.removeEventListener("pointerup", release));
32
+ on("scroll", () => {
33
+ if (this.pointer && this.viewport.scrollTop < this.lastTop)
34
+ this.unpin();
35
+ this.lastTop = this.viewport.scrollTop;
36
+ if (this.atBottom())
37
+ this.pinned = true;
38
+ this.notify();
39
+ });
40
+ if (typeof ResizeObserver !== "undefined") {
41
+ this.observer = new ResizeObserver(() => this.contentChanged());
42
+ this.observer.observe(this.viewport);
43
+ if (content !== this.viewport)
44
+ this.observer.observe(content);
45
+ }
46
+ }
47
+ atBottom() { return this.viewport.scrollHeight - this.viewport.clientHeight - this.viewport.scrollTop < 40; }
48
+ notify() { this.onChange(!this.pinned && !this.atBottom()); }
49
+ unpin() { this.pinned = false; this.notify(); }
50
+ contentChanged(newMessage = false) { if (newMessage)
51
+ this.pinned = true; if (this.pinned)
52
+ this.viewport.scrollTop = this.viewport.scrollHeight; this.notify(); }
53
+ jump() {
54
+ this.pinned = true;
55
+ const reduced = this.viewport.ownerDocument.defaultView?.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
56
+ this.viewport.scrollTo?.({ top: this.viewport.scrollHeight, behavior: reduced ? "instant" : "smooth" });
57
+ this.onChange(false);
58
+ }
59
+ disconnect() { this.cleanup.forEach(fn => fn()); this.cleanup = []; this.observer?.disconnect(); }
60
+ }
@@ -6,6 +6,8 @@
6
6
  * prefers-reduced-motion policy is one place to update.
7
7
  */
8
8
  export declare const boeMotionDuration: {
9
+ readonly arrival: "var(--boe-profile-motion-arrival, 200ms)";
10
+ readonly panel: "var(--boe-profile-motion-panel, 320ms)";
9
11
  /** Quick accent transitions (charts, micro-feedback) */
10
12
  readonly fast: "var(--boe-profile-motion-fast, 120ms)";
11
13
  /**
@@ -6,6 +6,8 @@
6
6
  * prefers-reduced-motion policy is one place to update.
7
7
  */
8
8
  export const boeMotionDuration = {
9
+ arrival: "var(--boe-profile-motion-arrival, 200ms)",
10
+ panel: "var(--boe-profile-motion-panel, 320ms)",
9
11
  /** Quick accent transitions (charts, micro-feedback) */
10
12
  fast: "var(--boe-profile-motion-fast, 120ms)",
11
13
  /**
@@ -52,6 +52,9 @@ export declare const DESIGN_PROFILE_VARIABLES: {
52
52
  readonly primaryFocus: "--boe-profile-shadow-primary-focus";
53
53
  };
54
54
  readonly motion: {
55
+ readonly arrival: "--boe-profile-motion-arrival";
56
+ readonly panel: "--boe-profile-motion-panel";
57
+ readonly stagger: "--boe-profile-motion-stagger";
55
58
  readonly fast: "--boe-profile-motion-fast";
56
59
  readonly interactive: "--boe-profile-motion-interactive";
57
60
  readonly medium: "--boe-profile-motion-medium";
@@ -51,6 +51,9 @@ export const DESIGN_PROFILE_VARIABLES = {
51
51
  primaryFocus: "--boe-profile-shadow-primary-focus",
52
52
  },
53
53
  motion: {
54
+ arrival: "--boe-profile-motion-arrival",
55
+ panel: "--boe-profile-motion-panel",
56
+ stagger: "--boe-profile-motion-stagger",
54
57
  fast: "--boe-profile-motion-fast",
55
58
  interactive: "--boe-profile-motion-interactive",
56
59
  medium: "--boe-profile-motion-medium",
@@ -50,6 +50,9 @@ export interface DesignProfileElevation {
50
50
  primaryFocus?: string;
51
51
  }
52
52
  export interface DesignProfileMotion {
53
+ arrival?: string;
54
+ panel?: string;
55
+ stagger?: string;
53
56
  fast?: string;
54
57
  interactive?: string;
55
58
  medium?: string;
@@ -0,0 +1,6 @@
1
+ export type StatusKind = "pending" | "active" | "done" | "warning" | "failed" | "skipped";
2
+ export declare const toStatusKind: (status: string) => StatusKind;
3
+ export declare const statusLabel: (kind: StatusKind) => string;
4
+ /** Decorative markup. Pair with visible words or the StatusIcon component. */
5
+ export declare const boeStatusGlyph: (kind: StatusKind) => string;
6
+ export declare const boeStatusStyles = "\n.boe-status { flex: none; vertical-align: middle; fill: none; stroke: currentColor; stroke-width: 1.5; }\n.boe-status[data-kind=\"active\"] { color: var(--boe-token-surface-surface-brand, #0061d5); animation: boe-status-spin .8s linear infinite; }\n.boe-status[data-kind=\"active\"] circle { opacity: .25; }\n.boe-status[data-kind=\"done\"] { color: var(--boe-token-text-status-text-success, #187657); }\n.boe-status[data-kind=\"warning\"] { color: var(--boe-token-text-status-text-warning, #946400); }\n.boe-status[data-kind=\"failed\"] { color: var(--boe-token-text-status-text-error, #c52a46); }\n.boe-status[data-kind=\"skipped\"] circle { stroke-dasharray: 2 2; }\n.boe-status:is([data-kind=\"done\"], [data-kind=\"warning\"], [data-kind=\"failed\"]) circle { fill: currentColor; }\n.boe-status:is([data-kind=\"done\"], [data-kind=\"warning\"], [data-kind=\"failed\"]) path { stroke: var(--boe-token-surface-surface, #fff); stroke-linecap: round; }\n.boe-status[data-kind=\"done\"] { animation: boe-pop 200ms ease-out both; }\n.boe-status[data-kind=\"done\"] path { stroke-dasharray: 20; animation: boe-status-check 200ms ease-out both; }\n";
@@ -0,0 +1,33 @@
1
+ const mappings = {
2
+ pending: "pending", info: "pending", active: "active", running: "active", in_progress: "active",
3
+ done: "done", succeeded: "done", completed: "done", pass: "done", approved: "done",
4
+ warning: "warning", warn: "warning", failed: "failed", fail: "failed", error: "failed",
5
+ skipped: "skipped", rejected: "skipped",
6
+ };
7
+ export const toStatusKind = (status) => mappings[status] ?? "pending";
8
+ export const statusLabel = (kind) => ({
9
+ pending: "Pending", active: "In progress", done: "Done", warning: "Done with a warning",
10
+ failed: "Failed", skipped: "Skipped",
11
+ })[kind];
12
+ /** Decorative markup. Pair with visible words or the StatusIcon component. */
13
+ export const boeStatusGlyph = (kind) => {
14
+ const state = toStatusKind(kind);
15
+ const path = state === "done" ? '<path d="m4 8 3 3 5-6"/>'
16
+ : state === "failed" ? '<path d="m5 5 6 6m0-6-6 6"/>'
17
+ : state === "warning" ? '<path d="M5 8h6"/>'
18
+ : state === "active" ? '<path d="M8 1a7 7 0 0 1 7 7"/>' : "";
19
+ return `<svg class="boe-status" data-kind="${state}" aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><circle cx="8" cy="8" r="7"/>${path}</svg>`;
20
+ };
21
+ export const boeStatusStyles = `
22
+ .boe-status { flex: none; vertical-align: middle; fill: none; stroke: currentColor; stroke-width: 1.5; }
23
+ .boe-status[data-kind="active"] { color: var(--boe-token-surface-surface-brand, #0061d5); animation: boe-status-spin .8s linear infinite; }
24
+ .boe-status[data-kind="active"] circle { opacity: .25; }
25
+ .boe-status[data-kind="done"] { color: var(--boe-token-text-status-text-success, #187657); }
26
+ .boe-status[data-kind="warning"] { color: var(--boe-token-text-status-text-warning, #946400); }
27
+ .boe-status[data-kind="failed"] { color: var(--boe-token-text-status-text-error, #c52a46); }
28
+ .boe-status[data-kind="skipped"] circle { stroke-dasharray: 2 2; }
29
+ .boe-status:is([data-kind="done"], [data-kind="warning"], [data-kind="failed"]) circle { fill: currentColor; }
30
+ .boe-status:is([data-kind="done"], [data-kind="warning"], [data-kind="failed"]) path { stroke: var(--boe-token-surface-surface, #fff); stroke-linecap: round; }
31
+ .boe-status[data-kind="done"] { animation: boe-pop 200ms ease-out both; }
32
+ .boe-status[data-kind="done"] path { stroke-dasharray: 20; animation: boe-status-check 200ms ease-out both; }
33
+ `;
package/dist/index.d.ts CHANGED
@@ -134,3 +134,13 @@ export * from "./patterns/task/index.js";
134
134
  export * from "./patterns/governance/index.js";
135
135
  export * from "./patterns/insights/index.js";
136
136
  export * from "./element-maps.js";
137
+ export * from "./components/feedback/status-icon.js";
138
+ export * from "./components/collections/fact-list.js";
139
+ export * from "./components/collections/check-list.js";
140
+ export * from "./components/collections/document-list.js";
141
+ export * from "./components/collections/result-blocks.js";
142
+ export * from "./patterns/run/run-summary.js";
143
+ export * from "./patterns/agent-workspace/index.js";
144
+ export * from "./patterns/call-console/index.js";
145
+ export * from "./foundations/status/index.js";
146
+ export * from "./patterns/run/progress.js";
package/dist/index.js CHANGED
@@ -148,3 +148,13 @@ export * from "./patterns/task/index.js";
148
148
  export * from "./patterns/governance/index.js";
149
149
  export * from "./patterns/insights/index.js";
150
150
  export * from "./element-maps.js";
151
+ export * from "./components/feedback/status-icon.js";
152
+ export * from "./components/collections/fact-list.js";
153
+ export * from "./components/collections/check-list.js";
154
+ export * from "./components/collections/document-list.js";
155
+ export * from "./components/collections/result-blocks.js";
156
+ export * from "./patterns/run/run-summary.js";
157
+ export * from "./patterns/agent-workspace/index.js";
158
+ export * from "./patterns/call-console/index.js";
159
+ export * from "./foundations/status/index.js";
160
+ export * from "./patterns/run/progress.js";
@@ -16,6 +16,15 @@ export declare class AgentChat extends BaseElement {
16
16
  static readonly tagName: string;
17
17
  static get observedAttributes(): string[];
18
18
  private controller;
19
+ private readonly scope;
20
+ private composing;
21
+ private scrollPin?;
22
+ private resizeObserver?;
23
+ private messageCount;
24
+ private jumpEl;
25
+ focusComposer(): void;
26
+ proposalAnchor(messageId: string, proposalId: string): string;
27
+ focusProposal(messageId: string, proposalId: string): void;
19
28
  private ownsController;
20
29
  private pendingStart;
21
30
  private unsubscribeFns;
@@ -3,6 +3,11 @@ import { isSafeHref } from "../internal/safe-href.js";
3
3
  import { BaseElement } from "../../core/index.js";
4
4
  import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
5
5
  import { boePanel, boeRadius } from "../../foundations/geometry/index.js";
6
+ import { ScrollPinController, boeEntranceKeyframes, boeReducedMotionPolicy } from "../../foundations/motion/index.js";
7
+ import { boeStatusGlyph, boeStatusStyles } from "../../foundations/status/index.js";
8
+ import { ResultBlocks, resultDocumentIds } from "../../components/collections/result-blocks.js";
9
+ import { RunSummary } from "../run/run-summary.js";
10
+ let chatInstance = 0;
6
11
  const DEFAULT_TAG_NAME = "box-agent-chat";
7
12
  const escapeHtml = (value) => value
8
13
  .replaceAll("&", "&amp;")
@@ -365,9 +370,22 @@ const elementStyles = `
365
370
  export class AgentChat extends BaseElement {
366
371
  static tagName = DEFAULT_TAG_NAME;
367
372
  static get observedAttributes() {
368
- return ["agent-name", "heading", "placeholder", "token"];
373
+ return ["agent-name", "heading", "placeholder", "token", "hidden", "hide-citations"];
369
374
  }
370
375
  controller = null;
376
+ scope = `boe-chat-${++chatInstance}`;
377
+ composing = false;
378
+ scrollPin;
379
+ resizeObserver;
380
+ messageCount = 0;
381
+ jumpEl;
382
+ focusComposer() { this.inputEl?.focus({ preventScroll: true }); }
383
+ proposalAnchor(messageId, proposalId) { return `${this.scope}-${encodeURIComponent(messageId)}-${encodeURIComponent(proposalId)}`; }
384
+ focusProposal(messageId, proposalId) {
385
+ const card = this.shadowRoot?.getElementById(this.proposalAnchor(messageId, proposalId));
386
+ card?.scrollIntoView({ block: "nearest" });
387
+ card?.focus({ preventScroll: true });
388
+ }
371
389
  ownsController = false;
372
390
  pendingStart = false;
373
391
  unsubscribeFns = [];
@@ -425,6 +443,8 @@ export class AgentChat extends BaseElement {
425
443
  this.adoptController(value);
426
444
  }
427
445
  attributeChangedCallback(name, oldValue, newValue) {
446
+ if (name === "hidden" && newValue === null && this.isRendered)
447
+ this.focusComposer();
428
448
  if (name === "token") {
429
449
  this.scheduleStart();
430
450
  }
@@ -442,10 +462,26 @@ export class AgentChat extends BaseElement {
442
462
  }
443
463
  connectedCallback() {
444
464
  super.connectedCallback();
465
+ if (this.controller && !this.ownsController && this.unsubscribeFns.length === 0)
466
+ this.subscribeToController(this.controller);
467
+ this.scrollPin?.connect();
468
+ if (typeof ResizeObserver !== "undefined") {
469
+ this.resizeObserver = new ResizeObserver(() => this.scrollPin?.contentChanged());
470
+ this.resizeObserver.observe(this.threadEl);
471
+ for (const child of Array.from(this.threadEl.children))
472
+ this.resizeObserver.observe(child);
473
+ }
445
474
  this.scheduleStart();
446
475
  }
447
476
  disconnectedCallback() {
448
- this.teardownController();
477
+ this.scrollPin?.disconnect();
478
+ this.resizeObserver?.disconnect();
479
+ if (this.ownsController)
480
+ this.teardownController();
481
+ else {
482
+ this.unsubscribeFns.forEach(fn => fn());
483
+ this.unsubscribeFns = [];
484
+ }
449
485
  }
450
486
  /** Send the composer's contents (or an explicit body) as a user turn. */
451
487
  async send(body) {
@@ -455,11 +491,13 @@ export class AgentChat extends BaseElement {
455
491
  }
456
492
  // Clear only once the controller has accepted the turn, so a refused
457
493
  // 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) {
494
+ const accepted = this.controller?.getState().connected;
495
+ const sending = this.controller?.send(text);
496
+ if (accepted && body === undefined && this.inputEl) {
460
497
  this.inputEl.value = "";
461
498
  this.syncComposer();
462
499
  }
500
+ await sending;
463
501
  }
464
502
  /** Stop the in-flight generation; the partial reply is kept. */
465
503
  stop() {
@@ -558,6 +596,13 @@ export class AgentChat extends BaseElement {
558
596
  const canResolve = Boolean(this.controller?.config.transport.resolveAction);
559
597
  return message.proposals
560
598
  .map(proposal => {
599
+ if (proposal.decision) {
600
+ const failed = proposal.outcome === "failed";
601
+ return `<div part="proposal" tabindex="-1" id="${this.proposalAnchor(message.id, proposal.id)}" data-proposal-id="${escapeHtml(proposal.id)}" data-decision="${escapeHtml(proposal.decision)}" ${failed ? 'role="alert"' : ""}>
602
+ <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>
603
+ ${proposal.note ? `<p part="proposal-note">${escapeHtml(proposal.note)}</p>` : ""}
604
+ ${failed ? `<button part="retry" data-message-id="${escapeHtml(message.id)}">Try again</button>` : ""}</div>`;
605
+ }
561
606
  const params = (proposal.params ?? [])
562
607
  .map(param => `
563
608
  <dt part="param-label">${escapeHtml(param.label)}</dt>
@@ -567,20 +612,22 @@ export class AgentChat extends BaseElement {
567
612
  const actions = canResolve && !proposal.decision
568
613
  ? `
569
614
  <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>
615
+ <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>
616
+ <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>
617
+ <button type="button" part="proposal-action" data-action="modify" data-proposal-id="${escapeHtml(proposal.id)}" ${proposal.resolving ? "disabled" : ""}>Modify</button>
573
618
  </div>
574
619
  `
575
620
  : "";
576
621
  return `
577
- <div part="proposal" data-proposal-id="${escapeHtml(proposal.id)}"${proposal.decision ? ` data-decision="${escapeHtml(proposal.decision)}"` : ""}>
622
+ <div part="proposal" tabindex="-1" id="${this.proposalAnchor(message.id, proposal.id)}" data-proposal-id="${escapeHtml(proposal.id)}">
623
+ <span>${boeStatusGlyph("pending")} Needs your approval</span>
578
624
  <span part="proposal-title">${escapeHtml(proposal.title)}</span>
579
625
  ${proposal.summary ? `<p part="proposal-summary">${escapeHtml(proposal.summary)}</p>` : ""}
580
626
  ${params ? `<dl part="proposal-params">${params}</dl>` : ""}
581
627
  ${proposal.decision ? `<span part="decision">${proposal.decision === "approved" ? "Approved" : "Rejected"}</span>` : ""}
582
628
  ${proposal.note ? `<p part="proposal-note">${escapeHtml(proposal.note)}</p>` : ""}
583
629
  ${actions}
630
+ <p part="proposal-note">To change it, reply with the new values.</p>
584
631
  </div>
585
632
  `;
586
633
  })
@@ -589,7 +636,8 @@ export class AgentChat extends BaseElement {
589
636
  messageInnerHtml(message) {
590
637
  const name = message.role === "user" ? "You" : (message.actor?.name ?? this.agentName);
591
638
  const initials = message.actor?.initials ?? initialsOf(name);
592
- const citations = message.citations
639
+ const documentIds = resultDocumentIds(message.blocks ?? []);
640
+ const citations = (this.hasAttribute("hide-citations") ? [] : message.citations.filter(c => !documentIds.includes(c.id)))
593
641
  .map(citation => this.citationHtml(message.id, citation))
594
642
  .join("");
595
643
  return `
@@ -597,10 +645,14 @@ export class AgentChat extends BaseElement {
597
645
  <span part="avatar" aria-hidden="true">${escapeHtml(initials)}</span>
598
646
  <span part="author">${escapeHtml(name)}</span>
599
647
  </div>
600
- <p part="body">${escapeHtml(message.body)}${message.status === "streaming" ? `<span part="caret" aria-hidden="true"></span>` : ""}</p>
648
+ ${message.role === "agent" ? '<box-run-summary></box-run-summary>' : ""}
649
+ <p part="body">${escapeHtml(message.body)}${message.status === "streaming" && !message.blocks?.length ? `<span part="caret" aria-hidden="true"></span>` : ""}</p>
650
+ <box-result-blocks></box-result-blocks>
651
+ ${message.completeness?.status === "incomplete" ? '<p part="incomplete" role="status">This reply may be incomplete.</p>' : ""}
601
652
  ${message.status === "error" && message.errorMessage ? `<p part="error" role="alert">${escapeHtml(message.errorMessage)}</p>` : ""}
602
653
  ${citations ? `<div part="citations">${citations}</div>` : ""}
603
654
  ${this.proposalHtml(message)}
655
+ ${message.proposals.some(p => p.outcome === "failed") ? "" : (message.options ?? []).map((o, i) => `<button part="option" data-option="${i}">${escapeHtml(o.label)}</button>`).join("")}
604
656
  `;
605
657
  }
606
658
  /**
@@ -620,7 +672,12 @@ export class AgentChat extends BaseElement {
620
672
  },
621
673
  agentName: this.agentName,
622
674
  citations: message.citations.map(({ id, href, label }) => ({ id, href, label })),
623
- proposals: message.proposals.map(({ id, title, summary, params, decision, note }) => ({
675
+ hideCitations: this.hasAttribute("hide-citations"),
676
+ blocks: message.blocks,
677
+ completeness: message.completeness?.status,
678
+ options: message.options,
679
+ proposals: message.proposals.map(({ id, title, summary, params, decision, note, outcome, resolving }) => ({
680
+ outcome, resolving,
624
681
  id,
625
682
  title,
626
683
  summary,
@@ -656,21 +713,38 @@ export class AgentChat extends BaseElement {
656
713
  body.insertBefore(document.createTextNode(message.body), body.firstChild);
657
714
  }
658
715
  renderTemplate() {
716
+ ResultBlocks.register();
717
+ RunSummary.register();
659
718
  if (!this.shadowRoot) {
660
719
  return;
661
720
  }
662
721
  // The composer is part of the stable shell: a streaming reply patches
663
722
  // only [part="thread"], so typing is never interrupted.
664
723
  this.shadowRoot.innerHTML = `
665
- <style>${elementStyles}</style>
724
+ <style>${elementStyles}
725
+ [part="message"][data-role="agent"]{border:0;background:transparent;padding-inline:0;max-width:75ch;min-width:0}
726
+ [part="message"][data-role="user"]{background:var(--boe-token-surface-surface-secondary,#f4f4f4)}
727
+ [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)}
728
+ [part="proposal"][data-decision]{border:0;background:transparent;box-shadow:none;padding-inline:0}
729
+ [part="decision"]{background:none;border-radius:0;padding:0;gap:.4rem;font:inherit}
730
+ [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}
731
+ [part="proposal-action"]:disabled:not([aria-busy="true"]){opacity:.5}
732
+ [part="composer"]{position:relative;border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:12px;padding:.4rem}
733
+ [part="input"]{border:0;resize:none;max-height:180px;padding-right:5rem;box-sizing:border-box}
734
+ [part="composer-actions"]{position:absolute;right:.5rem;bottom:.5rem}
735
+ [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}
736
+ .sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
737
+ ${boeEntranceKeyframes}${boeStatusStyles}${boeReducedMotionPolicy}</style>
666
738
  <section part="panel">
667
739
  <div part="header">
668
740
  <h2 part="title"></h2>
669
741
  <span part="status" role="status" aria-live="polite"></span>
670
742
  </div>
671
- <ol part="thread" role="log" aria-live="polite" aria-relevant="additions"></ol>
743
+ <ol part="thread" role="log" aria-live="polite" aria-relevant="additions" tabindex="0"></ol>
744
+ <button part="jump" hidden>Jump to latest</button>
672
745
  <div part="composer">
673
- <textarea part="input" rows="2"></textarea>
746
+ <label class="sr-only" id="${this.scope}-label">Message</label>
747
+ <textarea part="input" rows="2" aria-labelledby="${this.scope}-label"></textarea>
674
748
  <div part="composer-actions">
675
749
  <button type="button" part="stop" hidden>Stop</button>
676
750
  <button type="button" part="send">Send</button>
@@ -685,6 +759,11 @@ export class AgentChat extends BaseElement {
685
759
  this.statusEl = this.shadowRoot.querySelector('[part="status"]');
686
760
  }
687
761
  setupListeners() {
762
+ this.jumpEl = this.shadowRoot.querySelector('[part="jump"]');
763
+ this.scrollPin = new ScrollPinController(this.threadEl, show => { this.jumpEl.hidden = !show; });
764
+ this.jumpEl.addEventListener("click", () => this.scrollPin?.jump());
765
+ this.inputEl.addEventListener("compositionstart", () => { this.composing = true; });
766
+ this.inputEl.addEventListener("compositionend", () => { this.composing = false; });
688
767
  this.sendEl.addEventListener("click", () => {
689
768
  void this.send();
690
769
  });
@@ -693,7 +772,7 @@ export class AgentChat extends BaseElement {
693
772
  });
694
773
  this.inputEl.addEventListener("keydown", event => {
695
774
  // Enter sends, Shift+Enter makes a newline — the conversational default.
696
- if (event.key === "Enter" && !event.shiftKey) {
775
+ if (event.key === "Enter" && !event.shiftKey && !event.isComposing && !this.composing && event.keyCode !== 229) {
697
776
  event.preventDefault();
698
777
  void this.send();
699
778
  }
@@ -703,15 +782,31 @@ export class AgentChat extends BaseElement {
703
782
  });
704
783
  this.threadEl.addEventListener("click", event => {
705
784
  const target = event.target;
785
+ const messageId = target.closest('[part="message"]')?.getAttribute("data-message-id") ?? "";
786
+ const option = target.closest("[data-option]");
787
+ if (option) {
788
+ const prompt = this.controller?.getMessage(messageId)?.options?.[Number(option.dataset.option)]?.prompt;
789
+ if (prompt)
790
+ void this.send(prompt);
791
+ return;
792
+ }
793
+ if (target.closest('[part="retry"]')) {
794
+ const messages = this.controller?.getState().messages ?? [];
795
+ const index = messages.findIndex(m => m.id === messageId);
796
+ const request = messages.slice(0, index).reverse().find(m => m.role === "user");
797
+ if (request)
798
+ void this.send(request.body);
799
+ return;
800
+ }
706
801
  const proposalButton = target.closest('[part="proposal-action"]');
707
802
  if (proposalButton && this.threadEl.contains(proposalButton)) {
708
803
  const proposalId = proposalButton.getAttribute("data-proposal-id") ?? "";
709
804
  const action = proposalButton.getAttribute("data-action") ?? "";
710
805
  if (action === "approve") {
711
- void this.controller?.resolveAction(proposalId, "approved");
806
+ void this.controller?.resolveAction(proposalId, "approved", undefined, messageId);
712
807
  }
713
808
  else if (action === "reject") {
714
- void this.controller?.resolveAction(proposalId, "rejected");
809
+ void this.controller?.resolveAction(proposalId, "rejected", undefined, messageId);
715
810
  }
716
811
  else if (action === "modify") {
717
812
  // Modifying needs the host's own editor, so this surfaces intent.
@@ -744,6 +839,9 @@ export class AgentChat extends BaseElement {
744
839
  const streaming = this.controller?.getState().streaming ?? false;
745
840
  this.inputEl.placeholder = this.placeholder;
746
841
  this.sendEl.disabled = streaming || this.inputEl.value.trim().length === 0;
842
+ this.sendEl.hidden = streaming;
843
+ this.inputEl.style.height = "auto";
844
+ this.inputEl.style.height = `${Math.min(180, Math.max(56, this.inputEl.scrollHeight))}px`;
747
845
  this.stopEl.hidden = !streaming;
748
846
  this.statusEl.textContent = streaming ? "Agent is responding…" : "";
749
847
  }
@@ -762,7 +860,8 @@ export class AgentChat extends BaseElement {
762
860
  : null;
763
861
  const state = this.controller?.getState() ?? null;
764
862
  const messages = state?.messages ?? [];
765
- const atBottom = this.threadEl.scrollHeight - this.threadEl.scrollTop - this.threadEl.clientHeight < 40;
863
+ const newMessage = messages.length > this.messageCount;
864
+ this.messageCount = messages.length;
766
865
  this.shadowRoot.querySelector('[part="title"]').textContent = this.heading;
767
866
  if (messages.length === 0) {
768
867
  if (!this.threadEl.querySelector('[part="empty"]')) {
@@ -793,7 +892,13 @@ export class AgentChat extends BaseElement {
793
892
  else {
794
893
  node.setAttribute("data-signature", signature);
795
894
  node.setAttribute("data-status", message.status);
895
+ const summary = node.querySelector("box-run-summary");
896
+ const results = node.querySelector("box-result-blocks");
796
897
  node.innerHTML = this.messageInnerHtml(message);
898
+ if (summary)
899
+ node.querySelector("box-run-summary")?.replaceWith(summary);
900
+ if (results)
901
+ node.querySelector("box-result-blocks")?.replaceWith(results);
797
902
  }
798
903
  }
799
904
  else {
@@ -806,8 +911,19 @@ export class AgentChat extends BaseElement {
806
911
  this.threadEl.insertBefore(node, anchor);
807
912
  }
808
913
  previous = node;
914
+ this.resizeObserver?.observe(node);
915
+ const blocks = node.querySelector("box-result-blocks");
916
+ if (blocks && blocks.blocks !== message.blocks)
917
+ blocks.blocks = message.blocks ?? [];
918
+ const summary = node.querySelector("box-run-summary");
919
+ if (summary) {
920
+ summary.toggleAttribute("failed", message.status === "error");
921
+ summary.turn = { steps: message.trace ?? [], todos: message.todos ?? [], startedAt: message.startedAt ?? 0,
922
+ ...(message.endedAt !== undefined ? { endedAt: message.endedAt } : {}), incomplete: message.completeness?.status === "incomplete" };
923
+ }
809
924
  }
810
925
  for (const stale of existing.values()) {
926
+ this.resizeObserver?.unobserve(stale);
811
927
  stale.remove();
812
928
  }
813
929
  if (focusKey?.part) {
@@ -817,9 +933,7 @@ export class AgentChat extends BaseElement {
817
933
  target?.focus();
818
934
  }
819
935
  // Follow the stream only when the reader is already at the bottom.
820
- if (atBottom) {
821
- this.threadEl.scrollTop = this.threadEl.scrollHeight;
822
- }
936
+ this.scrollPin?.contentChanged(newMessage);
823
937
  this.syncComposer();
824
938
  }
825
939
  }
@@ -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
  }