@unofficialbox/box-open-elements 0.21.0 → 0.23.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 (115) hide show
  1. package/README.md +13 -12
  2. package/dist/components/actions/menu-item.js +1 -1
  3. package/dist/components/actions/menu.js +1 -1
  4. package/dist/components/collections/carousel.js +1 -1
  5. package/dist/components/collections/draggable-list.js +1 -1
  6. package/dist/components/collections/grid-view.js +1 -1
  7. package/dist/components/collections/pagination.js +1 -1
  8. package/dist/components/collections/result-blocks.d.ts +13 -1
  9. package/dist/components/collections/result-blocks.js +22 -10
  10. package/dist/components/collections/tree-grid.js +1 -1
  11. package/dist/components/feedback/alert.js +1 -1
  12. package/dist/components/feedback/chip.js +1 -1
  13. package/dist/components/feedback/empty-state.js +1 -1
  14. package/dist/components/feedback/nudge.js +1 -1
  15. package/dist/components/feedback/status-icon.d.ts +2 -0
  16. package/dist/components/feedback/status-icon.js +6 -5
  17. package/dist/components/feedback/toast.js +1 -1
  18. package/dist/components/files/drop-zone.js +1 -1
  19. package/dist/components/forms/category-selector.js +1 -1
  20. package/dist/components/forms/checkbox-group.js +1 -1
  21. package/dist/components/forms/checkbox.js +1 -1
  22. package/dist/components/forms/combobox.js +1 -1
  23. package/dist/components/forms/date-field.js +1 -1
  24. package/dist/components/forms/dropdown.js +1 -1
  25. package/dist/components/forms/multi-select.js +1 -1
  26. package/dist/components/forms/number-input.js +1 -1
  27. package/dist/components/forms/radio-group.js +1 -1
  28. package/dist/components/forms/range-slider.js +1 -1
  29. package/dist/components/forms/slider.js +1 -1
  30. package/dist/components/forms/tile-group.js +1 -1
  31. package/dist/components/layout/sidebar-toggle-button.js +1 -1
  32. package/dist/components/navigation/accordion.js +1 -1
  33. package/dist/components/navigation/tabs.js +1 -1
  34. package/dist/components/output/code-block.js +1 -1
  35. package/dist/components/overlays/dialog.js +1 -1
  36. package/dist/components/overlays/drawer.js +1 -1
  37. package/dist/components/overlays/guide-tooltip.js +1 -1
  38. package/dist/components/overlays/popover.js +1 -1
  39. package/dist/components/overlays/tooltip.js +1 -1
  40. package/dist/foundations/motion/scroll-pin.d.ts +10 -2
  41. package/dist/foundations/motion/scroll-pin.js +43 -7
  42. package/dist/foundations/motion/tokens.d.ts +1 -0
  43. package/dist/foundations/motion/tokens.js +1 -0
  44. package/dist/foundations/profiles/built-ins.js +8 -0
  45. package/dist/foundations/profiles/registry.d.ts +1 -0
  46. package/dist/foundations/profiles/registry.js +1 -0
  47. package/dist/foundations/profiles/types.d.ts +1 -0
  48. package/dist/foundations/status/index.d.ts +4 -2
  49. package/dist/foundations/status/index.js +10 -6
  50. package/dist/foundations/theming/controller.js +3 -1
  51. package/dist/foundations/tokens/box-defaults.d.ts +2 -0
  52. package/dist/foundations/tokens/box-defaults.js +32 -5
  53. package/dist/foundations/tokens/index.d.ts +1 -1
  54. package/dist/foundations/tokens/index.js +1 -1
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/patterns/agent-chat/agent-chat.js +9 -10
  58. package/dist/patterns/agent-chat/controller.js +8 -4
  59. package/dist/patterns/agent-chat/ndjson.d.ts +1 -0
  60. package/dist/patterns/agent-chat/ndjson.js +20 -4
  61. package/dist/patterns/agent-chat/types.d.ts +7 -0
  62. package/dist/patterns/agent-workspace/agent-workspace.d.ts +13 -1
  63. package/dist/patterns/agent-workspace/agent-workspace.js +47 -15
  64. package/dist/patterns/agent-workspace/controller.d.ts +7 -1
  65. package/dist/patterns/agent-workspace/controller.js +41 -3
  66. package/dist/patterns/call-console/call-console.d.ts +10 -0
  67. package/dist/patterns/call-console/call-console.js +23 -8
  68. package/dist/patterns/call-console/controller.d.ts +2 -1
  69. package/dist/patterns/call-console/controller.js +5 -3
  70. package/dist/patterns/call-console/http-transport.js +13 -2
  71. package/dist/patterns/call-console/types.d.ts +1 -1
  72. package/dist/patterns/call-console/types.js +2 -2
  73. package/dist/patterns/comments/comment-thread.js +1 -1
  74. package/dist/patterns/content-explorer/adapters/action-menu.js +1 -1
  75. package/dist/patterns/content-explorer/adapters/list.js +1 -1
  76. package/dist/patterns/content-explorer/adapters/table.js +1 -1
  77. package/dist/patterns/content-explorer/adapters/toolbar.js +1 -1
  78. package/dist/patterns/content-uploader/content-uploader.js +1 -1
  79. package/dist/patterns/file-request/file-request-builder.js +1 -1
  80. package/dist/patterns/flow-builder/flow-builder.d.ts +45 -14
  81. package/dist/patterns/flow-builder/flow-builder.js +54 -11
  82. package/dist/patterns/flow-builder/model.d.ts +57 -9
  83. package/dist/patterns/flow-builder/model.js +20 -8
  84. package/dist/patterns/flow-builder/primitives.d.ts +17 -7
  85. package/dist/patterns/flow-builder/primitives.js +80 -16
  86. package/dist/patterns/governance/governance-panel.js +1 -1
  87. package/dist/patterns/insights/chart-panel.js +1 -1
  88. package/dist/patterns/insights/metric-card.js +1 -1
  89. package/dist/patterns/item/bulk-action-bar.js +1 -1
  90. package/dist/patterns/item/item-details-panel.js +1 -1
  91. package/dist/patterns/item/item-form.js +1 -1
  92. package/dist/patterns/item/preview-header.js +1 -1
  93. package/dist/patterns/metadata/metadata-filter-builder.js +1 -1
  94. package/dist/patterns/metadata/metadata-inspector.js +1 -1
  95. package/dist/patterns/preview/annotation-inspector.js +1 -1
  96. package/dist/patterns/preview/annotation-toolbar.js +1 -1
  97. package/dist/patterns/run/progress.d.ts +1 -1
  98. package/dist/patterns/run/progress.js +1 -1
  99. package/dist/patterns/run/run-summary.js +18 -4
  100. package/dist/patterns/run/run-trace.js +19 -5
  101. package/dist/patterns/search/filter-bar.js +1 -1
  102. package/dist/patterns/search/saved-view-picker.js +1 -1
  103. package/dist/patterns/search/search-results-header.js +1 -1
  104. package/dist/patterns/share/invite-collaborators-modal.js +1 -1
  105. package/dist/patterns/share/permission-matrix.js +1 -1
  106. package/dist/patterns/share/share-panel.js +1 -1
  107. package/dist/patterns/share/unified-share-modal.js +1 -1
  108. package/dist/patterns/task/review-queue-item.js +1 -1
  109. package/dist/patterns/task/task-assignment-panel.js +1 -1
  110. package/dist/patterns/undo/index.js +5 -3
  111. package/docs/api-guidelines.md +284 -0
  112. package/docs/foundations/motion.md +83 -0
  113. package/docs/patterns/agent-ui-guidelines.md +57 -0
  114. package/docs/patterns/agent-ui.md +197 -0
  115. package/package.json +6 -2
