@genesislcap/ai-assistant 15.85.1 → 15.86.1

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 (40) hide show
  1. package/dist/ai-assistant.api.json +65 -4
  2. package/dist/ai-assistant.d.ts +56 -2
  3. package/dist/chat-driver.cjs +2 -0
  4. package/dist/chat-driver.cjs.map +2 -2
  5. package/dist/chat-driver.mjs +2 -0
  6. package/dist/chat-driver.mjs.map +2 -2
  7. package/dist/custom-elements.json +1181 -993
  8. package/dist/dts/autopilot/autopilot-driver.d.ts +46 -2
  9. package/dist/dts/autopilot/autopilot-driver.d.ts.map +1 -1
  10. package/dist/dts/components/chat-bubble/chat-bubble.d.ts +9 -2
  11. package/dist/dts/components/chat-bubble/chat-bubble.d.ts.map +1 -1
  12. package/dist/dts/components/chat-markdown/chat-markdown.d.ts.map +1 -1
  13. package/dist/dts/genesis/write-review/write-review.d.ts +23 -0
  14. package/dist/dts/genesis/write-review/write-review.d.ts.map +1 -1
  15. package/dist/dts/genesis/write-review/write-review.styles.d.ts.map +1 -1
  16. package/dist/dts/genesis/write-review/write-review.template.d.ts.map +1 -1
  17. package/dist/dts/main/main.d.ts +8 -0
  18. package/dist/dts/main/main.d.ts.map +1 -1
  19. package/dist/dts/main/main.styles.d.ts.map +1 -1
  20. package/dist/dts/main/main.template.d.ts.map +1 -1
  21. package/dist/dts/main/main.types.d.ts +2 -0
  22. package/dist/dts/main/main.types.d.ts.map +1 -1
  23. package/dist/dts/react.d.ts +1 -0
  24. package/dist/dts/state/debug-event-log.d.ts +1 -1
  25. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  26. package/dist/esm/autopilot/autopilot-driver.js +112 -6
  27. package/dist/esm/components/chat-bubble/chat-bubble.js +18 -21
  28. package/dist/esm/components/chat-markdown/chat-markdown.js +18 -2
  29. package/dist/esm/genesis/write-review/write-review.js +48 -0
  30. package/dist/esm/genesis/write-review/write-review.styles.js +67 -1
  31. package/dist/esm/genesis/write-review/write-review.template.js +44 -27
  32. package/dist/esm/main/main.js +13 -0
  33. package/dist/esm/main/main.styles.js +42 -0
  34. package/dist/esm/main/main.template.js +11 -1
  35. package/dist/esm/main/main.types.js +2 -0
  36. package/dist/esm/state/debug-event-log.js +3 -1
  37. package/dist/react.cjs +6 -1
  38. package/dist/react.mjs +6 -1
  39. package/docs/styling.md +10 -4
  40. package/package.json +17 -17
@@ -2,6 +2,7 @@ import { __decorate } from "tslib";
2
2
  import { attr, customElement, GenesisElement, html, observable } from '@genesislcap/web-core';
3
3
  import { agenticActivityBus } from '../../channel/ai-activity-bus';
4
4
  import { autopilotNeedsUser } from '../../channel/autopilot-needs-user';
5
+ import { AiAssistantEvent } from '../../main/main.types';
5
6
  import { styles } from './chat-bubble.styles';
6
7
  import { AiChatBubbleTemplate } from './chat-bubble.template';
7
8
  /**
@@ -325,12 +326,10 @@ let AiChatBubble = class AiChatBubble extends GenesisElement {
325
326
  this.removeCloseButton();
326
327
  return;
327
328
  }
328
- if (this.injectedCloseBtn) {
329
- if (this.injectedCloseBtn.parentElement === assistant)
330
- return;
331
- this.removeCloseButton();
332
- }
333
- this.injectCloseButton(assistant);
329
+ if (this.closableAssistant === assistant)
330
+ return;
331
+ this.removeCloseButton();
332
+ this.showCloseButton(assistant);
334
333
  }
335
334
  findAssistant(nodes) {
336
335
  var _a, _b, _c, _d;
@@ -345,24 +344,22 @@ let AiChatBubble = class AiChatBubble extends GenesisElement {
345
344
  }
346
345
  return null;
347
346
  }
348
- injectCloseButton(assistant) {
349
- const prefix = this.designSystemPrefix;
350
- const btn = document.createElement(`${prefix}-button`);
351
- btn.setAttribute('slot', 'header-actions-extra');
352
- btn.setAttribute('appearance', 'stealth');
353
- btn.setAttribute('title', 'Close');
354
- const icon = document.createElement(`${prefix}-icon`);
355
- icon.setAttribute('name', 'times');
356
- btn.appendChild(icon);
357
- btn.addEventListener('click', this.onCloseBtnClick);
358
- assistant.appendChild(btn);
359
- this.injectedCloseBtn = btn;
347
+ /**
348
+ * The Close button is the assistant's own, rendered in its header beside the cog. It used to be
349
+ * a design-system button injected into the assistant's light DOM, where the assistant's styles
350
+ * could reach its host but not the padded control inside it, so it sat narrower than the cog
351
+ * and off its centre line, and every host carried a shim to square it up.
352
+ */
353
+ showCloseButton(assistant) {
354
+ assistant.toggleAttribute('closable', true);
355
+ assistant.addEventListener(AiAssistantEvent.CloseRequested, this.onCloseBtnClick);
356
+ this.closableAssistant = assistant;
360
357
  }
361
358
  removeCloseButton() {
362
359
  var _a, _b;
363
- (_a = this.injectedCloseBtn) === null || _a === void 0 ? void 0 : _a.removeEventListener('click', this.onCloseBtnClick);
364
- (_b = this.injectedCloseBtn) === null || _b === void 0 ? void 0 : _b.remove();
365
- this.injectedCloseBtn = undefined;
360
+ (_a = this.closableAssistant) === null || _a === void 0 ? void 0 : _a.removeEventListener(AiAssistantEvent.CloseRequested, this.onCloseBtnClick);
361
+ (_b = this.closableAssistant) === null || _b === void 0 ? void 0 : _b.removeAttribute('closable');
362
+ this.closableAssistant = undefined;
366
363
  }
367
364
  onBubbleMouseDown(e) {
368
365
  e.preventDefault();
@@ -23,15 +23,24 @@ const styles = css `
23
23
  padding-left: calc(var(--design-unit, 4) * 5px);
24
24
  }
25
25
 
26
+ /*
27
+ * Code paints a surface of its own, so it names the foreground that goes with it rather than
28
+ * inheriting the bubble's. A layer and neutral-foreground-rest are the pair the design system
29
+ * keeps in contrast; neutral-fill-rest is not paired with anything, and a theme that keeps it
30
+ * light in dark mode (corporate-light does, as its button fill) put dark mode's light text on
31
+ * a near-white chip. Inherited, it was also the on-accent colour inside the user's bubble.
32
+ */
26
33
  code {
27
- background-color: var(--neutral-fill-rest);
34
+ background-color: var(--neutral-layer-1);
35
+ color: var(--neutral-foreground-rest);
28
36
  padding: 2px 4px;
29
37
  border-radius: calc(var(--design-unit, 4) * 0.5px);
30
38
  font-family: monospace;
31
39
  }
32
40
 
33
41
  pre {
34
- background-color: var(--neutral-fill-rest);
42
+ background-color: var(--neutral-layer-1);
43
+ color: var(--neutral-foreground-rest);
35
44
  padding: calc(var(--design-unit, 4) * 2px);
36
45
  border-radius: calc(var(--design-unit, 4) * 1px);
37
46
  overflow-x: auto;
@@ -48,6 +57,13 @@ const styles = css `
48
57
  color: inherit;
49
58
  }
50
59
 