@@ -1,12 +1,12 @@
1
1
  import { BaseElement } from "../../core/element.js";
2
2
  import { AgentChat } from "../agent-chat/agent-chat.js";
3
3
  import { isSafeHref } from "../internal/safe-href.js";
4
- import { boeReducedMotionPolicy } from "../../foundations/motion/index.js";
4
+ import { boeMotionEasing, boeReducedMotionPolicy } from "../../foundations/motion/index.js";
5
5
  import { trapTabKey } from "../../foundations/a11y/focus.js";
6
6
  const DEFAULT_TAG_NAME = "box-agent-workspace";
7
7
  export class AgentWorkspace extends BaseElement {
8
8
  static tagName = DEFAULT_TAG_NAME;
9
- static get observedAttributes() { return ["viewer-key"]; }
9
+ static get observedAttributes() { return ["viewer-key", "hide-toggles"]; }
10
10
  model = null;
11
11
  unsubscribe;
12
12
  observer;
@@ -15,7 +15,20 @@ export class AgentWorkspace extends BaseElement {
15
15
  detailsOpen = false;
16
16
  initialized = false;
17
17
  renderedActive = null;
18
+ chatIds = "";
19
+ lastPanes = null;
18
20
  sessions = new Map();
21
+ /** Host-controlled pane state; mobile drawers remain mutually exclusive. */
22
+ get panes() { return { chats: this.chatsOpen, details: this.detailsOpen }; }
23
+ set panes(value) {
24
+ if (value.chats !== undefined)
25
+ this.chatsOpen = value.chats;
26
+ if (value.details !== undefined)
27
+ this.detailsOpen = value.details;
28
+ if (this.mobile && this.chatsOpen && this.detailsOpen)
29
+ this.chatsOpen = false;
30
+ this.syncPanes();
31
+ }
19
32
  get workspaceController() { return this.model; }
20
33
  set workspaceController(value) {
21
34
  this.unsubscribe?.();
@@ -58,15 +71,16 @@ export class AgentWorkspace extends BaseElement {
58
71
  this.syncPanes();
59
72
  }
60
73
  storageKey() { return `boe-workspace:${this.getAttribute("viewer-key") ?? "default"}`; }
61
- toggle(pane) {
74
+ toggle(pane) { this.open(pane, !(pane === "chats" ? this.chatsOpen : this.detailsOpen)); }
75
+ open(pane, open) {
62
76
  if (pane === "chats") {
63
- this.chatsOpen = !this.chatsOpen;
64
- if (this.mobile)
77
+ this.chatsOpen = open;
78
+ if (this.mobile && open)
65
79
  this.detailsOpen = false;
66
80
  }
67
81
  else {
68
- this.detailsOpen = !this.detailsOpen;
69
- if (this.mobile)
82
+ this.detailsOpen = open;
83
+ if (this.mobile && open)
70
84
  this.chatsOpen = false;
71
85
  }
72
86
  if (!this.mobile)
@@ -75,13 +89,16 @@ export class AgentWorkspace extends BaseElement {
75
89
  }
76
90
  catch { }
77
91
  this.syncPanes();
78
- if (this.mobile && (this.chatsOpen || this.detailsOpen))
92
+ if (this.mobile && open)
79
93
  this.shadowRoot.querySelector(`#${pane} button`)?.focus({ preventScroll: true });
80
94
  }
81
95
  closeDrawers() { this.chatsOpen = false; this.detailsOpen = false; this.syncPanes(); this.sessions.get(this.model?.activeId ?? "")?.focusComposer(); }
82
96
  syncPanes() {
83
97
  if (!this.isRendered)
84
98
  return;
99
+ const toggleGroup = this.shadowRoot.querySelector('[part="toggles"]');
100
+ if (toggleGroup)
101
+ toggleGroup.hidden = this.hasAttribute("hide-toggles");
85
102
  this.toggleAttribute("data-mobile", this.mobile);
86
103
  for (const [pane, open] of [["chats", this.chatsOpen], ["details", this.detailsOpen]]) {
87
104
  const el = this.shadowRoot.getElementById(pane);
@@ -103,13 +120,19 @@ export class AgentWorkspace extends BaseElement {
103
120
  this.shadowRoot.querySelector("#scrim").hidden = !overlaid;
104
121
  for (const chat of this.sessions.values())
105
122
  chat.toggleAttribute("hide-citations", this.detailsOpen);
123
+ for (const pane of ["chats", "details"]) {
124
+ const open = this.panes[pane];
125
+ if (this.lastPanes?.[pane] !== open)
126
+ this.dispatchEvent(new CustomEvent("pane-change", { bubbles: true, composed: true, detail: { pane, open, mobile: this.mobile } }));
127
+ }
128
+ this.lastPanes = this.panes;
106
129
  }
107
130
  renderTemplate() {
108
131
  AgentChat.register();
109
132
  this.shadowRoot.innerHTML = `<style>
110
133
  :host{display:block;height:100%;min-height:30rem;color:var(--boe-token-text-text,#222);font:inherit}:host([hidden]){display:none !important}
111
134
  *{box-sizing:border-box} section{position:relative;overflow:clip;height:100%;display:flex;min-width:0}
112
- aside{width:0;overflow:clip;transition:width var(--boe-profile-motion-panel,320ms) ease-out;background:var(--boe-token-surface-surface,#fff)} aside[data-open]{width:280px;flex-shrink:0}
135
+ aside{width:0;overflow:clip;transition:width var(--boe-profile-motion-panel,320ms) ${boeMotionEasing.enter};background:var(--boe-token-surface-surface,#fff)} aside[data-open]{width:280px;flex-shrink:0}
113
136
  aside>div{width:280px;height:100%;padding:1rem;overflow:auto} main{min-width:0;flex:1;overflow:clip;display:flex;flex-direction:column;background:var(--boe-token-surface-surface,#fff)} nav{display:flex;justify-content:space-between;padding:.5rem}
114
137
  #conversation{flex:1;min-height:0} #sessions,box-agent-chat{height:100%}
115
138
  box-agent-chat::part(panel){height:100%;box-sizing:border-box;display:flex;flex-direction:column;border:0;border-radius:0}
@@ -118,13 +141,13 @@ export class AgentWorkspace extends BaseElement {
118
141
  button:focus-visible,a:focus-visible{outline:2px solid var(--boe-token-surface-surface-brand,#0061d5);outline-offset:2px}
119
142
  [part="chat"]{display:block;text-align:left;width:100%;border:0;background:none;padding:.65rem;border-radius:6px}
120
143
  [aria-current="page"]{background:var(--boe-token-surface-surface-secondary,#f3f3f3)} small{display:block;color:var(--boe-token-text-text-secondary,#666)}
121
- #scrim{position:absolute;inset:0;background:#0005;border:0;border-radius:0;z-index:2} [hidden]{display:none!important}
144
+ #scrim{position:absolute;inset:0;background:var(--boe-workspace-scrim,rgba(0,0,0,.33));border:0;border-radius:0;z-index:2} [hidden]{display:none!important}
122
145
  [data-close]{display:none} :host([data-mobile]) [data-close]{display:block;margin-bottom:.5rem}
123
146
  :host([data-mobile]) aside{position:absolute;inset-block:0;z-index:3} :host([data-mobile]) #details{right:0}
124
147
  #approvals button,#sources a{display:block;margin:.5rem 0;text-align:left} #context{white-space:pre-wrap;overflow-wrap:anywhere}
125
148
  ${boeReducedMotionPolicy}</style><section part="workspace">
126
149
  <aside id="chats" part="chats"><div><button data-close>Close conversations</button><slot name="chats"><button id="new">New chat</button><div id="list"></div></slot></div></aside>
127
- <main><nav><button data-toggle="chats" aria-controls="chats">Conversations</button><button data-toggle="details" aria-controls="details">Details</button></nav><div id="conversation" part="conversation"><slot name="conversation"><div id="sessions"></div></slot></div></main>
150
+ <main><nav><slot name="header"><div part="toggles" ${this.hasAttribute("hide-toggles") ? "hidden" : ""}><button data-toggle="chats" aria-controls="chats">Conversations</button><button data-toggle="details" aria-controls="details">Details</button></div></slot></nav><div id="conversation" part="conversation"><slot name="conversation"><div id="sessions"></div></slot></div></main>
128
151
  <button id="scrim" aria-label="Close panel" hidden></button>
129
152
  <aside id="details" part="details"><div><button data-close>Close details</button><slot name="details"><h2>Conversation details</h2><div id="context"></div><h3>Approvals</h3><div id="approvals"></div><h3>Sources</h3><div id="sources"></div></slot></div></aside></section>`;
130
153
  }
@@ -134,6 +157,8 @@ export class AgentWorkspace extends BaseElement {
134
157
  this.shadowRoot.querySelector("#scrim").addEventListener("click", () => this.closeDrawers());
135
158
  this.shadowRoot.querySelectorAll("[data-close]").forEach(b => b.addEventListener("click", () => this.closeDrawers()));
136
159
  this.shadowRoot.querySelectorAll("[data-toggle]").forEach(b => b.addEventListener("click", () => this.toggle(b.dataset.toggle)));
160
+ this.shadowRoot.querySelector('slot[name="conversation"]').addEventListener("slotchange", () => this.update());
161
+ this.shadowRoot.querySelector('slot[name="chats"]').addEventListener("slotchange", () => this.update());
137
162
  this.shadowRoot.addEventListener("keydown", event => {
138
163
  const e = event;
139
164
  if (e.key === "Escape" && this.mobile) {
@@ -148,9 +173,15 @@ export class AgentWorkspace extends BaseElement {
148
173
  if (!this.isRendered)
149
174
  return;
150
175
  const list = this.shadowRoot.querySelector("#list");
176
+ const hostChats = this.shadowRoot.querySelector('slot[name="chats"]').assignedElements().length > 0;
177
+ const ids = (this.model?.chats ?? []).map(chat => chat.id).join("\u0000");
178
+ if (ids !== this.chatIds) {
179
+ this.chatIds = ids;
180
+ this.dispatchEvent(new CustomEvent("chats-change", { bubbles: true, composed: true, detail: { ids: (this.model?.chats ?? []).map(chat => chat.id), activeId: this.model?.activeId ?? null } }));
181
+ }
151
182
  const activeRow = this.shadowRoot.activeElement?.getAttribute("data-chat-id");
152
183
  list.replaceChildren();
153
- for (const summary of this.model?.summaries ?? []) {
184
+ for (const summary of hostChats ? [] : this.model?.summaries ?? []) {
154
185
  const b = document.createElement("button");
155
186
  b.setAttribute("part", "chat");
156
187
  b.dataset.chatId = summary.id;
@@ -160,18 +191,19 @@ export class AgentWorkspace extends BaseElement {
160
191
  const small = document.createElement("small");
161
192
  small.textContent = summary.status;
162
193
  b.append(small);
163
- b.addEventListener("click", () => { this.model?.select(summary.id); if (this.mobile)
194
+ b.addEventListener("click", () => { this.model?.select(summary.id); this.dispatchEvent(new CustomEvent("conversation-selected", { bubbles: true, composed: true, detail: { id: summary.id } })); if (this.mobile)
164
195
  this.closeDrawers(); });
165
196
  list.append(b);
166
197
  if (summary.id === activeRow)
167
198
  b.focus({ preventScroll: true });
168
199
  }
200
+ const hostConversation = this.shadowRoot.querySelector('slot[name="conversation"]').assignedElements().length > 0;
169
201
  for (const [id, element] of this.sessions)
170
- if (!this.model?.chats.some(c => c.id === id && c.controller === element.chatController)) {
202
+ if (hostConversation || !this.model?.chats.some(c => c.id === id && c.controller === element.chatController)) {
171
203
  element.remove();
172
204
  this.sessions.delete(id);
173
205
  }
174
- for (const chat of this.model?.chats ?? []) {
206
+ for (const chat of hostConversation ? [] : this.model?.chats ?? []) {
175
207
  let element = this.sessions.get(chat.id);
176
208
  if (!element) {
177
209
  element = new AgentChat();
@@ -23,15 +23,21 @@ export declare function conversationDetails(state: Readonly<AgentChatState>): Wo
23
23
  export declare class AgentWorkspaceController {
24
24
  private readonly createSession;
25
25
  readonly chats: WorkspaceChat[];
26
+ private readonly removed;
26
27
  activeId: string | null;
27
28
  private seq;
28
29
  private listeners;
29
30
  private subscriptions;
30
- constructor(createSession: () => AgentChatController);
31
+ constructor(createSession: (id: string, seed?: unknown) => AgentChatController);
31
32
  subscribe(listener: () => void): () => void;
32
33
  private emit;
33
34
  newChat(): WorkspaceChat;
34
35
  select(id: string): void;
36
+ /** Temporarily remove a conversation without destroying its stream or history. */
37
+ remove(id: string): boolean;
38
+ restore(id: string, seed?: unknown): WorkspaceChat | null;
39
+ removeChat(id: string): boolean;
40
+ restoreChat(id: string, seed?: unknown): boolean;
35
41
  get summaries(): WorkspaceSummary[];
36
42
  get details(): WorkspaceDetails;
37
43
  destroy(): void;
@@ -4,7 +4,8 @@ export const conversationTitle = (body, max = 48) => {
4
4
  return text || "New chat";
5
5
  const cut = text.slice(0, max);
6
6
  const space = cut.lastIndexOf(" ");
7
- return `${space > 0 ? cut.slice(0, space) : cut}…`;
7
+ const end = space >= max / 2 ? cut.slice(0, space) : cut;
8
+ return `${end.replace(/[\s,.;:!?-]+$/g, "")}…`;
8
9
  };
9
10
  export function conversationDetails(state) {
10
11
  const sources = new Map();
@@ -31,6 +32,7 @@ export function conversationDetails(state) {
31
32
  export class AgentWorkspaceController {
32
33
  createSession;
33
34
  chats = [];
35
+ removed = new Map();
34
36
  activeId = null;
35
37
  seq = 0;
36
38
  listeners = new Set();
@@ -43,7 +45,8 @@ export class AgentWorkspaceController {
43
45
  newChat() {
44
46
  let chat = this.chats.find(c => c.controller.getState().messages.length === 0);
45
47
  if (!chat) {
46
- chat = { id: `chat-${++this.seq}`, controller: this.createSession() };
48
+ const id = `chat-${++this.seq}`;
49
+ chat = { id, controller: this.createSession(id) };
47
50
  chat.controller.connect();
48
51
  this.chats.push(chat);
49
52
  this.subscriptions.push(chat.controller.subscribe("messagesChanged", () => this.emit()), chat.controller.subscribe("streamingChanged", () => this.emit()));
@@ -53,6 +56,41 @@ export class AgentWorkspaceController {
53
56
  }
54
57
  select(id) { if (!this.chats.some(c => c.id === id))
55
58
  return; this.activeId = id; this.emit(); }
59
+ /** Temporarily remove a conversation without destroying its stream or history. */
60
+ remove(id) {
61
+ const index = this.chats.findIndex(chat => chat.id === id);
62
+ if (index < 0)
63
+ return false;
64
+ const [chat] = this.chats.splice(index, 1);
65
+ chat.controller.stop();
66
+ this.removed.set(id, { chat: chat, index });
67
+ if (this.activeId === id)
68
+ this.activeId = this.chats[Math.min(index, this.chats.length - 1)]?.id ?? null;
69
+ this.emit();
70
+ return true;
71
+ }
72
+ restore(id, seed) {
73
+ const removed = this.removed.get(id);
74
+ if (this.chats.some(chat => chat.id === id))
75
+ return null;
76
+ if (removed) {
77
+ this.removed.delete(id);
78
+ this.chats.splice(Math.min(removed.index, this.chats.length), 0, removed.chat);
79
+ this.select(id);
80
+ return removed.chat;
81
+ }
82
+ const chat = { id, controller: this.createSession(id, seed) };
83
+ chat.controller.connect();
84
+ this.chats.push(chat);
85
+ this.subscriptions.push(chat.controller.subscribe("messagesChanged", () => this.emit()), chat.controller.subscribe("streamingChanged", () => this.emit()));
86
+ const match = /^chat-(\d+)$/.exec(id);
87
+ if (match)
88
+ this.seq = Math.max(this.seq, Number(match[1]));
89
+ this.select(id);
90
+ return chat;
91
+ }
92
+ removeChat(id) { return this.remove(id); }
93
+ restoreChat(id, seed) { return this.restore(id, seed) !== null; }
56
94
  get summaries() {
57
95
  return this.chats.map(c => {
58
96
  const state = c.controller.getState();
@@ -61,5 +99,5 @@ export class AgentWorkspaceController {
61
99
  });
62
100
  }
63
101
  get details() { const chat = this.chats.find(c => c.id === this.activeId); return chat ? conversationDetails(chat.controller.getState()) : { approvals: [], sources: [] }; }
64
- destroy() { this.subscriptions.forEach(fn => fn()); this.subscriptions = []; this.chats.forEach(c => { c.controller.disconnect(); c.controller.destroy(); }); this.listeners.clear(); }
102
+ destroy() { this.subscriptions.forEach(fn => fn()); this.subscriptions = []; [...this.chats, ...[...this.removed.values()].map(item => item.chat)].forEach(c => { c.controller.disconnect(); c.controller.destroy(); }); this.removed.clear(); this.listeners.clear(); }
65
103
  }
@@ -1,15 +1,25 @@
1
1
  import { BaseElement } from "../../core/element.js";
2
+ import { type CallEntry } from "./types.js";
2
3
  import type { CallConsoleController } from "./controller.js";
3
4
  export declare function copyCallText(text: string, doc?: Document): Promise<boolean>;
4
5
  export declare class CallConsole extends BaseElement {
5
6
  static readonly tagName = "box-call-console";
7
+ static get observedAttributes(): string[];
6
8
  private model;
9
+ private labels;
10
+ private failureRule;
7
11
  private unsubscribe?;
8
12
  private selected;
9
13
  private clearing;
10
14
  private rowSignatures;
11
15
  get callController(): CallConsoleController | null;
12
16
  set callController(value: CallConsoleController | null);
17
+ get heading(): string;
18
+ set heading(value: string);
19
+ get serviceLabels(): Record<string, string>;
20
+ set serviceLabels(value: Record<string, string>);
21
+ get isFailed(): (entry: CallEntry) => boolean;
22
+ set isFailed(value: (entry: CallEntry) => boolean);
13
23
  private listen;
14
24
  connectedCallback(): void;
15
25
  disconnectedCallback(): void;
@@ -30,7 +30,10 @@ export async function copyCallText(text, doc = document) {
30
30
  }
31
31
  export class CallConsole extends BaseElement {
32
32
  static tagName = DEFAULT_TAG_NAME;
33
+ static get observedAttributes() { return ["heading", "hide-heading"]; }
33
34
  model = null;
35
+ labels = {};
36
+ failureRule = callFailed;
34
37
  unsubscribe;
35
38
  selected = "";
36
39
  clearing = false;
@@ -39,6 +42,14 @@ export class CallConsole extends BaseElement {
39
42
  set callController(value) { this.unsubscribe?.(); this.model = value; if (this.isConnected)
40
43
  this.listen(); if (this.isRendered)
41
44
  this.update(); }
45
+ get heading() { return this.getAttribute("heading") ?? "API calls"; }
46
+ set heading(value) { this.setAttribute("heading", value); }
47
+ get serviceLabels() { return this.labels; }
48
+ set serviceLabels(value) { this.labels = value; if (this.isRendered)
49
+ this.update(); }
50
+ get isFailed() { return this.failureRule; }
51
+ set isFailed(value) { this.failureRule = value; if (this.isRendered)
52
+ this.update(); }
42
53
  listen() { this.unsubscribe?.(); this.unsubscribe = this.model?.subscribe(() => this.update()); }
43
54
  connectedCallback() { super.connectedCallback(); this.listen(); }
44
55
  disconnectedCallback() { this.unsubscribe?.(); }
@@ -73,7 +84,7 @@ export class CallConsole extends BaseElement {
73
84
  @container(max-width:640px){box-split-view::part(split-view){grid-template-columns:minmax(0,1fr)!important}box-split-view::part(separator){display:none}#list{min-height:0;max-height:17rem}#inspector{border-top:1px solid var(--boe-token-stroke-stroke,#ddd)}.search{flex-basis:100%}}
74
85
  ${boeEntranceKeyframes}${boeStatusStyles}${boeReducedMotionPolicy}</style>
75
86
  <section part="console" aria-label="API call console">
76
- <header><div><h2>API calls</h2><small>Inspect requests, responses, and tool outcomes</small></div><span id="connection" role="status"></span><box-button id="retry" label="Reconnect" tone="neutral" hidden></box-button></header>
87
+ <header><div part="heading"><h2></h2><small>Inspect requests, responses, and tool outcomes</small></div><span id="connection" role="status"></span><box-button id="retry" label="Reconnect" tone="neutral" hidden></box-button></header>
77
88
  <div class="filters"><label class="search">Search <input id="search" type="search" placeholder="Method, URL, or summary"></label><label>Service <select id="service" aria-label="Service"><option value="">All services</option></select></label><label><input type="checkbox" id="errors"> Errors only</label><box-button id="clear" label="Clear calls" tone="neutral"></box-button></div>
78
89
  <box-split-view resizable ratio="0.42" label="Calls and request inspector"><section slot="primary" aria-label="Calls"><div class="pane-heading"><h3>Requests</h3><span id="count" class="muted" role="status"></span></div><div id="list"></div><p id="list-empty" role="status" hidden></p></section><section aria-label="Request inspector"><div class="pane-heading"><h3>Inspector</h3><box-button data-copy="both" label="Copy both" tone="neutral"></box-button><span id="copy-status" role="status"></span></div><p id="detail-empty">Select a request to inspect its details.</p><div id="inspector" hidden><h3 id="summary"></h3><p id="metadata" class="muted"></p><p id="outcome" role="status" hidden></p><h3 class="code-heading">Request</h3><box-code-block id="request" copy-label="Copy request" wrap></box-code-block><h3 class="code-heading">Response</h3><box-code-block id="response" copy-label="Copy response" wrap></box-code-block></div></section></box-split-view></section>`;
79
90
  }
@@ -125,6 +136,8 @@ export class CallConsole extends BaseElement {
125
136
  update() {
126
137
  if (!this.isRendered)
127
138
  return;
139
+ this.shadowRoot.querySelector('[part="heading"]').hidden = this.hasAttribute("hide-heading");
140
+ this.shadowRoot.querySelector("h2").textContent = this.heading;
128
141
  const state = this.model?.connection ?? "connecting";
129
142
  const connection = this.shadowRoot.querySelector("#connection");
130
143
  connection.textContent = this.model ? ({ live: "Live", connecting: "Connecting…", reconnecting: "Reconnecting…", unavailable: "Endpoint unavailable" }[state]) : "Not connected";
@@ -138,13 +151,15 @@ export class CallConsole extends BaseElement {
138
151
  if (!Array.from(service.options).some(o => o.value === name)) {
139
152
  const option = document.createElement("option");
140
153
  option.value = name;
141
- option.textContent = name;
142
154
  service.append(option);
143
155
  }
156
+ for (const option of Array.from(service.options))
157
+ if (option.value)
158
+ option.textContent = this.labels[option.value] ?? option.value;
144
159
  const errors = this.shadowRoot.querySelector("#errors").checked;
145
- const entries = this.model?.entries.filter(e => (!service.value || e.service === service.value) && (!errors || callFailed(e))) ?? [];
160
+ const entries = this.model?.entries.filter(e => (!service.value || e.service === service.value) && (!errors || this.failureRule(e))) ?? [];
146
161
  const query = this.filters.query.trim().toLowerCase();
147
- const filtered = entries.filter(e => `${e.method} ${e.url} ${e.summary} ${e.service}`.toLowerCase().includes(query));
162
+ const filtered = entries.filter(e => `${e.method} ${e.url} ${e.summary} ${e.service} ${this.labels[e.service] ?? ""}`.toLowerCase().includes(query));
148
163
  entries.splice(0, entries.length, ...filtered);
149
164
  if (!entries.some(e => e.id === this.selected))
150
165
  this.selected = entries[0]?.id ?? "";
@@ -163,12 +178,12 @@ export class CallConsole extends BaseElement {
163
178
  button.addEventListener("click", () => { this.selected = entry.id; this.update(); this.dispatchEvent(new CustomEvent("call-selected", { bubbles: true, composed: true, detail: { id: entry.id } })); });
164
179
  }
165
180
  button.setAttribute("aria-current", String(entry.id === this.selected));
166
- const labels = [callStatus(entry), entry.service, entry.summary, entry.pending ? "In progress" : `${Math.round(entry.durationMs)} ms`];
181
+ const labels = [callStatus(entry, this.failureRule), this.labels[entry.service] ?? entry.service, entry.summary, entry.pending ? "In progress" : `${Math.round(entry.durationMs)} ms`];
167
182
  const signature = JSON.stringify(labels);
168
183
  if (this.rowSignatures.get(button) !== signature) {
169
184
  button.replaceChildren(...labels.map((text, index) => { const span = document.createElement("span"); span.textContent = text; span.className = ["state", "muted", "summary", "duration"][index]; if (index === 0) {
170
185
  const icon = document.createElement("span");
171
- icon.innerHTML = boeStatusGlyph(entry.pending ? "active" : callFailed(entry) ? "failed" : entry.expected ? "skipped" : "done");
186
+ icon.innerHTML = boeStatusGlyph(this.failureRule(entry) ? "failed" : entry.pending ? "active" : entry.expected ? "skipped" : "done");
172
187
  span.prepend(icon);
173
188
  } return span; }));
174
189
  this.rowSignatures.set(button, signature);
@@ -184,13 +199,13 @@ export class CallConsole extends BaseElement {
184
199
  this.shadowRoot.querySelector("#count").textContent = `${entries.length} of ${this.model?.entries.length ?? 0}`;
185
200
  const empty = this.shadowRoot.querySelector("#list-empty");
186
201
  empty.hidden = entries.length > 0 || !!this.model && state === "connecting";
187
- empty.textContent = !this.model ? "Connect a call controller to inspect requests." : state === "unavailable" ? "The /calls endpoint is unavailable. Start the agent, then reconnect." : state === "reconnecting" ? "Connection interrupted. Reconnect to load calls." : this.model.entries.length ? "No requests match these filters." : "No calls yet. New requests will appear here.";
202
+ empty.textContent = !this.model ? "Connect a call controller to inspect requests." : state === "unavailable" ? "Call history is unavailable. Reconnect when the service is ready." : state === "reconnecting" ? "Connection interrupted. Reconnect to load calls." : this.model.entries.length ? "No requests match these filters." : "No calls yet. New requests will appear here.";
188
203
  const entry = entries.find(e => e.id === this.selected);
189
204
  this.shadowRoot.querySelector('[data-copy="both"]').disabled = !entry;
190
205
  this.shadowRoot.querySelector("#inspector").hidden = !entry;
191
206
  this.shadowRoot.querySelector("#detail-empty").hidden = !!entry;
192
207
  this.shadowRoot.querySelector("#summary").textContent = entry?.summary ?? "";
193
- this.shadowRoot.querySelector("#metadata").textContent = entry ? `${entry.service} · ${entry.method} · ${entry.pending ? "In progress" : `${entry.durationMs} ms`} · ${new Date(entry.startedAt).toLocaleTimeString()}` : "";
208
+ this.shadowRoot.querySelector("#metadata").textContent = entry ? `${this.labels[entry.service] ?? entry.service} · ${entry.method} · ${entry.pending ? "In progress" : `${entry.durationMs} ms`} · ${new Date(entry.startedAt).toLocaleTimeString()}` : "";
194
209
  const outcome = this.shadowRoot.querySelector("#outcome");
195
210
  outcome.hidden = !entry || !(entry.error || entry.rpcError || entry.expected || entry.pending);
196
211
  outcome.textContent = entry?.rpcError ? `Tool failed despite HTTP ${entry.status}: ${entry.rpcError}` : entry?.error ?? entry?.expected ?? (entry?.pending ? "Waiting for the response…" : "");
@@ -7,12 +7,13 @@ export interface CallConsoleTransport {
7
7
  /** Transport injection keeps networking out of the component. */
8
8
  export declare class CallConsoleController {
9
9
  readonly transport: CallConsoleTransport;
10
+ readonly maxEntries: number;
10
11
  entries: CallEntry[];
11
12
  connection: CallConnection;
12
13
  private listeners;
13
14
  private abort?;
14
15
  private close?;
15
- constructor(transport: CallConsoleTransport);
16
+ constructor(transport: CallConsoleTransport, maxEntries?: number);
16
17
  subscribe(fn: () => void): () => void;
17
18
  private emit;
18
19
  apply(event: CallEvent): void;
@@ -1,23 +1,25 @@
1
1
  /** Transport injection keeps networking out of the component. */
2
2
  export class CallConsoleController {
3
3
  transport;
4
+ maxEntries;
4
5
  entries = [];
5
6
  connection = "connecting";
6
7
  listeners = new Set();
7
8
  abort;
8
9
  close;
9
- constructor(transport) {
10
+ constructor(transport, maxEntries = 300) {
10
11
  this.transport = transport;
12
+ this.maxEntries = maxEntries;
11
13
  }
12
14
  subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); }
13
15
  emit() { this.listeners.forEach(fn => fn()); }
14
16
  apply(event) {
15
17
  if (event.type === "snapshot")
16
- this.entries = event.entries;
18
+ this.entries = event.entries.slice(0, this.maxEntries);
17
19
  else if (event.type === "clear")
18
20
  this.entries = [];
19
21
  else
20
- this.entries = this.entries.some(e => e.id === event.entry.id) ? this.entries.map(e => e.id === event.entry.id ? event.entry : e) : [event.entry, ...this.entries];
22
+ this.entries = (this.entries.some(e => e.id === event.entry.id) ? this.entries.map(e => e.id === event.entry.id ? event.entry : e) : [event.entry, ...this.entries]).slice(0, this.maxEntries);
21
23
  this.emit();
22
24
  }
23
25
  async connect() {
@@ -11,8 +11,19 @@ export function createCallConsoleTransport(base = "/calls") {
11
11
  events.addEventListener(type, event => {
12
12
  try {
13
13
  const data = JSON.parse(event.data);
14
- if (data.type === type)
15
- onEvent(data);
14
+ if (data && typeof data === "object" && "type" in data) {
15
+ if (data.type === type)
16
+ onEvent(data);
17
+ }
18
+ else if (type === "call" && data && !Array.isArray(data)) {
19
+ onEvent({ type: "call", entry: data });
20
+ }
21
+ else if (type === "snapshot" && (Array.isArray(data) || "entries" in data)) {
22
+ onEvent({ type: "snapshot", entries: Array.isArray(data) ? data : data.entries });
23
+ }
24
+ else if (type === "clear") {
25
+ onEvent({ type: "clear" });
26
+ }
16
27
  }
17
28
  catch {
18
29
  onConnection("reconnecting");
@@ -28,5 +28,5 @@ export type CallEvent = {
28
28
  };
29
29
  export type CallConnection = "connecting" | "live" | "reconnecting" | "unavailable";
30
30
  export declare const callFailed: (entry: CallEntry) => boolean;
31
- export declare const callStatus: (entry: CallEntry) => string;
31
+ export declare const callStatus: (entry: CallEntry, isFailed?: (entry: CallEntry) => boolean) => string;
32
32
  export declare const formatCallHttp: (entry: CallEntry, side?: "request" | "response" | "both") => string;
@@ -1,5 +1,5 @@
1
- export const callFailed = (entry) => !entry.pending && Boolean(entry.error || entry.rpcError || (!entry.expected && (entry.status < 200 || entry.status >= 300)));
2
- export const callStatus = (entry) => entry.pending ? "Pending" : entry.rpcError ? "Tool error" : entry.error ? "Failed" : entry.expected ? "Expected" : callFailed(entry) ? "Failed" : "Done";
1
+ export const callFailed = (entry) => Boolean(entry.rpcError || (!entry.expected && (entry.error || entry.status === 0 || entry.status >= 400)));
2
+ export const callStatus = (entry, isFailed = callFailed) => entry.rpcError ? "Tool error" : entry.expected && !isFailed(entry) ? "Expected" : isFailed(entry) ? "Failed" : entry.pending ? "Pending" : "Done";
3
3
  export const formatCallHttp = (entry, side = "both") => {
4
4
  const headers = (h) => Object.entries(h).map(([k, v]) => `${k}: ${v}`).join("\n");
5
5
  const request = `${entry.method} ${entry.url}\n${headers(entry.requestHeaders)}\n\n${entry.requestBody ?? ""}`;
@@ -1,5 +1,5 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
- import { boeBrandInteractiveStyles, boeFocusRingShadow, boeNeutralInteractiveStyles, } from "../../foundations/tokens/index.js";
2
+ import { boeBrandInteractiveStyles, boeFocusRingShadow, boeNeutralInteractiveStyles, } from "../../foundations/tokens/interaction.js";
3
3
  import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
4
4
  import { boePanel } from "../../foundations/geometry/index.js";
5
5
  const DEFAULT_TAG_NAME = "box-comment-thread";
@@ -1,6 +1,6 @@
1
1
  import { BaseElement } from "../../../core/index.js";
2
2
  import { applyRovingTabindex, handleRovingKeydown } from "../../../foundations/a11y/index.js";
3
- import { boeFocusRingShadow, boeNeutralInteractiveStyles, } from "../../../foundations/tokens/index.js";
3
+ import { boeFocusRingShadow, boeNeutralInteractiveStyles, } from "../../../foundations/tokens/interaction.js";
4
4
  import { boeMotionDuration, boeMotionEasing } from "../../../foundations/motion/index.js";
5
5
  import { boePanel } from "../../../foundations/geometry/index.js";
6
6
  const DEFAULT_TAG_NAME = "box-explorer-action-menu";
@@ -1,7 +1,7 @@
1
1
  import { resolveExplorerItemGesture, shouldActivateOnClick, shouldToggleOnEnter, } from "../types.js";
2
2
  import { formatItemMetaLine, itemSummarySignature } from "./item-summary.js";
3
3
  import { BaseElement } from "../../../core/index.js";
4
- import { boeFocusVisibleStyles, boeNeutralInteractiveStyles, } from "../../../foundations/tokens/index.js";
4
+ import { boeFocusVisibleStyles, boeNeutralInteractiveStyles, } from "../../../foundations/tokens/interaction.js";
5
5
  import { boeMotionDuration, boeMotionEasing } from "../../../foundations/motion/index.js";
6
6
  import { boePanel, boeRadius } from "../../../foundations/geometry/index.js";
7
7
  const DEFAULT_TAG_NAME = "box-explorer-list";
@@ -1,7 +1,7 @@
1
1
  import { resolveExplorerItemGesture, shouldActivateOnClick, shouldToggleOnEnter, } from "../types.js";
2
2
  import { formatItemDate, formatItemOwner, formatItemShared, formatItemSize, itemSummarySignature, } from "./item-summary.js";
3
3
  import { BaseElement } from "../../../core/index.js";
4
- import { boeFocusVisibleStyles, boeNeutralInteractiveStyles, } from "../../../foundations/tokens/index.js";
4
+ import { boeFocusVisibleStyles, boeNeutralInteractiveStyles, } from "../../../foundations/tokens/interaction.js";
5
5
  import { boeMotionDuration, boeMotionEasing } from "../../../foundations/motion/index.js";
6
6
  import { boePanel, boeRadius } from "../../../foundations/geometry/index.js";
7
7
  const DEFAULT_TAG_NAME = "box-explorer-table";
@@ -1,7 +1,7 @@
1
1
  import { BaseElement } from "../../../core/index.js";
2
2
  import { SearchField, } from "../../../components/forms/search-field.js";
3
3
  import { applyRovingTabindex, handleRovingKeydown } from "../../../foundations/a11y/index.js";
4
- import { boeNeutralInteractiveStyles } from "../../../foundations/tokens/index.js";
4
+ import { boeNeutralInteractiveStyles } from "../../../foundations/tokens/interaction.js";
5
5
  import { boeMotionDuration, boeMotionEasing } from "../../../foundations/motion/index.js";
6
6
  import { boePanel } from "../../../foundations/geometry/index.js";
7
7
  const DEFAULT_TAG_NAME = "box-explorer-toolbar";
@@ -4,7 +4,7 @@ import { formatItemSize } from "../content-explorer/adapters/item-summary.js";
4
4
  import { DropZone } from "../../components/files/drop-zone.js";
5
5
  import { ProgressBar } from "../../components/feedback/progress-bar.js";
6
6
  import { BaseElement } from "../../core/index.js";
7
- import { DESIGN_SYSTEM_CHANGE_EVENT, resolveDesignIllustration, } from "../../foundations/tokens/index.js";
7
+ import { DESIGN_SYSTEM_CHANGE_EVENT, resolveDesignIllustration, } from "../../foundations/tokens/registry.js";
8
8
  import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
9
9
  import { boePanel, boeRadius } from "../../foundations/geometry/index.js";
10
10
  const DEFAULT_TAG_NAME = "box-content-uploader";
@@ -1,6 +1,6 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
2
  import { boePanel } from "../../foundations/geometry/index.js";
3
- import { boeBrandInteractiveStyles, boeFocusVisibleStyles, boeNeutralInteractiveStyles, } from "../../foundations/tokens/index.js";
3
+ import { boeBrandInteractiveStyles, boeFocusVisibleStyles, boeNeutralInteractiveStyles, } from "../../foundations/tokens/interaction.js";
4
4
  const DEFAULT_TAG_NAME = "box-file-request-builder";
5
5
  const escapeHtml = (value) => value
6
6
  .replaceAll("&", "&amp;")