60
+ /* The browser's own link blue fails on a dark bubble. The user's bubble resets this to its
61
+ on-accent colour (main.styles), where an accent link would vanish into the accent fill. */
62
+ a {
63
+ color: var(--accent-foreground-rest);
64
+ text-decoration: underline;
65
+ }
66
+
51
67
  table {
52
68
  display: block;
53
69
  overflow-x: auto;
@@ -79,6 +79,37 @@ const PARKED = {
79
79
  * @beta
80
80
  */
81
81
  let GenesisWriteReview = class GenesisWriteReview extends GenesisElement {
82
+ connectedCallback() {
83
+ super.connectedCallback();
84
+ // jsdom has no ResizeObserver: there the review stays a table, which is what it renders first.
85
+ if (typeof ResizeObserver === 'undefined')
86
+ return;
87
+ this.resizeObserver = new ResizeObserver(() => this.fitLayout());
88
+ this.resizeObserver.observe(this);
89
+ }
90
+ disconnectedCallback() {
91
+ var _a;
92
+ super.disconnectedCallback();
93
+ (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
94
+ this.resizeObserver = undefined;
95
+ }
96
+ /**
97
+ * Stacks the rows when the table is wider than the review, and goes back to the table once the
98
+ * review is as wide as the table was. While stacked there is no table to measure, so the width it
99
+ * had is kept from the last time it was one.
100
+ */
101
+ fitLayout() {
102
+ var _a;
103
+ const scroller = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.scroller');
104
+ const table = scroller === null || scroller === void 0 ? void 0 : scroller.querySelector('table');
105
+ if (!scroller || !table)
106
+ return;
107
+ if (!this.stacked)
108
+ this.tableWidth = table.offsetWidth;
109
+ const stacked = this.tableWidth > scroller.clientWidth;
110
+ if (stacked !== this.stacked)
111
+ this.stacked = stacked;
112
+ }
82
113
  constructor() {
83
114
  super();
84
115
  /** Set by the interaction wrapper on every (re)mount. */
@@ -91,6 +122,20 @@ let GenesisWriteReview = class GenesisWriteReview extends GenesisElement {
91
122
  this.changeText = '';
92
123
  /** What the live region says: the outcome once the review has resolved. See `sayOutcome`. */
93
124
  this.announcement = '';
125
+ /**
126
+ * True when the table would not fit the width it has, and each row is laid out as its fields
127
+ * stacked label beside value instead.
128
+ *
129
+ * @remarks
130
+ * The table scrolls sideways rather than clip (see the styles), but in a chat bubble that left
131
+ * three of a trade's nine columns on screen, and the changing ones were behind the scrollbar.
132
+ * Measured rather than a fixed breakpoint, because whether a table fits depends on how many
133
+ * columns it has and how long their values are: a two-field insert fits a bubble a nine-field
134
+ * modify does not.
135
+ */
136
+ this.stacked = false;
137
+ /** The table's width when last laid out as a table: what the review needs to stop stacking. */
138
+ this.tableWidth = 0;
94
139
  /**
95
140
  * Set when this element sends its answer. The host marks the review resolved after that, and
96
141
  * until it did, a second click sent a second answer — a Deny straight after a Write included.
@@ -508,6 +553,9 @@ __decorate([
508
553
  __decorate([
509
554
  observable
510
555
  ], GenesisWriteReview.prototype, "announcement", void 0);
556
+ __decorate([
557
+ observable
558
+ ], GenesisWriteReview.prototype, "stacked", void 0);
511
559
  __decorate([
512
560
  observable
513
561
  ], GenesisWriteReview.prototype, "answered", void 0);
@@ -72,6 +72,70 @@ export const WriteReviewStyles = css `
72
72
  max-width: 40ch;
73
73
  }
74
74
 
75
+ /*
76
+ * Stacked: the table did not fit (see GenesisWriteReview.stacked), so each row becomes a block
77
+ * and each field a line of its own, its label beside its value, and nothing is off to the side.
78
+ * The headings go, as every value now carries its own; the select-all box stays, labelled.
79
+ */
80
+ .stacked table,
81
+ .stacked tbody,
82
+ .stacked tr,
83
+ .stacked td {
84
+ display: block;
85
+ }
86
+
87
+ .stacked thead tr {
88
+ display: flex;
89
+ }
90
+
91
+ .stacked thead th:not(.select) {
92
+ display: none;
93
+ }
94
+
95
+ .stacked thead th.select {
96
+ border-bottom: none;
97
+ font-weight: 400;
98
+ }
99
+
100
+ /* Without its column of row boxes beneath it, a lone box at the top says nothing on sight. Its
101
+ spoken name is its aria-label, so the words here are for the eye only (the empty alt). */
102
+ .stacked thead th.select::after {
103
+ content: 'Select all' / '';
104
+ margin-inline-start: calc(var(--design-unit, 4) * 1.5px);
105
+ vertical-align: middle;
106
+ }
107
+
108
+ .stacked tbody tr {
109
+ padding-block: calc(var(--design-unit, 4) * 1px);
110
+ border-top: calc(var(--stroke-width, 1) * 1px) solid var(--neutral-stroke-rest);
111
+ }
112
+
113
+ .stacked td {
114
+ border-bottom: none;
115
+ }
116
+
117
+ .stacked td[data-label] {
118
+ display: grid;
119
+ grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
120
+ gap: calc(var(--design-unit, 4) * 2px);
121
+ white-space: normal;
122
+ overflow-wrap: anywhere;
123
+ }
124
+
125
+ .stacked td[data-label]::before {
126
+ content: attr(data-label);
127
+ font-weight: 600;
128
+ }
129
+
130
+ /* No column to bound any more: a long value takes the line's width like any other, and wraps
131
+ within it like any other. */
132
+ .stacked td.long {
133
+ min-width: 0;
134
+ max-width: none;
135
+ white-space: normal;
136
+ overflow-wrap: anywhere;
137
+ }
138
+
75
139
  /*
76
140
  * The two marks this table carries — "this field is changing" and "this field decides which
77
141
  * record disappears" — were weight, colour and a strike-through only. None of that reaches
@@ -197,7 +261,7 @@ export const WriteReviewStyles = css `
197
261
  white-space: nowrap;
198
262
  }
199
263
 
200
- td.select rapid-checkbox,
264
+ .select rapid-checkbox,
201
265
  td.select .mark {
202
266
  vertical-align: middle;
203
267
  }
@@ -223,8 +287,10 @@ export const WriteReviewStyles = css `
223
287
  white-space: normal;
224
288
  }
225
289
 
290
+ /* Wraps, so in a narrow bubble the last button drops to a new line rather than off the edge. */
226
291
  .actions {
227
292
  display: flex;
293
+ flex-wrap: wrap;
228
294
  gap: calc(var(--design-unit, 4) * 2px);
229
295
  margin-top: calc(var(--design-unit, 4) * 2px);
230
296
  }
@@ -79,34 +79,46 @@ const valueOrBlank = (pick) => html `
79
79
  </span>
80
80
  `)}
81
81
  `;
82
- /** A cell shows what the field becomes, and — when it is changing — what it is now. */
82
+ /**
83
+ * A cell shows what the field becomes, and — when it is changing — what it is now.
84
+ *
85
+ * @remarks
86
+ * The cell carries its column's label, and its content sits in one wrapper, for the stacked
87
+ * layout: there the headings are gone and each cell is a label beside its value (see
88
+ * `GenesisWriteReview.stacked`), and the wrapper keeps the − and + lines together in the value's
89
+ * column rather than letting each take a grid cell of its own.
90
+ */
83
91
  const cell = html `
84
92
  <td
93
+ role="cell"
94
+ data-label="${(x) => x.label}"
85
95
  class="${(x) => [x.changed ? 'changed' : '', x.identifying ? 'identifying' : '', x.long ? 'long' : '']
86
96
  .join(' ')
87
97
  .trim()}"
88
98
  title="${(x) => { var _a; return x.identifying ? `This field identifies ${(_a = x.identifies) !== null && _a !== void 0 ? _a : 'the row being removed'}` : ''; }}"
89
99
  >
90
- ${when((x) => x.identifying, html `
91
- <span class="sr-only">identifies this row:</span>
92
- `)}
93
- ${when((x) => x.changed && x.previousDisplay !== undefined, html `
94
- <span class="sr-only">was</span>
95
- <span class="was">${mark('−')}${valueOrBlank((x) => x.previousDisplay)}</span>
96
- <span class="sr-only">now</span>
97
- <span class="now">${mark('+')}${valueOrBlank((x) => x.display)}</span>
98
- `)}
99
- ${when((x) => !!x.input, html `
100
- <span class="input-mark">input:</span>
101
- `)}
102
- ${when((x) => !(x.changed && x.previousDisplay !== undefined), html `
103
- ${(x) => x.display}
104
- `)}
100
+ <div class="value">
101
+ ${when((x) => x.identifying, html `
102
+ <span class="sr-only">identifies this row:</span>
103
+ `)}
104
+ ${when((x) => x.changed && x.previousDisplay !== undefined, html `
105
+ <span class="sr-only">was</span>
106
+ <span class="was">${mark('−')}${valueOrBlank((x) => x.previousDisplay)}</span>
107
+ <span class="sr-only">now</span>
108
+ <span class="now">${mark('+')}${valueOrBlank((x) => x.display)}</span>
109
+ `)}
110
+ ${when((x) => !!x.input, html `
111
+ <span class="input-mark">input:</span>
112
+ `)}
113
+ ${when((x) => !(x.changed && x.previousDisplay !== undefined), html `
114
+ ${(x) => x.display}
115
+ `)}
116
+ </div>
105
117
  </td>
106
118
  `;
107
119
  const row = html `
108
- <tr class="${(x, c) => rowClass(x, c.parent)}">
109
- <td class="select">
120
+ <tr role="row" class="${(x, c) => rowClass(x, c.parent)}">
121
+ <td role="cell" class="select">
110
122
  <rapid-checkbox
111
123
  aria-label="Select ${(x) => x.label}"
112
124
  aria-describedby="${(x) => { var _a; return (((_a = x.problems) === null || _a === void 0 ? void 0 : _a.length) ? `problems-${x.index}` : ''); }}"
@@ -130,9 +142,9 @@ const row = html `
130
142
  ${repeat((x) => x.cells, cell)}
131
143
  </tr>
132
144
  ${when((x) => { var _a; return !!((_a = x.problems) === null || _a === void 0 ? void 0 : _a.length); }, html `
133
- <tr class="refused">
134
- <td></td>
135
- <td class="problems" colspan="99" id="problems-${(x) => String(x.index)}">
145
+ <tr role="row" class="refused">
146
+ <td role="cell"></td>
147
+ <td role="cell" class="problems" colspan="99" id="problems-${(x) => String(x.index)}">
136
148
  ${(x) => { var _a; return (_a = x.problems) === null || _a === void 0 ? void 0 : _a.join('; '); }}
137
149
  </td>
138
150
  </tr>
@@ -157,8 +169,13 @@ export const WriteReviewTemplate = html `
157
169
  ${when((x) => !!x.customCodeLine, html `
158
170
  <p class="custom-code" id="${CUSTOM_CODE_LINE_ID}">${(x) => x.customCodeLine}</p>
159
171
  `)}
160
- <div class="scroller">
172
+ ${
173
+ /* The table's roles are stated, not left to the tags: the stacked layout sets display: block
174
+ on its parts, and a browser then drops their table semantics, so a screen reader would
175
+ hear a run of values with no row or column to put them in. */ ''}
176
+ <div class="scroller ${(x) => (x.stacked ? 'stacked' : '')}">
161
177
  <table
178
+ role="table"
162
179
  aria-label="${(x) => x.tableLabel}"
163
180
  aria-describedby="${(x) => x.customCodeLine
164
181
  ? CUSTOM_CODE_LINE_ID
@@ -166,9 +183,9 @@ export const WriteReviewTemplate = html `
166
183
  ? ROW_ACTION_LINES_ID
167
184
  : undefined}"
168
185
  >
169
- <thead>
170
- <tr>
171
- <th>
186
+ <thead role="rowgroup">
187
+ <tr role="row">
188
+ <th role="columnheader" class="select">
172
189
  <rapid-checkbox
173
190
  aria-label="Select all rows"
174
191
  :checked="${(x) => x.allSelected}"
@@ -177,11 +194,11 @@ export const WriteReviewTemplate = html `
177
194
  ></rapid-checkbox>
178
195
  </th>
179
196
  ${repeat((x) => { var _a, _b; return (_b = (_a = x.model) === null || _a === void 0 ? void 0 : _a.columns) !== null && _b !== void 0 ? _b : []; }, html `
180
- <th>${(c) => c.label}</th>
197
+ <th role="columnheader">${(c) => c.label}</th>
181
198
  `)}
182
199
  </tr>
183
200
  </thead>
184
- <tbody>${repeat((x) => { var _a, _b; return (_b = (_a = x.model) === null || _a === void 0 ? void 0 : _a.rows) !== null && _b !== void 0 ? _b : []; }, row)}</tbody>
201
+ <tbody role="rowgroup">${repeat((x) => { var _a, _b; return (_b = (_a = x.model) === null || _a === void 0 ? void 0 : _a.rows) !== null && _b !== void 0 ? _b : []; }, row)}</tbody>
185
202
  </table>
186
203
  </div>
187
204
  <div class="actions">
@@ -203,6 +203,7 @@ avoidTreeShaking(AiChatMarkdown, AiChatInteractionWrapper, AiHaloOverlay, AiWave
203
203
  * {@link FoundationAiAssistant.finalizeCostSession} to flush early; it is idempotent.
204
204
  *
205
205
  * @fires chat-header-mousedown - Fired when the user presses the chat header in `popout-mode="expand"` (for drag-to-popout hosts). Bubbles and composed. detail: `ChatHeaderMouseDownDetail`
206
+ * @fires close-requested - Fired when the user presses the header's Close button, which shows while `closable` is set (the chat bubble sets it while the assistant is in its dialog). Bubbles and composed.
206
207
  * @fires session-cleared - Fired after the session is wiped (the lifecycle menu's Clear). The payload is captured before teardown so the host can clean up any external state the active agent was working on. Bubbles and composed. detail: `SessionClearedDetail`
207
208
  * @fires handoff-started - Fired when an agent's `handOff` takes effect and its target becomes active. Bubbles and composed. detail: `HandOffFrame`
208
209
  * @fires handoff-returned - Fired when a hand-off ends and its caller is active again; `detail.outcome` says how it ended. Bubbles and composed. detail: `HandOffFrame`
@@ -324,6 +325,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
324
325
  /** Sanitized `userIcon`, or the default person mark; bound into the template. @internal */
325
326
  this.userIconSafe = userIconSvg;
326
327
  this.placeholder = 'Message assistant...';
328
+ /**
329
+ * Shows a Close button in the header, which fires `close-requested` and leaves the closing to
330
+ * whatever holds the assistant. The chat bubble sets it while the assistant is in its dialog.
331
+ */
332
+ this.closable = false;
327
333
  this.chatConfig = {};
328
334
  /**
329
335
  * Optional session persistence (GENC-1351). Off by default — set `enabled`
@@ -4918,6 +4924,10 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
4918
4924
  return;
4919
4925
  agenticActivityBus.publish('agent-attention', { sessionKey, kind: 'turn-ended' });
4920
4926
  }
4927
+ /** Called when the user clicks the header's Close button. */
4928
+ handleClose() {
4929
+ this.dispatchEvent(new CustomEvent(AiAssistantEvent.CloseRequested, { bubbles: true, composed: true }));
4930
+ }
4921
4931
  onChatHeaderMouseDown(e) {
4922
4932
  if (this.popoutMode === 'collapse')
4923
4933
  return;
@@ -5060,6 +5070,9 @@ __decorate([
5060
5070
  __decorate([
5061
5071
  attr({ attribute: 'popout-mode' })
5062
5072
  ], FoundationAiAssistant.prototype, "popoutMode", void 0);
5073
+ __decorate([
5074
+ attr({ mode: 'boolean' })
5075
+ ], FoundationAiAssistant.prototype, "closable", void 0);
5063
5076
  __decorate([
5064
5077
  observable
5065
5078
  ], FoundationAiAssistant.prototype, "agents", void 0);
@@ -70,9 +70,45 @@ const baseStyles = css `
70
70
  margin-left: auto;
71
71
  }
72
72
 
73
+ /* Square, as wide as the control is tall, with the glyph on both centre lines. The
74
+ design-system control pads its sides and aligns its content to the baseline, and the icon
75
+ sits inline on a text line, so each button came out as wide as its glyph plus that padding
76
+ (the X narrower than the cog) and a little above centre, more so with a tall-ascender font.
77
+ An app's ::part() rules still win. */
78
+ .close-button,
73
79
  .settings-button,
74
80
  .popout-button {
75
81
  font-size: 1.2em;
82
+ min-width: 0;
83
+ }
84
+
85
+ .close-button::part(control),
86
+ .settings-button::part(control),
87
+ .popout-button::part(control) {
88
+ flex-grow: 0;
89
+ width: auto;
90
+ aspect-ratio: 1;
91
+ min-width: 0;
92
+ padding: 0;
93
+ align-items: center;
94
+ justify-content: center;
95
+ }
96
+
97
+ .close-button::part(content),
98
+ .settings-button::part(content),
99
+ .popout-button::part(content) {
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ line-height: 1;
104
+ }
105
+
106
+ /* The glyph's own wrapper, so the icon font's baseline nudge (vertical-align: -0.125em) has no
107
+ line to act on. */
108
+ .close-button > ::part(i-icon),
109
+ .settings-button > ::part(i-icon),
110
+ .popout-button > ::part(i-icon) {
111
+ display: flex;
76
112
  }
77
113
 
78
114
  ::slotted([slot='header-actions-extra']) {
@@ -327,6 +363,12 @@ const baseStyles = css `
327
363
  box-shadow: 0 2px 8px color-mix(in srgb, var(--accent-fill-rest) 35%, transparent);
328
364
  }
329
365
 
366
+ /* On the accent fill an accent-coloured link disappears; it takes the bubble's own colour and
367
+ keeps its underline. */
368
+ .message.user ai-chat-markdown::part(md-link) {
369
+ color: inherit;
370
+ }
371
+
330
372
  .message.ai {
331
373
  background: linear-gradient(135deg, var(--neutral-layer-3) 0%, var(--neutral-layer-4) 100%);
332
374
  color: var(--neutral-foreground-rest);
@@ -1038,7 +1038,7 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
1038
1038
  <span class="chat-title" part="chat-title">${(x) => x.headerTitle}</span>
1039
1039
  `)}
1040
1040
  ${
1041
- /* The two buttons below re-publish the design-system button's inner parts (control,
1041
+ /* The buttons below re-publish the design-system button's inner parts (control,
1042
1042
  content) into THIS component's part namespace via exportparts.
1043
1043
 
1044
1044
  Without it they are unreachable by an embedding app: ::part() crosses exactly one
@@ -1056,6 +1056,16 @@ ${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void
1056
1056
  parts are exported, and so control does not collide across buttons. */ ''}
1057
1057
  <div class="header-actions" part="header-actions">
1058
1058
  <slot name="header-actions-extra"></slot>
1059
+ ${when((x) => x.closable, html `
1060
+ <${buttonTag}
1061
+ class="close-button"
1062
+ part="close-button"
1063
+ exportparts="control: close-button-control, content: close-button-content"
1064
+ appearance="stealth"
1065
+ title="Close"
1066
+ @click=${(x) => x.handleClose()}
1067
+ ><${iconTag} name="times"></${iconTag}></${buttonTag}>
1068
+ `)}
1059
1069
  ${when((x) => !!x.popoutMode, html `
1060
1070
  <${buttonTag}
1061
1071
  class="popout-button"
@@ -30,6 +30,8 @@ export const AiAssistantEvent = {
30
30
  HandOffReturned: 'handoff-returned',
31
31
  /** A hand-off ended with no hand-back. `detail`: `HandOffFrame`, with `outcome`. */
32
32
  HandOffUnwound: 'handoff-unwound',
33
+ /** The header's Close button pressed, shown while `closable` is set. No `detail`. */
34
+ CloseRequested: 'close-requested',
33
35
  };
34
36
  /**
35
37
  * Registry of all available animations with their display metadata.
@@ -95,6 +95,8 @@ export const META_EVENT_IMPORTANCE = {
95
95
  'autopilot.handed-back': 'normal',
96
96
  'autopilot.paused': 'normal',
97
97
  'autopilot.resumed': 'normal',
98
+ // The mode went with the task across a hand-off or its hand-back, rather than ending with the flow.
99
+ 'autopilot.carried': 'normal',
98
100
  // The wrapper itself failed: the journey stops being automatic without anyone choosing that.
99
101
  'autopilot.error': 'high',
100
102
  // A written answer counted against the step's limit; reaching it withdraws `reply`.
@@ -262,7 +264,7 @@ export const DEBUG_LOG_README = [
262
264
  "kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
263
265
  "kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
264
266
  "Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, subagent.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, retries, handoffs, agent/provider changes, interactions, sub-agent start/complete), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated, context.condensed). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. A 'high' turn.error is often preceded by one or more 'normal' turn.retry events for the same reason — read them together to see how many attempts were made before bailing. 'message' and 'turn' entries carry no importance — they are the substance, always read them.",
265
- 'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated/refusal/budget-exhausted/provider-refused/context-exhausted/ai-service-error, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, for budget-exhausted the budgetUsd + spentUsd figures reported by the proxy plus the resolved vendor, for provider-refused the kind (spend|auth) plus the upstream status and error type — the vendor refusing the ACCOUNT rather than us refusing to spend, so the remedy is a top-up or a key rotation rather than a raised cap, and the kind is the only place that distinction survives because the user-facing copy is deliberately cause-free, for context-exhausted the estimated contextTokens against the guard that stopped the turn plus the iterations reached — the driver ending a runaway tool loop at the edge of the context window rather than issuing a request the provider would reject, so the transcript survives intact and can still be compacted, for ai-service-error the code (one of AI_SERVICE_ERROR_CODES, e.g. NO_API_KEY) and the status the AI service answered with — its own text goes to the browser console only, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated/refusal/budget_exhausted/provider_refused/context_exhausted/ai_service_error; budget_exhausted and provider_refused are terminal for the PARENT turn too, and so is ai_service_error when the refusal is about the service rather than the request and came from the vendor the parent calls — the parent stops rather than calling the model again into the same wall; after context_exhausted the parent goes on, but its own next call is likely to stop on the same guard), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), routing.decided (ONE PER ROUTING DECISION — see its own paragraph below), provider.selected (the resolved provider for the upcoming turns — detail.provider is the registry SLOT/tier name, detail.model the concrete model behind it and detail.vendor its vendor; emitted only when the slot CHANGES, so read the per-turn `model` for the model of any given call rather than assuming the nearest event still applies), interaction.requested/resolved (blocking user widgets — explain quiet gaps; note that when a sub-agent opens a widget, detail.agent — and the agentName on the interaction message — is the HOST agent that owns the widget, NOT the sub-agent that asked, because widgets render and resolve on the host driver; detail.ownerPath names the agents that actually asked, outermost first, the last being the one whose tool requested it, and detail.heldBy says who held it when it opened: user, or the id of the interceptor that claimed it, such as autopilot; on interaction.resolved, detail.answeredBy is who answered — the claimant id when it settled through its own hold, user for any other answer (a click counts as the person even on a widget a claimant held) — absent when it closed unanswered (a timeout or a stop), and absent too when it settled by itself other than through the hold of a claimant — a widget registered resolvesItself (one that does its work and settles on its own, asking the person nothing, such as a generation run), or one outcome its controller said settled itself (ctx.resolve with resolvedItself, such as a generation phase that failed outright before its review gate ever asked anything) — which then carries detail.resolvedItself: true instead, as the interaction message does — nobody answered it, and the platform cannot tell it finishing from the person interrupting it (a Skip), so only the result of the widget itself can say which; the same value is stamped on the interaction message as answeredBy and on the result the asking tool handler receives (absent there in the same cases), and when it is not user the tool result that interaction returned carries a decidedOnUsersBehalf record, so the model was told the choice was not made by the person; detail.edits, present only when non-zero, counts the editing actions a claimant ran on the widget before it was settled, whoever settled it, and those edits are stamped on the interaction message as edits, each with by, action, choice and args when it had some, in the order they ran, while a decidedOnUsersBehalf record carries their choice texts as edits, and when the person settled it after such edits the tool result carries an editedOnUsersBehalf record instead — the framing and those choice texts — so the model knows the widget was changed on the user behalf before the user answered, and that the user then confirmed it by finishing it), interaction.held (a claimant moved a pending interaction to the person or back to itself — detail.heldBy is the new holder, detail.claimant the interceptor; detail.late: true is an interceptor that declined it when it opened claiming it afterwards, such as the autopilot resumed while it was open; a widget held by anything but user was not answered by the person), interaction.claim-refused (a claim on a userOnly interaction, which stays with the person — detail.claimant + reason), autopilot.* (the autopilot, a decorator that answers interactions on behalf of the user — autopilot.claimed / autopilot.declined say whether it took each interaction and, for a decline, detail.reason: paused, other-flow, no-autopilot, unavailable, userOnly, policy or no-controller; one declined as paused that is still open when the person resumes the autopilot is looked at again, and the autopilot.claimed or autopilot.declined recorded for it then carries detail.onResume: true (a decline then can also be claimed-elsewhere, another interceptor having claimed it first); a restored mode looks at nothing; autopilot.decision-sent is what it was sent (detail.chars; detail.nothingEnabled: true when every action on the widget was unavailable — a gate whose controller said it was asking, such as a failed build the autopilot may not cancel — so it could only hand it back, which it may then do even after the person asked it to decide it itself; a widget whose controller says it is working (ctx.setWorking) is never sent a decision until it says it is asking, even when an action is enabled or the person resumes the autopilot); autopilot.acted is what it did (detail.action, detail.note, detail.finished); autopilot.handed-back is a decision it gave to the person (detail.reason and detail.trigger: model, no-decision or error); autopilot.paused / autopilot.resumed are mode changes (detail.scope, detail.trigger, detail.decideYourself when the person resumed during a hand-back; detail.trigger is restored when the mode came back with the session after a reload, applied once its flow re-activated, which starts nothing by itself); autopilot.error is the wrapper itself failing; autopilot.refinement counts a refining action against its step — a written answer (reply) or a widget action declared refines, such as a regenerate with instructions, counted together (detail.action, detail.stepKey, detail.count, detail.limitReached, after which every refining action is withdrawn for that step: not listed, and refused; the decision and every relisting of its actions say how many are left at the step, as a line Refinements left at this step: N, wherever the widget has a refining action); autopilot.handback-presented is the turn-level hand-back, shown when a working turn ended needing the person with no widget to hand over (detail.why: max-iterations, context-exhausted, failure or question; detail.options; detail.choice once answered); autopilot.catch-up is the autopilot being given the whole visible transcript at once (detail.why: first, for its first decision in a flow or after a reload; size, when its own history passed its budget and was rebuilt short — detail.ownChars; or compacted, when a compaction removed the last message it had seen, so it reads the summary and everything since, short; in each, what it had sent on the user behalf is included and labelled as its own, since its own history does not hold it); autopilot.cost-dropped is autopilot spend that no message carries, so it is NOT in the session total (detail.costUsd and detail.why: handed-back, aborted or stale for a written question it answered that no reply was sent for — stale when a pause, a resume, a new turn or the end of its flow came before the answer landed, which then went nowhere — or no-recorder when the driver had no way to book a decision to its interaction); a hand-back trigger can also be budget (the decision passed its spend cap), watchdog (its work went quiet: no ready surface and no heartbeat, or its own decision stalled with no model call landing, which stops that decision) or detached (the autopilot was removed while it held the interaction, which went to the person with no reason shown). To debug one autopilot decision, follow its interactionId: interaction.requested (heldBy), autopilot.claimed, autopilot.decision-sent, autopilot.acted or autopilot.handed-back, interaction.invoked, interaction.resolved (answeredBy), then the next turn of the working agent), interaction.invoked (a claimant ran one of the actions the widget itself exposes, through its hold, the same code the button runs — detail.claimant, detail.action (an option id, select or reply) and detail.finished, whether that settled the interaction, and when it did not, the run is recorded as an edit; the choice and narration it recorded reach the result of the tool that asked as decidedOnUsersBehalf), interaction.step (a long action run as named steps through runSteps, which survive a reload — detail.step and detail.outcome: started, done, skipped when the step was already done before a reload, or resumed when it started before a reload and runs again with what it saved part-way, such as a job id it re-attaches to), interaction.checkpoint (the checkpoints a conversation keeps for its long actions: the one in flight, and each one a reload cut, held apart until a request of its own widget type takes it over, so a resumed flow that opens other long widgets first, which save their own, loses nothing — detail.action: resumed when a reload cut the work and a request of the same widget type that the registration sameWork agrees is the same work took it over, refused when sameWork said a new request is different work (detail.saved is the identity it kept; detail.kept: true, since it stays for a later request of that type, and is offered to every one until one agrees) or when runSteps found the handed-over checkpoint malformed and started fresh (detail.reason, detail.saved), or cleared when its flow ended, once per checkpoint, with detail.cut: true for one a reload cut that nothing took over), context.updated/threshold-crossed (token + cost), context.condensed (a stale tool payload was collapsed out of the model-bound history by a `condenseWhen` declaration on the tool — detail.tool + toolCallId, target args|response, trigger (superseded:<key> or age:<n>), stubLen, and an estimated tokensSaved; stored history and this log keep the FULL payload, so the model-visible slice at any point is the full history minus the condensations recorded up to then), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
267
+ 'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated/refusal/budget-exhausted/provider-refused/context-exhausted/ai-service-error, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, for budget-exhausted the budgetUsd + spentUsd figures reported by the proxy plus the resolved vendor, for provider-refused the kind (spend|auth) plus the upstream status and error type — the vendor refusing the ACCOUNT rather than us refusing to spend, so the remedy is a top-up or a key rotation rather than a raised cap, and the kind is the only place that distinction survives because the user-facing copy is deliberately cause-free, for context-exhausted the estimated contextTokens against the guard that stopped the turn plus the iterations reached — the driver ending a runaway tool loop at the edge of the context window rather than issuing a request the provider would reject, so the transcript survives intact and can still be compacted, for ai-service-error the code (one of AI_SERVICE_ERROR_CODES, e.g. NO_API_KEY) and the status the AI service answered with — its own text goes to the browser console only, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated/refusal/budget_exhausted/provider_refused/context_exhausted/ai_service_error; budget_exhausted and provider_refused are terminal for the PARENT turn too, and so is ai_service_error when the refusal is about the service rather than the request and came from the vendor the parent calls — the parent stops rather than calling the model again into the same wall; after context_exhausted the parent goes on, but its own next call is likely to stop on the same guard), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), routing.decided (ONE PER ROUTING DECISION — see its own paragraph below), provider.selected (the resolved provider for the upcoming turns — detail.provider is the registry SLOT/tier name, detail.model the concrete model behind it and detail.vendor its vendor; emitted only when the slot CHANGES, so read the per-turn `model` for the model of any given call rather than assuming the nearest event still applies), interaction.requested/resolved (blocking user widgets — explain quiet gaps; note that when a sub-agent opens a widget, detail.agent — and the agentName on the interaction message — is the HOST agent that owns the widget, NOT the sub-agent that asked, because widgets render and resolve on the host driver; detail.ownerPath names the agents that actually asked, outermost first, the last being the one whose tool requested it, and detail.heldBy says who held it when it opened: user, or the id of the interceptor that claimed it, such as autopilot; on interaction.resolved, detail.answeredBy is who answered — the claimant id when it settled through its own hold, user for any other answer (a click counts as the person even on a widget a claimant held) — absent when it closed unanswered (a timeout or a stop), and absent too when it settled by itself other than through the hold of a claimant — a widget registered resolvesItself (one that does its work and settles on its own, asking the person nothing, such as a generation run), or one outcome its controller said settled itself (ctx.resolve with resolvedItself, such as a generation phase that failed outright before its review gate ever asked anything) — which then carries detail.resolvedItself: true instead, as the interaction message does — nobody answered it, and the platform cannot tell it finishing from the person interrupting it (a Skip), so only the result of the widget itself can say which; the same value is stamped on the interaction message as answeredBy and on the result the asking tool handler receives (absent there in the same cases), and when it is not user the tool result that interaction returned carries a decidedOnUsersBehalf record, so the model was told the choice was not made by the person; detail.edits, present only when non-zero, counts the editing actions a claimant ran on the widget before it was settled, whoever settled it, and those edits are stamped on the interaction message as edits, each with by, action, choice and args when it had some, in the order they ran, while a decidedOnUsersBehalf record carries their choice texts as edits, and when the person settled it after such edits the tool result carries an editedOnUsersBehalf record instead — the framing and those choice texts — so the model knows the widget was changed on the user behalf before the user answered, and that the user then confirmed it by finishing it), interaction.held (a claimant moved a pending interaction to the person or back to itself — detail.heldBy is the new holder, detail.claimant the interceptor; detail.late: true is an interceptor that declined it when it opened claiming it afterwards, such as the autopilot resumed while it was open; a widget held by anything but user was not answered by the person), interaction.claim-refused (a claim on a userOnly interaction, which stays with the person — detail.claimant + reason), autopilot.* (the autopilot, a decorator that answers interactions on behalf of the user — autopilot.claimed / autopilot.declined say whether it took each interaction and, for a decline, detail.reason: paused, other-flow, no-autopilot, unavailable, userOnly, policy or no-controller; one declined as paused that is still open when the person resumes the autopilot is looked at again, and the autopilot.claimed or autopilot.declined recorded for it then carries detail.onResume: true (a decline then can also be claimed-elsewhere, another interceptor having claimed it first); a restored mode looks at nothing; autopilot.decision-sent is what it was sent (detail.chars; detail.nothingEnabled: true when every action on the widget was unavailable — a gate whose controller said it was asking, such as a failed build the autopilot may not cancel — so it could only hand it back, which it may then do even after the person asked it to decide it itself; a widget whose controller says it is working (ctx.setWorking) is never sent a decision until it says it is asking, even when an action is enabled or the person resumes the autopilot); autopilot.acted is what it did (detail.action, detail.note, detail.finished); autopilot.handed-back is a decision it gave to the person (detail.reason and detail.trigger: model, no-decision or error); autopilot.paused / autopilot.resumed are mode changes (detail.scope, detail.trigger, detail.decideYourself when the person resumed during a hand-back; detail.trigger is restored when the mode came back with the session after a reload, applied once its flow re-activated, which starts nothing by itself); autopilot.carried is the mode, running or paused, going with the task to the agent that took it over, rather than ending with the flow as on any other change of flow owner: to the target of a hand-off, or back to its caller on the hand-back (detail.from, detail.to, detail.mode); a pause the person chose goes with it, and Stop and Clear still end it; a mode restored for the target of a hand-off applies to its caller when the reload hands back; autopilot.error is the wrapper itself failing; autopilot.refinement counts a refining action against its step — a written answer (reply) or a widget action declared refines, such as a regenerate with instructions, counted together (detail.action, detail.stepKey, detail.count, detail.limitReached, after which every refining action is withdrawn for that step: not listed, and refused; the decision and every relisting of its actions say how many are left at the step, as a line Refinements left at this step: N, wherever the widget has a refining action); autopilot.handback-presented is the turn-level hand-back, shown when a working turn ended needing the person with no widget to hand over (detail.why: max-iterations, context-exhausted, failure or question; detail.options; detail.choice once answered); autopilot.catch-up is the autopilot being given the whole visible transcript at once (detail.why: first, for its first decision in a flow or after a reload; size, when its own history passed its budget and was rebuilt short — detail.ownChars; or compacted, when a compaction removed the last message it had seen, so it reads the summary and everything since, short; in each, what it had sent on the user behalf is included and labelled as its own, since its own history does not hold it); autopilot.cost-dropped is autopilot spend that no message carries, so it is NOT in the session total (detail.costUsd and detail.why: handed-back, aborted or stale for a written question it answered that no reply was sent for — stale when a pause, a resume, a new turn or the end of its flow came before the answer landed, which then went nowhere — or no-recorder when the driver had no way to book a decision to its interaction); a hand-back trigger can also be budget (the decision passed its spend cap), watchdog (its work went quiet: no ready surface and no heartbeat, or its own decision stalled with no model call landing, which stops that decision) or detached (the autopilot was removed while it held the interaction, which went to the person with no reason shown). To debug one autopilot decision, follow its interactionId: interaction.requested (heldBy), autopilot.claimed, autopilot.decision-sent, autopilot.acted or autopilot.handed-back, interaction.invoked, interaction.resolved (answeredBy), then the next turn of the working agent), interaction.invoked (a claimant ran one of the actions the widget itself exposes, through its hold, the same code the button runs — detail.claimant, detail.action (an option id, select or reply) and detail.finished, whether that settled the interaction, and when it did not, the run is recorded as an edit; the choice and narration it recorded reach the result of the tool that asked as decidedOnUsersBehalf), interaction.step (a long action run as named steps through runSteps, which survive a reload — detail.step and detail.outcome: started, done, skipped when the step was already done before a reload, or resumed when it started before a reload and runs again with what it saved part-way, such as a job id it re-attaches to), interaction.checkpoint (the checkpoints a conversation keeps for its long actions: the one in flight, and each one a reload cut, held apart until a request of its own widget type takes it over, so a resumed flow that opens other long widgets first, which save their own, loses nothing — detail.action: resumed when a reload cut the work and a request of the same widget type that the registration sameWork agrees is the same work took it over, refused when sameWork said a new request is different work (detail.saved is the identity it kept; detail.kept: true, since it stays for a later request of that type, and is offered to every one until one agrees) or when runSteps found the handed-over checkpoint malformed and started fresh (detail.reason, detail.saved), or cleared when its flow ended, once per checkpoint, with detail.cut: true for one a reload cut that nothing took over), context.updated/threshold-crossed (token + cost), context.condensed (a stale tool payload was collapsed out of the model-bound history by a `condenseWhen` declaration on the tool — detail.tool + toolCallId, target args|response, trigger (superseded:<key> or age:<n>), stubLen, and an estimated tokensSaved; stored history and this log keep the FULL payload, so the model-visible slice at any point is the full history minus the condensations recorded up to then), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
266
268
  'Sub-agent meta events: a sub-agent\'s own turn.retry/turn.error/tool.failed/tool.unresolved events are merged into this same timeline, tagged with `detail.subAgent` — a `"<parent> › <sub-agent>"` breadcrumb that composes when nested (e.g. `"UI Builder › Planner › Grounding"`) — and interleaved by their original timestamps within the subagent.started→completed/failed bracket. These are the per-attempt/per-failure signals that do NOT appear among the sub-agent\'s (hoisted) messages: a malformed/empty attempt that gets retried produces no message, and the stale-vs-hallucinated split and streak counts live only on the event. A sub-agent\'s high-volume, message-derivable events (turn.start/turn.end, provider.selected, context.updated) are intentionally NOT merged — read its hoisted messages for model/tokens/cost and turn-by-turn activity, and the bracketing subagent.* events for the run\'s span.',
267
269
  "`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, contextTokens/contextLimit/contextUsagePercent for the last call, and the session totals — sessionCostUsd, sessionTokensConsumed, and the four-bucket sessionUsage lifted to the top of this log), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export — reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.",
268
270
  'Note the two different scopes in `meta.context`: `contextTokens` is the prompt size of the LAST call (against `contextLimit`, the model context window), while `sessionUsage`/`sessionTokensConsumed` are cumulative BILLED throughput. Every turn resends the conversation, so the cumulative figure counts each turn’s prompt again in the next turn’s and is expected to dwarf the context size — that is not double-counting.',
package/dist/react.cjs CHANGED
@@ -53,12 +53,14 @@ const AiWavesIndicator = React.forwardRef(function AiWavesIndicator(props, ref)
53
53
  });
54
54
 
55
55
  const FoundationAiAssistant = React.forwardRef(function FoundationAiAssistant(props, ref) {
56
- const { onBudgetRefreshRequested, onChatHeaderMousedown, onSessionCleared, onHandoffStarted, onHandoffReturned, onHandoffUnwound, children, ...rest } = props;
56
+ const { onBudgetRefreshRequested, onChatHeaderMousedown, onCloseRequested, onSessionCleared, onHandoffStarted, onHandoffReturned, onHandoffUnwound, children, ...rest } = props;
57
57
  const _innerRef = React.useRef(null);
58
58
  const _onBudgetRefreshRequestedRef = React.useRef(onBudgetRefreshRequested);
59
59
  _onBudgetRefreshRequestedRef.current = onBudgetRefreshRequested;
60
60
  const _onChatHeaderMousedownRef = React.useRef(onChatHeaderMousedown);
61
61
  _onChatHeaderMousedownRef.current = onChatHeaderMousedown;
62
+ const _onCloseRequestedRef = React.useRef(onCloseRequested);
63
+ _onCloseRequestedRef.current = onCloseRequested;
62
64
  const _onSessionClearedRef = React.useRef(onSessionCleared);
63
65
  _onSessionClearedRef.current = onSessionCleared;
64
66
  const _onHandoffStartedRef = React.useRef(onHandoffStarted);
@@ -74,6 +76,8 @@ const FoundationAiAssistant = React.forwardRef(function FoundationAiAssistant(pr
74
76
  el.addEventListener('budget-refresh-requested', _onBudgetRefreshRequestedFn);
75
77
  const _onChatHeaderMousedownFn = (e) => _onChatHeaderMousedownRef.current?.(e);
76
78
  el.addEventListener('chat-header-mousedown', _onChatHeaderMousedownFn);
79
+ const _onCloseRequestedFn = (e) => _onCloseRequestedRef.current?.(e);
80
+ el.addEventListener('close-requested', _onCloseRequestedFn);
77
81
  const _onSessionClearedFn = (e) => _onSessionClearedRef.current?.(e);
78
82
  el.addEventListener('session-cleared', _onSessionClearedFn);
79
83
  const _onHandoffStartedFn = (e) => _onHandoffStartedRef.current?.(e);
@@ -85,6 +89,7 @@ const FoundationAiAssistant = React.forwardRef(function FoundationAiAssistant(pr
85
89
  return () => {
86
90
  el.removeEventListener('budget-refresh-requested', _onBudgetRefreshRequestedFn);
87
91
  el.removeEventListener('chat-header-mousedown', _onChatHeaderMousedownFn);
92
+ el.removeEventListener('close-requested', _onCloseRequestedFn);
88
93
  el.removeEventListener('session-cleared', _onSessionClearedFn);
89
94
  el.removeEventListener('handoff-started', _onHandoffStartedFn);
90
95
  el.removeEventListener('handoff-returned', _onHandoffReturnedFn);