@tylertech/forge-ai 0.15.2 → 0.15.3-dev.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 (59) hide show
  1. package/custom-elements.json +8266 -6476
  2. package/dist/ai-artifact-card/ai-artifact-card.d.ts +75 -0
  3. package/dist/ai-artifact-card/ai-artifact-card.mjs +142 -0
  4. package/dist/ai-artifact-card/ai-artifact-card.scss.mjs +283 -0
  5. package/dist/ai-artifact-card/index.d.ts +1 -0
  6. package/dist/ai-artifact-card/index.mjs +5 -0
  7. package/dist/ai-artifact-card-group/ai-artifact-card-group.d.ts +57 -0
  8. package/dist/ai-artifact-card-group/ai-artifact-card-group.mjs +137 -0
  9. package/dist/ai-artifact-card-group/ai-artifact-card-group.scss.mjs +173 -0
  10. package/dist/ai-artifact-card-group/index.d.ts +1 -0
  11. package/dist/ai-artifact-card-group/index.mjs +5 -0
  12. package/dist/ai-assistant-response/ai-assistant-response.mjs +25 -2
  13. package/dist/ai-chatbot/ag-ui-adapter.mjs +1 -1
  14. package/dist/ai-chatbot/agent-adapter.d.ts +108 -1
  15. package/dist/ai-chatbot/agent-adapter.mjs +22 -0
  16. package/dist/ai-chatbot/ai-chatbot-base.d.ts +26 -0
  17. package/dist/ai-chatbot/ai-chatbot-base.mjs +136 -3
  18. package/dist/ai-chatbot/index.d.ts +2 -2
  19. package/dist/ai-chatbot/message-state-controller.d.ts +10 -1
  20. package/dist/ai-chatbot/message-state-controller.mjs +78 -20
  21. package/dist/ai-chatbot/types.d.ts +199 -1
  22. package/dist/ai-chatbot/utils.mjs +3 -1
  23. package/dist/ai-chatbot-launcher/ai-chatbot-launcher.mjs +0 -2
  24. package/dist/ai-message-thread/ai-message-thread.d.ts +1 -0
  25. package/dist/ai-message-thread/ai-message-thread.mjs +35 -3
  26. package/dist/ai-steps/ai-steps.d.ts +67 -0
  27. package/dist/ai-steps/ai-steps.mjs +163 -0
  28. package/dist/ai-steps/ai-steps.scss.mjs +4 -0
  29. package/dist/ai-steps/index.d.ts +1 -0
  30. package/dist/ai-steps/index.mjs +5 -0
  31. package/dist/ai-tool-call-indicator/ai-tool-call-indicator.mjs +1 -1
  32. package/dist/core/chatbot-core-controller.d.ts +2 -0
  33. package/dist/core/chatbot-core-controller.mjs +99 -4
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.mjs +9 -0
  36. package/dist/tools/ai-mcp-app/ai-mcp-app.d.ts +69 -0
  37. package/dist/tools/ai-mcp-app/ai-mcp-app.mjs +574 -0
  38. package/dist/tools/ai-mcp-app/ai-mcp-app.scss.mjs +4 -0
  39. package/dist/tools/ai-mcp-app/index.d.ts +3 -0
  40. package/dist/tools/ai-mcp-app/index.mjs +6 -0
  41. package/dist/tools/ai-mcp-app/mcp-app-bridge-controller.d.ts +61 -0
  42. package/dist/tools/ai-mcp-app/mcp-app-bridge-controller.mjs +156 -0
  43. package/dist/tools/ai-mcp-app/mcp-app-bridge-types.d.ts +123 -0
  44. package/dist/tools/ai-mcp-app/mcp-app-bridge.d.ts +41 -0
  45. package/dist/tools/ai-mcp-app/mcp-app-bridge.mjs +293 -0
  46. package/dist/tools/ai-mcp-app/mcp-app-context.d.ts +46 -0
  47. package/dist/tools/ai-mcp-app/mcp-app-context.mjs +5 -0
  48. package/dist/tools/ai-steps/ai-steps-definition.d.ts +2 -0
  49. package/dist/tools/ai-steps/ai-steps-definition.mjs +50 -0
  50. package/dist/tools/ai-steps/ai-steps.d.ts +28 -0
  51. package/dist/tools/ai-steps/ai-steps.mjs +49 -0
  52. package/dist/tools/ai-steps/ai-steps.scss.mjs +4 -0
  53. package/dist/tools/ai-steps/index.d.ts +2 -0
  54. package/dist/tools/ai-steps/index.mjs +6 -0
  55. package/dist/tools/index.d.ts +1 -0
  56. package/dist/tools/index.mjs +5 -1
  57. package/dist/utils/inert-outside.d.ts +7 -0
  58. package/dist/utils/inert-outside.mjs +28 -0
  59. package/package.json +4 -1
@@ -479,7 +479,6 @@ threadName_get = function() {
479
479
  isEditing,
480
480
  () => html`
481
481
  <forge-ai-edit-thread
482
- density="medium"
483
482
  .thread=${{
484
483
  id: threadId,
485
484
  title: this.threadName || "",
@@ -522,7 +521,6 @@ threadActionsMenu_get = function() {
522
521
  };
523
522
  return html`
524
523
  <forge-ai-thread-actions-menu
525
- density="medium"
526
524
  .thread=${thread}
527
525
  ?show-rename=${this.showThreadRename}
528
526
  ?show-delete=${this.showThreadDelete}
@@ -61,6 +61,7 @@ export declare class AiMessageThreadComponent extends LitElement {
61
61
  debugMode: boolean;
62
62
  autoScroll: FeatureToggle;
63
63
  private _messageThreadContainer;
64
+ private _messageThreadContent;
64
65
  connectedCallback(): void;
65
66
  disconnectedCallback(): void;
66
67
  firstUpdated(): void;
@@ -2,6 +2,7 @@ import { unsafeCSS, LitElement, html, nothing } from "lit";
2
2
  import { property, query, customElement } from "lit/decorators.js";
3
3
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
4
4
  import { when } from "lit/directives/when.js";
5
+ import { keyed } from "lit/directives/keyed.js";
5
6
  import { MarkdownStreamController } from "../ai-chatbot/markdown-stream-controller.mjs";
6
7
  import "../ai-assistant-response/ai-assistant-response.mjs";
7
8
  import "../ai-client-message/ai-client-message.mjs";
@@ -12,6 +13,7 @@ import "../ai-thinking-indicator/ai-thinking-indicator.mjs";
12
13
  import "../ai-user-message/ai-user-message.mjs";
13
14
  import "../ai-chatbot/ai-chatbot-tool-call.mjs";
14
15
  import "../core/tooltip/tooltip.mjs";
16
+ import "../tools/ai-mcp-app/ai-mcp-app.mjs";
15
17
  import styles from "./ai-message-thread.scss.mjs";
16
18
  var __defProp = Object.defineProperty;
17
19
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -31,7 +33,7 @@ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read fr
31
33
  var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
32
34
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
33
35
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
34
- var _markdownController, _canAutoScroll, _resizeObserver, _AiMessageThreadComponent_instances, scheduleScroll_fn, checkScrollState_fn, _handleScroll, _handleScrollToBottomClick, scrollToBottomButton_get, handleCopy_fn, handleResend_fn, handleThumbsUp_fn, handleThumbsDown_fn, handleUserCopy_fn, handleUserResend_fn, handleUserEdit_fn, renderToolCall_fn, activeResponse_get, renderAssistantResponse_fn, emptyState_get, thinkingIndicator_get, messages_get, dispatchEvent_fn;
36
+ var _markdownController, _canAutoScroll, _lastScrollTop, _resizeObserver, _contentResizeObserver, _AiMessageThreadComponent_instances, observe_fn, handleContentResize_fn, scheduleScroll_fn, checkScrollState_fn, _handleScroll, _handleScrollToBottomClick, scrollToBottomButton_get, handleCopy_fn, handleResend_fn, handleThumbsUp_fn, handleThumbsDown_fn, handleUserCopy_fn, handleUserResend_fn, handleUserEdit_fn, renderToolCall_fn, activeResponse_get, renderAssistantResponse_fn, emptyState_get, thinkingIndicator_get, messages_get, dispatchEvent_fn;
35
37
  const AiMessageThreadComponentTagName = "forge-ai-message-thread";
36
38
  const SCROLL_THRESHOLD = 100;
37
39
  let AiMessageThreadComponent = class extends LitElement {
@@ -45,7 +47,9 @@ let AiMessageThreadComponent = class extends LitElement {
45
47
  this.autoScroll = "on";
46
48
  __privateAdd(this, _markdownController);
47
49
  __privateAdd(this, _canAutoScroll, true);
50
+ __privateAdd(this, _lastScrollTop, 0);
48
51
  __privateAdd(this, _resizeObserver);
52
+ __privateAdd(this, _contentResizeObserver);
49
53
  __privateAdd(this, _handleScroll, () => {
50
54
  __privateMethod(this, _AiMessageThreadComponent_instances, checkScrollState_fn).call(this);
51
55
  });
@@ -57,13 +61,18 @@ let AiMessageThreadComponent = class extends LitElement {
57
61
  super.connectedCallback();
58
62
  __privateSet(this, _markdownController, new MarkdownStreamController(this));
59
63
  __privateSet(this, _resizeObserver, new ResizeObserver(() => __privateMethod(this, _AiMessageThreadComponent_instances, checkScrollState_fn).call(this)));
64
+ __privateSet(this, _contentResizeObserver, new ResizeObserver(() => __privateMethod(this, _AiMessageThreadComponent_instances, handleContentResize_fn).call(this)));
65
+ if (this.hasUpdated) {
66
+ __privateMethod(this, _AiMessageThreadComponent_instances, observe_fn).call(this);
67
+ }
60
68
  }
61
69
  disconnectedCallback() {
62
70
  super.disconnectedCallback();
63
71
  __privateGet(this, _resizeObserver)?.disconnect();
72
+ __privateGet(this, _contentResizeObserver)?.disconnect();
64
73
  }
65
74
  firstUpdated() {
66
- __privateGet(this, _resizeObserver)?.observe(this._messageThreadContainer);
75
+ __privateMethod(this, _AiMessageThreadComponent_instances, observe_fn).call(this);
67
76
  }
68
77
  updated(changedProperties) {
69
78
  super.updated(changedProperties);
@@ -95,8 +104,19 @@ let AiMessageThreadComponent = class extends LitElement {
95
104
  };
96
105
  _markdownController = /* @__PURE__ */ new WeakMap();
97
106
  _canAutoScroll = /* @__PURE__ */ new WeakMap();
107
+ _lastScrollTop = /* @__PURE__ */ new WeakMap();
98
108
  _resizeObserver = /* @__PURE__ */ new WeakMap();
109
+ _contentResizeObserver = /* @__PURE__ */ new WeakMap();
99
110
  _AiMessageThreadComponent_instances = /* @__PURE__ */ new WeakSet();
111
+ observe_fn = function() {
112
+ __privateGet(this, _resizeObserver)?.observe(this._messageThreadContainer);
113
+ __privateGet(this, _contentResizeObserver)?.observe(this._messageThreadContent);
114
+ };
115
+ handleContentResize_fn = function() {
116
+ if (this.autoScroll === "on") {
117
+ this.scrollToBottom({ behavior: "instant" });
118
+ }
119
+ };
100
120
  scheduleScroll_fn = async function() {
101
121
  await this.updateComplete;
102
122
  await new Promise(requestAnimationFrame);
@@ -108,7 +128,10 @@ checkScrollState_fn = function() {
108
128
  return;
109
129
  }
110
130
  const { scrollTop, scrollHeight, clientHeight } = container;
111
- const canAutoScroll = scrollTop + clientHeight >= scrollHeight - SCROLL_THRESHOLD;
131
+ const isNearBottom = scrollTop + clientHeight >= scrollHeight - SCROLL_THRESHOLD;
132
+ const scrolledUp = scrollTop < __privateGet(this, _lastScrollTop);
133
+ __privateSet(this, _lastScrollTop, scrollTop);
134
+ const canAutoScroll = isNearBottom || __privateGet(this, _canAutoScroll) && !scrolledUp;
112
135
  if (canAutoScroll !== __privateGet(this, _canAutoScroll)) {
113
136
  __privateSet(this, _canAutoScroll, canAutoScroll);
114
137
  this.requestUpdate();
@@ -173,6 +196,9 @@ handleUserEdit_fn = function(messageId, content) {
173
196
  });
174
197
  };
175
198
  renderToolCall_fn = function(toolCall) {
199
+ if (toolCall.uiResource) {
200
+ return keyed(toolCall.id, html`<forge-ai-mcp-app .toolCall=${toolCall}></forge-ai-mcp-app>`);
201
+ }
176
202
  const toolDefinition = this.tools?.get(toolCall.name);
177
203
  return html`<forge-ai-chatbot-tool-call
178
204
  .toolCall=${toolCall}
@@ -231,6 +257,9 @@ messages_get = function() {
231
257
  return true;
232
258
  }
233
259
  if (item.type === "toolCall") {
260
+ if (item.data.uiResource) {
261
+ return true;
262
+ }
234
263
  const toolDef = this.tools?.get(item.data.name);
235
264
  return !!toolDef?.renderer;
236
265
  }
@@ -318,6 +347,9 @@ __decorateClass([
318
347
  __decorateClass([
319
348
  query(".message-thread")
320
349
  ], AiMessageThreadComponent.prototype, "_messageThreadContainer", 2);
350
+ __decorateClass([
351
+ query(".message-thread-content")
352
+ ], AiMessageThreadComponent.prototype, "_messageThreadContent", 2);
321
353
  AiMessageThreadComponent = __decorateClass([
322
354
  customElement(AiMessageThreadComponentTagName)
323
355
  ], AiMessageThreadComponent);
@@ -0,0 +1,67 @@
1
+ import { LitElement, PropertyValues, nothing, TemplateResult, CSSResult } from 'lit';
2
+ declare global {
3
+ interface HTMLElementTagNameMap {
4
+ 'forge-ai-steps': AiStepsComponent;
5
+ }
6
+ }
7
+ export declare const AiStepsComponentTagName: keyof HTMLElementTagNameMap;
8
+ /** Lifecycle state of an individual step. */
9
+ export type AiStepStatus = 'pending' | 'running' | 'complete';
10
+ /**
11
+ * A single step in the timeline. Every field is supplied by the agent — the component derives no
12
+ * labels of its own and makes no assumption about how the agent names its work.
13
+ */
14
+ export interface AiStep {
15
+ /**
16
+ * Agent-provided label for the step, e.g. `'Filtered by \`crime_category\`'`. Rendered as inline
17
+ * markdown, so the agent decides which part of its own label to emphasize. Literal `*`, `_` and
18
+ * `` ` `` characters need escaping.
19
+ */
20
+ label: string;
21
+ /**
22
+ * Optional agent-provided elaboration, rendered as a detail card beneath the label. Also rendered
23
+ * as inline markdown.
24
+ */
25
+ detail?: string;
26
+ /** Lifecycle state of this step. Defaults to `'complete'` when omitted. */
27
+ status?: AiStepStatus;
28
+ }
29
+ /**
30
+ * @tag forge-ai-steps
31
+ *
32
+ * @summary Expandable timeline summarizing the steps an agent took while producing a response.
33
+ *
34
+ * @description
35
+ * Collapsed, it shows a step count and a summary of the first couple of step labels. When expanded,
36
+ * it lists each step as a row in a vertical timeline along with its status. Steps carrying a
37
+ * `detail` render as detail cards; steps without one render as a simple labeled row.
38
+ *
39
+ * Labels and details are rendered as sanitized inline markdown, so the agent controls its own
40
+ * emphasis — bold, italic, strikethrough, `code` and links — rather than the component or the
41
+ * consumer prescribing which part of a step matters.
42
+ *
43
+ * This is a presentational primitive: it renders whatever `steps` it is given and knows nothing
44
+ * about tool calls or tool definitions. `forge-ai-tool-steps` is the tool renderer that maps an
45
+ * agent's `displaySteps` tool call onto these properties.
46
+ *
47
+ * While `running` is `true` the timeline stays expanded and the summary row is hidden. When
48
+ * `running` flips back to `false` the timeline auto-collapses, leaving the summary row as the way
49
+ * back in. An element that mounts with `running` already `false` renders expanded, so static usage
50
+ * (a finished run, or a Storybook story) is readable without interaction.
51
+ *
52
+ * @property {AiStep[]} steps - The steps to render as timeline rows
53
+ * @property {boolean} running - Whether the run producing these steps is still in progress
54
+ */
55
+ export declare class AiStepsComponent extends LitElement {
56
+ #private;
57
+ static styles: CSSResult;
58
+ steps: AiStep[];
59
+ /**
60
+ * Whether the run producing these steps is still in progress. While `true` the timeline is held
61
+ * open and the summary row is hidden; the transition back to `false` collapses the timeline.
62
+ */
63
+ running: boolean;
64
+ private _expanded;
65
+ willUpdate(changedProperties: PropertyValues<this>): void;
66
+ render(): TemplateResult | typeof nothing;
67
+ }
@@ -0,0 +1,163 @@
1
+ import { unsafeCSS, LitElement, html, nothing } from "lit";
2
+ import { property, state, customElement } from "lit/decorators.js";
3
+ import { join } from "lit/directives/join.js";
4
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
5
+ import { when } from "lit/directives/when.js";
6
+ import { renderInlineMarkdown } from "../utils/markdown.mjs";
7
+ import styles from "./ai-steps.scss.mjs";
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __typeError = (msg) => {
11
+ throw TypeError(msg);
12
+ };
13
+ var __decorateClass = (decorators, target, key, kind) => {
14
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
15
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
16
+ if (decorator = decorators[i])
17
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
18
+ if (kind && result) __defProp(target, key, result);
19
+ return result;
20
+ };
21
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
22
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
23
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
24
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
25
+ var _chevronIcon, _rowMarker, _AiStepsComponent_instances, renderInline_fn, summaryLabel_get, toggle_fn, renderNoDetailStep_fn, renderStep_fn, isExpanded_get, steps_get, stepsSummary_get, stepsCountLabel_get;
26
+ const AiStepsComponentTagName = "forge-ai-steps";
27
+ const MAX_DETAIL_LENGTH = 2e3;
28
+ let AiStepsComponent = class extends LitElement {
29
+ constructor() {
30
+ super(...arguments);
31
+ __privateAdd(this, _AiStepsComponent_instances);
32
+ this.steps = [];
33
+ this.running = false;
34
+ this._expanded = true;
35
+ __privateAdd(this, _chevronIcon, html`
36
+ <svg
37
+ class="chevron"
38
+ xmlns="http://www.w3.org/2000/svg"
39
+ viewBox="0 0 24 24"
40
+ height="20"
41
+ width="20"
42
+ fill="currentColor"
43
+ aria-hidden="true">
44
+ <path d="M7.41 8.59 12 13.17l4.59-4.58L18 10l-6 6-6-6z" />
45
+ </svg>
46
+ `);
47
+ __privateAdd(this, _rowMarker, html`<span class="row-marker"></span>`);
48
+ }
49
+ willUpdate(changedProperties) {
50
+ if (changedProperties.has("running") && !this.running && changedProperties.get("running") === true) {
51
+ this._expanded = false;
52
+ }
53
+ }
54
+ render() {
55
+ if (this.steps.length === 0) {
56
+ return nothing;
57
+ }
58
+ return html`
59
+ <div class="steps">
60
+ <div class="steps-count">${__privateGet(this, _AiStepsComponent_instances, stepsCountLabel_get)}</div>
61
+ ${__privateGet(this, _AiStepsComponent_instances, stepsSummary_get)} ${__privateGet(this, _AiStepsComponent_instances, steps_get)}
62
+ </div>
63
+ `;
64
+ }
65
+ };
66
+ _chevronIcon = /* @__PURE__ */ new WeakMap();
67
+ _rowMarker = /* @__PURE__ */ new WeakMap();
68
+ _AiStepsComponent_instances = /* @__PURE__ */ new WeakSet();
69
+ renderInline_fn = function(value) {
70
+ return html`${unsafeHTML(renderInlineMarkdown(value))}`;
71
+ };
72
+ summaryLabel_get = function() {
73
+ const stepLabels = this.steps.map((step) => html`<span>${__privateMethod(this, _AiStepsComponent_instances, renderInline_fn).call(this, step.label)}</span>`);
74
+ const visibleLabels = stepLabels.slice(0, 2);
75
+ const remaining = stepLabels.length - visibleLabels.length;
76
+ return remaining > 0 ? html`${join(visibleLabels, html`, `)} +${remaining} more` : html`${join(visibleLabels, html`, `)}`;
77
+ };
78
+ toggle_fn = function() {
79
+ this._expanded = !this._expanded;
80
+ };
81
+ renderNoDetailStep_fn = function(step) {
82
+ return html`
83
+ <div class="timeline-row" data-status=${step.status ?? "complete"}>
84
+ <div class="row-header">
85
+ ${__privateGet(this, _rowMarker)}
86
+ <span class="row-label">
87
+ <span class="row-name">${__privateMethod(this, _AiStepsComponent_instances, renderInline_fn).call(this, step.label)}</span>
88
+ </span>
89
+ </div>
90
+ </div>
91
+ `;
92
+ };
93
+ renderStep_fn = function(step) {
94
+ const detail = step.detail;
95
+ if (!detail) {
96
+ return __privateMethod(this, _AiStepsComponent_instances, renderNoDetailStep_fn).call(this, step);
97
+ }
98
+ const truncated = detail.length > MAX_DETAIL_LENGTH ? `${detail.slice(0, MAX_DETAIL_LENGTH)}… (truncated)` : detail;
99
+ return html`
100
+ <div class="timeline-row" data-status=${step.status ?? "complete"}>
101
+ <div class="row-header">
102
+ ${__privateGet(this, _rowMarker)}
103
+ <div class="step-card">
104
+ <span class="step-card-title">${__privateMethod(this, _AiStepsComponent_instances, renderInline_fn).call(this, step.label)}</span>
105
+ <div class="step-card-body">
106
+ <div class="step-card-result">${__privateMethod(this, _AiStepsComponent_instances, renderInline_fn).call(this, truncated)}</div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ `;
112
+ };
113
+ isExpanded_get = function() {
114
+ return this.running || this._expanded;
115
+ };
116
+ steps_get = function() {
117
+ return html`
118
+ <div
119
+ id="timeline-content"
120
+ class="timeline ${__privateGet(this, _AiStepsComponent_instances, isExpanded_get) ? "expanded" : ""}"
121
+ aria-hidden=${__privateGet(this, _AiStepsComponent_instances, isExpanded_get) ? "false" : "true"}>
122
+ <div class="timeline-content">${this.steps.map((step) => __privateMethod(this, _AiStepsComponent_instances, renderStep_fn).call(this, step))}</div>
123
+ </div>
124
+ `;
125
+ };
126
+ stepsSummary_get = function() {
127
+ return when(
128
+ !this.running,
129
+ () => html`
130
+ <button
131
+ class="summary"
132
+ type="button"
133
+ aria-expanded=${this._expanded}
134
+ aria-controls="timeline-content"
135
+ @click=${__privateMethod(this, _AiStepsComponent_instances, toggle_fn)}>
136
+ ${__privateGet(this, _chevronIcon)}
137
+ <span class="status-text">${__privateGet(this, _AiStepsComponent_instances, summaryLabel_get)}</span>
138
+ <span class="focus-indicator"></span>
139
+ </button>
140
+ `
141
+ );
142
+ };
143
+ stepsCountLabel_get = function() {
144
+ const count = this.steps.length;
145
+ return `${count} step${count === 1 ? "" : "s"}`;
146
+ };
147
+ AiStepsComponent.styles = unsafeCSS(styles);
148
+ __decorateClass([
149
+ property({ attribute: false })
150
+ ], AiStepsComponent.prototype, "steps", 2);
151
+ __decorateClass([
152
+ property({ type: Boolean, reflect: true })
153
+ ], AiStepsComponent.prototype, "running", 2);
154
+ __decorateClass([
155
+ state()
156
+ ], AiStepsComponent.prototype, "_expanded", 2);
157
+ AiStepsComponent = __decorateClass([
158
+ customElement(AiStepsComponentTagName)
159
+ ], AiStepsComponent);
160
+ export {
161
+ AiStepsComponent,
162
+ AiStepsComponentTagName
163
+ };
@@ -0,0 +1,4 @@
1
+ const styles = '/* prettier-ignore */\n@keyframes fadein {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n@keyframes forge-focus-indicator-outward-grow {\n from {\n outline-width: 0;\n }\n to {\n outline-width: var(--_focus-indicator-active-width);\n }\n}\n@keyframes forge-focus-indicator-outward-shrink {\n from {\n outline-width: var(--_focus-indicator-active-width);\n }\n}\n@keyframes forge-focus-indicator-inward-grow {\n from {\n border-width: 0;\n }\n to {\n border-width: var(--_focus-indicator-active-width);\n }\n}\n@keyframes forge-focus-indicator-inward-shrink {\n from {\n border-width: var(--_focus-indicator-active-width);\n }\n}\n:host {\n display: flex;\n flex-direction: column;\n gap: var(--forge-spacing-small, 12px);\n}\n\n.summary {\n display: flex;\n align-items: center;\n gap: var(--forge-spacing-xsmall, 8px);\n padding-block: var(--forge-spacing-xxsmall, 4px);\n padding-inline: var(--forge-spacing-xsmall, 8px);\n border: none;\n background-color: var(--forge-theme-surface-container-minimum, #f5f5f5);\n border-radius: calc(var(--forge-shape-large, 8px) * var(--forge-shape-factor, 1));\n cursor: pointer;\n text-align: start;\n position: relative;\n outline: none;\n --_state-layer-display: var(--forge-state-layer-display, flex);\n --_state-layer-color: var(--forge-state-layer-color, var(--forge-theme-on-surface, #000000));\n --_state-layer-hover-color: var(--forge-state-layer-hover-color, var(--_state-layer-color));\n --_state-layer-hover-opacity: var(--forge-state-layer-hover-opacity, 0.08);\n --_state-layer-pressed-color: var(--forge-state-layer-pressed-color, var(--_state-layer-color));\n --_state-layer-pressed-opacity: var(--forge-state-layer-pressed-opacity, 0.12);\n --_state-layer-hover-duration: var(--forge-state-layer-hover-duration, 15ms);\n --_state-layer-pressed-duration: var(--forge-state-layer-pressed-duration, 105ms);\n --_state-layer-animation-duration: var(--forge-state-layer-animation-duration, 375ms);\n}\n.summary::before {\n opacity: 0;\n position: absolute;\n backface-visibility: hidden;\n transform: translateZ(0);\n background-color: var(--_state-layer-hover-color);\n inset: 0;\n transition: opacity var(--_state-layer-hover-duration) linear, background-color var(--_state-layer-hover-duration) linear;\n --_state-layer-hover-duration: var(--forge-state-layer-hover-duration, 100ms);\n content: "";\n opacity: 0;\n border-radius: inherit;\n}\n.summary:hover::before {\n background-color: var(--_state-layer-hover-color);\n opacity: var(--_state-layer-hover-opacity);\n}\n.summary:active::before {\n opacity: var(--_state-layer-pressed-opacity);\n transition-duration: var(--_state-layer-pressed-duration);\n --_state-layer-pressed-opacity: var(--forge-state-layer-pressed-opacity, 0.18);\n}\n\n.status-text {\n -moz-osx-font-smoothing: grayscale;\n -webkit-font-smoothing: antialiased;\n font-family: var(--forge-typography-body1-font-family, var(--forge-typography-font-family, "Roboto", sans-serif));\n font-size: var(--forge-typography-body1-font-size, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-body-font-size-scale, 0.875)));\n font-weight: var(--forge-typography-body1-font-weight, 400);\n line-height: var(--forge-typography-body1-line-height, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-body-line-height-scale, 1.125)));\n letter-spacing: var(--forge-typography-body1-letter-spacing, 0.0357142857em);\n text-transform: var(--forge-typography-body1-text-transform, inherit);\n text-decoration: var(--forge-typography-body1-text-decoration, inherit);\n}\n\n.chevron {\n flex-shrink: 0;\n transform: rotate(-90deg);\n transition: transform var(--forge-animation-duration-short4, 200ms) var(--forge-animation-easing-standard, cubic-bezier(0.2, 0, 0, 1));\n}\n\n.summary[aria-expanded=true] .chevron {\n transform: rotate(0deg);\n}\n\n.focus-indicator {\n position: absolute;\n display: none;\n}\n\n:host .summary:focus .focus-indicator {\n --_focus-indicator-display: var(--forge-focus-indicator-display, flex);\n --_focus-indicator-width: var(--forge-focus-indicator-width, var(--forge-border-medium, 2px));\n --_focus-indicator-active-width: var(--forge-focus-indicator-active-width, 6px);\n --_focus-indicator-color: var(--forge-focus-indicator-color, var(--forge-theme-primary, #3f51b5));\n --_focus-indicator-shape: var(--forge-focus-indicator-shape, calc(var(--forge-shape-extra-small, 1px) * var(--forge-shape-factor, 1)));\n --_focus-indicator-duration: var(--forge-focus-indicator-duration, var(--forge-animation-duration-long4, 600ms));\n --_focus-indicator-easing: var(--forge-focus-indicator-easing, var(--forge-animation-easing-emphasized, cubic-bezier(0.2, 0, 0, 1)));\n --_focus-indicator-shape-start-start: var(--forge-focus-indicator-shape-start-start, var(--_focus-indicator-shape));\n --_focus-indicator-shape-start-end: var(--forge-focus-indicator-shape-start-end, var(--_focus-indicator-shape));\n --_focus-indicator-shape-end-end: var(--forge-focus-indicator-shape-end-end, var(--_focus-indicator-shape));\n --_focus-indicator-shape-end-start: var(--forge-focus-indicator-shape-end-start, var(--_focus-indicator-shape));\n --_focus-indicator-outward-offset: var(--forge-focus-indicator-outward-offset, var(--forge-spacing-xxsmall, 4px));\n --_focus-indicator-inward-offset: var(--forge-focus-indicator-inward-offset, 0px);\n --_focus-indicator-offset-block: var(--forge-focus-indicator-offset-block, 0);\n --_focus-indicator-offset-inline: var(--forge-focus-indicator-offset-inline, 0);\n animation-delay: 0s, calc(var(--_focus-indicator-duration) * 0.25);\n animation-duration: calc(var(--_focus-indicator-duration) * 0.25), calc(var(--_focus-indicator-duration) * 0.75);\n animation-timing-function: var(--_focus-indicator-easing);\n box-sizing: border-box;\n color: var(--_focus-indicator-color);\n display: none;\n pointer-events: none;\n position: absolute;\n margin-block: var(--_focus-indicator-offset-block);\n margin-inline: var(--_focus-indicator-offset-inline);\n animation-name: forge-focus-indicator-inward-grow, forge-focus-indicator-inward-shrink;\n border-end-end-radius: calc(var(--_focus-indicator-shape-end-end) - var(--_focus-indicator-inward-offset));\n border-end-start-radius: calc(var(--_focus-indicator-shape-end-start) - var(--_focus-indicator-inward-offset));\n border-start-end-radius: calc(var(--_focus-indicator-shape-start-end) - var(--_focus-indicator-inward-offset));\n border-start-start-radius: calc(var(--_focus-indicator-shape-start-start) - var(--_focus-indicator-inward-offset));\n border: var(--_focus-indicator-width) solid currentColor;\n inset: var(--_focus-indicator-inward-offset);\n}\n:host .summary:focus .focus-indicator::before {\n content: "";\n}\n:host .summary:focus .focus-indicator {\n display: block;\n --forge-focus-indicator-shape: calc(var(--forge-shape-large, 8px) * var(--forge-shape-factor, 1));\n}\n\n.steps {\n display: flex;\n flex-direction: column;\n gap: var(--forge-spacing-xsmall, 8px);\n -moz-osx-font-smoothing: grayscale;\n -webkit-font-smoothing: antialiased;\n font-family: var(--forge-typography-body1-font-family, var(--forge-typography-font-family, "Roboto", sans-serif));\n font-size: var(--forge-typography-body1-font-size, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-body-font-size-scale, 0.875)));\n font-weight: var(--forge-typography-body1-font-weight, 400);\n line-height: var(--forge-typography-body1-line-height, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-body-line-height-scale, 1.125)));\n letter-spacing: var(--forge-typography-body1-letter-spacing, 0.0357142857em);\n text-transform: var(--forge-typography-body1-text-transform, inherit);\n text-decoration: var(--forge-typography-body1-text-decoration, inherit);\n font-size: var(--forge-ai-markdown-font-size, 0.875rem);\n line-height: var(--forge-ai-markdown-line-height, 1.5rem);\n color: var(--forge-theme-on-surface, #000000);\n overflow-wrap: break-word;\n}\n.steps h1,\n.steps h2,\n.steps h3,\n.steps h4,\n.steps h5,\n.steps h6 {\n margin: 1.75rem 0 var(--forge-spacing-small, 12px);\n}\n.steps strong {\n font-weight: 500;\n}\n.steps a {\n color: var(--forge-theme-primary, #3f51b5);\n}\n.steps hr {\n border: none;\n border-top: 1px solid var(--forge-theme-outline, #e0e0e0);\n}\n.steps code {\n color: var(--forge-theme-on-surface-container-low, #000000);\n background-color: var(--forge-theme-surface-container-low, #ebebeb);\n border-radius: calc(var(--forge-shape-medium, 4px) * var(--forge-shape-factor, 1));\n padding: var(--forge-spacing-xxxsmall, 2px) var(--forge-spacing-xxsmall, 4px);\n font-size: 0.75em;\n}\n.steps pre {\n border: 1px solid var(--forge-theme-outline, #e0e0e0);\n border-radius: calc(var(--forge-shape-medium, 4px) * var(--forge-shape-factor, 1));\n margin: 0;\n max-width: 100%;\n overflow-x: auto;\n}\n.steps pre:focus-visible {\n outline: 2px solid var(--forge-theme-primary, #3f51b5);\n outline-offset: 2px;\n}\n.steps pre code {\n display: block;\n padding: var(--forge-spacing-xsmall, 8px) var(--forge-spacing-small, 12px);\n color: var(--forge-theme-on-surface-container-minimum, #000000);\n background: var(--forge-theme-surface-container-minimum, #f5f5f5);\n white-space: pre;\n}\n.steps [data-scroll-region=table] {\n max-width: 100%;\n overflow-x: auto;\n border-radius: calc(var(--forge-shape-large, 8px) * var(--forge-shape-factor, 1));\n margin-block: var(--forge-spacing-medium, 16px);\n}\n.steps [data-scroll-region=table]:focus-visible {\n outline: 2px solid var(--forge-theme-primary, #3f51b5);\n outline-offset: 2px;\n}\n.steps [data-scroll-region=table] table {\n margin-block: 0;\n}\n.steps table {\n width: 100%;\n border-spacing: 0;\n background-color: var(--forge-theme-surface, #ffffff);\n border-radius: calc(var(--forge-shape-large, 8px) * var(--forge-shape-factor, 1));\n overflow: hidden;\n margin-block: var(--forge-spacing-medium, 16px);\n}\n.steps table thead {\n color: var(--forge-theme-text-medium, rgba(0, 0, 0, 0.6));\n font-weight: bold;\n text-align: left;\n font-size: calc(var(--forge-typography-font-size, 1rem) * 0.75);\n box-sizing: border-box;\n}\n.steps table tr {\n height: 36px;\n height: 32px;\n}\n.steps table tr:nth-child(even) {\n background-color: var(--forge-theme-surface-dim, #fafafa);\n}\n.steps table tr:last-child td {\n border-bottom: none;\n}\n.steps table th,\n.steps table td {\n -moz-osx-font-smoothing: grayscale;\n -webkit-font-smoothing: antialiased;\n font-family: var(--forge-typography-label1-font-family, var(--forge-typography-font-family, "Roboto", sans-serif));\n font-size: var(--forge-typography-label1-font-size, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-label-font-size-scale, 0.75)));\n font-weight: var(--forge-typography-label1-font-weight, 400);\n line-height: var(--forge-typography-label1-line-height, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-label-line-height-scale, 1.25)));\n letter-spacing: var(--forge-typography-label1-letter-spacing, 0.0357142857em);\n text-transform: var(--forge-typography-label1-text-transform, inherit);\n text-decoration: var(--forge-typography-label1-text-decoration, inherit);\n padding-block: var(--forge-spacing-xsmall, 8px);\n padding-inline-start: var(--forge-spacing-xsmall, 8px);\n padding-inline-end: var(--forge-spacing-medium, 16px);\n border-bottom-width: 1px;\n border-bottom-style: solid;\n border-bottom-color: var(--forge-theme-outline, #e0e0e0);\n}\n.steps table th {\n -moz-osx-font-smoothing: grayscale;\n -webkit-font-smoothing: antialiased;\n font-family: var(--forge-typography-label2-font-family, var(--forge-typography-font-family, "Roboto", sans-serif));\n font-size: var(--forge-typography-label2-font-size, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-label-font-size-scale, 0.8125)));\n font-weight: var(--forge-typography-label2-font-weight, 400);\n line-height: var(--forge-typography-label2-line-height, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-label-line-height-scale, 1.25)));\n letter-spacing: var(--forge-typography-label2-letter-spacing, 0.0096153846em);\n text-transform: var(--forge-typography-label2-text-transform, inherit);\n text-decoration: var(--forge-typography-label2-text-decoration, inherit);\n font-weight: 600;\n}\n.steps > p:has(+ ol),\n.steps > p:has(+ ul) {\n margin-block-end: var(--forge-spacing-small, 12px);\n}\n.steps > ul {\n list-style-type: disc;\n padding-inline-start: 1.6875rem;\n}\n.steps > ol {\n padding-inline-start: 1.75rem;\n}\n.steps > ol,\n.steps > ul {\n margin: var(--forge-spacing-xsmall, 8px) 0;\n margin-block-end: var(--forge-spacing-medium, 16px);\n}\n.steps > :first-child {\n margin-block-start: 0;\n}\n.steps > p {\n margin-block-end: var(--forge-spacing-medium, 16px);\n overflow-wrap: anywhere;\n}\n.steps > p:first-child {\n margin-block-start: 0;\n}\n.steps > p:last-child {\n margin-block-end: 0;\n}\n.steps-count {\n color: var(--forge-theme-text-medium, rgba(0, 0, 0, 0.6));\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.timeline {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--forge-animation-duration-medium2, 300ms) var(--forge-animation-easing-decelerate, cubic-bezier(0, 0, 0, 1));\n}\n.timeline.expanded {\n grid-template-rows: 1fr;\n}\n\n.timeline-content {\n overflow: hidden;\n min-height: 0;\n padding-inline-start: var(--forge-spacing-small, 12px);\n}\n\n.timeline-row {\n position: relative;\n padding-inline-start: var(--forge-spacing-large, 24px);\n padding-block-end: var(--forge-spacing-medium, 16px);\n}\n.timeline-row::before {\n content: "";\n position: absolute;\n inset-block-start: calc((var(--forge-spacing-medium-large, 20px) - var(--forge-spacing-xsmall, 8px)) / 2);\n inset-block-end: calc(-1 * var(--forge-spacing-xsmall, 8px));\n inset-inline-start: calc((var(--forge-spacing-medium-large, 20px) - 1px) / 2);\n width: 1px;\n background-color: var(--forge-theme-outline, #e0e0e0);\n}\n.timeline-row:last-child::before {\n display: none;\n}\n\n.row-header {\n display: flex;\n align-items: flex-start;\n gap: var(--forge-spacing-small, 12px);\n width: 100%;\n min-height: var(--forge-spacing-medium-large, 20px);\n margin-inline-start: calc(-1 * var(--forge-spacing-large, 24px));\n}\n\n.row-marker {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--forge-spacing-medium-large, 20px);\n height: var(--forge-spacing-medium-large, 20px);\n border-radius: calc(var(--forge-shape-round, 50%) * var(--forge-shape-factor, 1));\n}\n.row-marker::before {\n content: "";\n flex-shrink: 0;\n width: var(--forge-spacing-xsmall, 8px);\n height: var(--forge-spacing-xsmall, 8px);\n border-radius: calc(var(--forge-shape-round, 50%) * var(--forge-shape-factor, 1));\n background-color: currentColor;\n position: relative;\n z-index: 1;\n}\n\n.row-label {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-width: 0;\n gap: var(--forge-spacing-xxxsmall, 2px);\n}\n\n.row-name {\n -moz-osx-font-smoothing: grayscale;\n -webkit-font-smoothing: antialiased;\n font-family: var(--forge-typography-body1-font-family, var(--forge-typography-font-family, "Roboto", sans-serif));\n font-size: var(--forge-typography-body1-font-size, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-body-font-size-scale, 0.875)));\n font-weight: var(--forge-typography-body1-font-weight, 400);\n line-height: var(--forge-typography-body1-line-height, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-body-line-height-scale, 1.125)));\n letter-spacing: var(--forge-typography-body1-letter-spacing, 0.0357142857em);\n text-transform: var(--forge-typography-body1-text-transform, inherit);\n text-decoration: var(--forge-typography-body1-text-decoration, inherit);\n display: flex;\n align-items: center;\n gap: var(--forge-spacing-xsmall, 8px);\n line-height: normal;\n font-weight: 500;\n color: var(--forge-theme-text-high, rgba(0, 0, 0, 0.87));\n}\n\n.step-card {\n width: 100%;\n}\n.step-card-title {\n -moz-osx-font-smoothing: grayscale;\n -webkit-font-smoothing: antialiased;\n font-family: var(--forge-typography-body1-font-family, var(--forge-typography-font-family, "Roboto", sans-serif));\n font-size: var(--forge-typography-body1-font-size, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-body-font-size-scale, 0.875)));\n font-weight: var(--forge-typography-body1-font-weight, 400);\n line-height: var(--forge-typography-body1-line-height, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-body-line-height-scale, 1.125)));\n letter-spacing: var(--forge-typography-body1-letter-spacing, 0.0357142857em);\n text-transform: var(--forge-typography-body1-text-transform, inherit);\n text-decoration: var(--forge-typography-body1-text-decoration, inherit);\n font-weight: 700;\n}\n.step-card-result {\n -moz-osx-font-smoothing: grayscale;\n -webkit-font-smoothing: antialiased;\n font-family: var(--forge-typography-body1-font-family, var(--forge-typography-font-family, "Roboto", sans-serif));\n font-size: var(--forge-typography-body1-font-size, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-body-font-size-scale, 0.875)));\n font-weight: var(--forge-typography-body1-font-weight, 400);\n line-height: var(--forge-typography-body1-line-height, calc(var(--forge-typography-font-size, 1rem) * var(--forge-typography-body-line-height-scale, 1.125)));\n letter-spacing: var(--forge-typography-body1-letter-spacing, 0.0357142857em);\n text-transform: var(--forge-typography-body1-text-transform, inherit);\n text-decoration: var(--forge-typography-body1-text-decoration, inherit);\n}\n\n.row-name strong,\n.step-card-title strong {\n font-weight: 700;\n}';
2
+ export {
3
+ styles as default
4
+ };
@@ -0,0 +1 @@
1
+ export * from './ai-steps';
@@ -0,0 +1,5 @@
1
+ import { AiStepsComponent, AiStepsComponentTagName } from "./ai-steps.mjs";
2
+ export {
3
+ AiStepsComponent,
4
+ AiStepsComponentTagName
5
+ };
@@ -218,7 +218,7 @@ highlightJson_fn = function(value) {
218
218
  return html`${unsafeHTML(highlighted)}`;
219
219
  };
220
220
  statusMarker_fn = function(toolCall) {
221
- if (toolCall.status === "error") {
221
+ if (toolCall.status === "error" || toolCall.status === "cancelled") {
222
222
  return html`<span class="row-marker row-marker--error">${__privateGet(this, _errorIcon)}</span>`;
223
223
  }
224
224
  if (isToolCallSettled(toolCall, this.tools?.get(toolCall.name))) {
@@ -38,6 +38,8 @@ export declare class ChatbotCoreController implements ReactiveController {
38
38
  getMessages(): ChatMessage[];
39
39
  setMessages(messages: ChatMessage[]): void;
40
40
  updateMessageContent(id: string, content: string): void;
41
+ /** Tool call ids rendered at or after `index` in {@link messageItems}. */
42
+ getToolCallIdsFrom(index: number): string[];
41
43
  removeMessageItemsFrom(index: number): void;
42
44
  setResponseFeedback(responseId: string, feedback: {
43
45
  type: 'positive' | 'negative';
@@ -2,6 +2,7 @@ import { SubscriptionManager } from "../ai-chatbot/event-emitter.mjs";
2
2
  import { FileUploadManager } from "../ai-chatbot/file-upload-manager.mjs";
3
3
  import { MessageStateController } from "../ai-chatbot/message-state-controller.mjs";
4
4
  import { generateId } from "../ai-chatbot/utils.mjs";
5
+ const UNSETTLED_TOOL_CALL_STATUSES = ["pending", "parsing", "executing"];
5
6
  class ChatbotCoreController {
6
7
  #host;
7
8
  #callbacks;
@@ -10,6 +11,14 @@ class ChatbotCoreController {
10
11
  #adapterSubscriptions;
11
12
  #adapter;
12
13
  #executingToolHandlers = 0;
14
+ /** Tool calls announced by an `mcp-ui-resource` event; they only settle via a tool result. */
15
+ #mcpAppToolCallIds = /* @__PURE__ */ new Set();
16
+ /** Agent tool calls marked complete on their `tool-call` chunk, before any result arrived. */
17
+ #presumedCompleteToolCallIds = /* @__PURE__ */ new Set();
18
+ /** Resolved UI resources whose tool call is not in state yet. */
19
+ #pendingUiResources = /* @__PURE__ */ new Map();
20
+ /** Bumped whenever the thread is cleared or replaced, so in-flight UI resource resolves from before are dropped. */
21
+ #threadGeneration = 0;
13
22
  constructor(host, config) {
14
23
  this.#host = host;
15
24
  this.#callbacks = config.callbacks;
@@ -83,6 +92,7 @@ class ChatbotCoreController {
83
92
  this.#adapter.onToolCallEnd(this.#handleToolCallEnd.bind(this)),
84
93
  this.#adapter.onToolCall(this.#handleToolCall.bind(this)),
85
94
  this.#adapter.onToolCallResult(this.#handleToolCallResult.bind(this)),
95
+ this.#adapter.onMcpUiResource(this.#handleMcpUiResource.bind(this)),
86
96
  this.#adapter.onStepStarted(this.#handleStepStarted.bind(this)),
87
97
  this.#adapter.onStepFinished(this.#handleStepFinished.bind(this)),
88
98
  this.#adapter.onRunFinished(this.#handleRunFinished.bind(this)),
@@ -115,10 +125,29 @@ class ChatbotCoreController {
115
125
  #handleRunFinished() {
116
126
  this.#tryCompleteResponse();
117
127
  }
128
+ /**
129
+ * A run ended (finished, aborted, or errored) without delivering a result for an
130
+ * MCP-app tool call: mark it cancelled so the widget receives `tool-cancelled`.
131
+ */
132
+ #cancelUnsettledMcpAppToolCalls() {
133
+ for (const id of this.#mcpAppToolCallIds) {
134
+ const toolCall = this.#messageStateController.getToolCall(id);
135
+ if (toolCall && UNSETTLED_TOOL_CALL_STATUSES.includes(toolCall.status)) {
136
+ this.#messageStateController.updateToolCallInResponse(id, { status: "cancelled" });
137
+ }
138
+ }
139
+ this.#mcpAppToolCallIds.clear();
140
+ this.#presumedCompleteToolCallIds.clear();
141
+ }
142
+ /**
143
+ * Finalizes once the run is truly over. An executing client-tool handler or a running
144
+ * adapter means a continuation follows, so widgets stay open for it to settle (§2).
145
+ */
118
146
  #tryCompleteResponse() {
119
147
  if (this.#executingToolHandlers > 0 || this.#adapter?.isRunning) {
120
148
  return;
121
149
  }
150
+ this.#cancelUnsettledMcpAppToolCalls();
122
151
  this.#messageStateController.tryFinalizeResponse();
123
152
  const messages = this.getMessages();
124
153
  const lastMessage = messages[messages.length - 1];
@@ -134,7 +163,8 @@ class ChatbotCoreController {
134
163
  args: {},
135
164
  argsBuffer: "",
136
165
  status: "parsing",
137
- type: this.tools.has(event.name) ? "client" : "agent"
166
+ type: this.tools.has(event.name) ? "client" : "agent",
167
+ ...this.#takePendingUiResource(event.id)
138
168
  };
139
169
  this.#messageStateController.addToolCallToResponse(toolCall);
140
170
  const toolDef = this.tools.get(event.name);
@@ -195,7 +225,8 @@ class ChatbotCoreController {
195
225
  name: event.name,
196
226
  args: event.args,
197
227
  status: "executing",
198
- type: this.tools.has(event.name) ? "client" : "agent"
228
+ type: this.tools.has(event.name) ? "client" : "agent",
229
+ ...this.#takePendingUiResource(event.id)
199
230
  };
200
231
  this.#messageStateController.addToolCallToResponse(toolCall);
201
232
  }
@@ -211,7 +242,8 @@ class ChatbotCoreController {
211
242
  } else {
212
243
  this.#sendToolResult(event.id, this.#createToolResponse(event.name));
213
244
  }
214
- } else {
245
+ } else if (!this.#mcpAppToolCallIds.has(event.id)) {
246
+ this.#presumedCompleteToolCallIds.add(event.id);
215
247
  this.#messageStateController.updateToolCallInResponse(event.id, { status: "complete" });
216
248
  }
217
249
  }
@@ -249,6 +281,7 @@ class ChatbotCoreController {
249
281
  }
250
282
  }
251
283
  #handleError(event) {
284
+ this.#cancelUnsettledMcpAppToolCalls();
252
285
  const errorMessage = {
253
286
  id: generateId(),
254
287
  role: "assistant",
@@ -261,6 +294,7 @@ class ChatbotCoreController {
261
294
  this.#callbacks.onDispatchEvent("forge-ai-chatbot-error", { error: event.message });
262
295
  }
263
296
  #handleRunAborted() {
297
+ this.#cancelUnsettledMcpAppToolCalls();
264
298
  this.#messageStateController.tryFinalizeResponse();
265
299
  const id = this.#messageStateController.addClientMessage({ content: "Run cancelled" });
266
300
  const abortMessage = this.#messageStateController.getMessage(id);
@@ -272,8 +306,53 @@ class ChatbotCoreController {
272
306
  this.#callbacks.onRequestUpdate();
273
307
  }
274
308
  #handleToolCallResult(event) {
309
+ this.#presumedCompleteToolCallIds.delete(event.toolCallId);
275
310
  this.#messageStateController.completeToolCallInResponse(event.toolCallId, event.result, event);
276
311
  }
312
+ /**
313
+ * Resolves and stamps the UI resource. Works whether the event arrives before or after
314
+ * the tool call's chunks, mid-run, after the run ended, or after `setMessages` for
315
+ * tool calls restored from history. A resolve that outlives the thread it started in
316
+ * (cleared/replaced, or the adapter swapped) is dropped.
317
+ */
318
+ async #handleMcpUiResource(event) {
319
+ const adapter = this.#adapter;
320
+ if (!adapter?.resolveMcpAppResource) {
321
+ return;
322
+ }
323
+ const { toolCallId, resourceUri } = event;
324
+ const generation = this.#threadGeneration;
325
+ this.#mcpAppToolCallIds.add(toolCallId);
326
+ if (this.#presumedCompleteToolCallIds.delete(toolCallId)) {
327
+ this.#messageStateController.updateToolCallInResponse(toolCallId, {
328
+ status: "executing",
329
+ endTimestamp: void 0
330
+ });
331
+ }
332
+ let uiResource;
333
+ try {
334
+ uiResource = await adapter.resolveMcpAppResource({ resourceUri, toolCallId });
335
+ } catch (error) {
336
+ console.warn(`[forge-ai-mcp-app] Failed to resolve UI resource for tool call ${toolCallId}:`, error);
337
+ return;
338
+ }
339
+ if (!uiResource || adapter !== this.#adapter || generation !== this.#threadGeneration) {
340
+ return;
341
+ }
342
+ if (this.#messageStateController.getToolCall(toolCallId)) {
343
+ this.#messageStateController.updateToolCallInResponse(toolCallId, { uiResource });
344
+ } else {
345
+ this.#pendingUiResources.set(toolCallId, uiResource);
346
+ }
347
+ }
348
+ #takePendingUiResource(toolCallId) {
349
+ const uiResource = this.#pendingUiResources.get(toolCallId);
350
+ if (!uiResource) {
351
+ return {};
352
+ }
353
+ this.#pendingUiResources.delete(toolCallId);
354
+ return { uiResource };
355
+ }
277
356
  async #sendToolResult(toolCallId, result) {
278
357
  const toolCall = this.#messageStateController.getToolCall(toolCallId);
279
358
  if (!toolCall || !this.#adapter) {
@@ -366,6 +445,10 @@ class ChatbotCoreController {
366
445
  return this.#messageStateController.getToolCall(id);
367
446
  }
368
447
  clearMessages() {
448
+ this.#threadGeneration++;
449
+ this.#mcpAppToolCallIds.clear();
450
+ this.#presumedCompleteToolCallIds.clear();
451
+ this.#pendingUiResources.clear();
369
452
  this.#messageStateController.clearMessages();
370
453
  this.#fileUploadManager.clear();
371
454
  }
@@ -373,13 +456,25 @@ class ChatbotCoreController {
373
456
  return this.#messageStateController.getMessages();
374
457
  }
375
458
  setMessages(messages) {
459
+ this.#threadGeneration++;
460
+ this.#mcpAppToolCallIds.clear();
461
+ this.#presumedCompleteToolCallIds.clear();
462
+ this.#pendingUiResources.clear();
376
463
  this.#messageStateController.setMessages(messages);
377
464
  }
378
465
  updateMessageContent(id, content) {
379
466
  this.#messageStateController.updateMessageContent(id, content);
380
467
  }
468
+ /** Tool call ids rendered at or after `index` in {@link messageItems}. */
469
+ getToolCallIdsFrom(index) {
470
+ return this.#messageStateController.getToolCallIdsFrom(index);
471
+ }
381
472
  removeMessageItemsFrom(index) {
382
- this.#messageStateController.removeMessageItemsFrom(index);
473
+ for (const id of this.#messageStateController.removeMessageItemsFrom(index)) {
474
+ this.#mcpAppToolCallIds.delete(id);
475
+ this.#presumedCompleteToolCallIds.delete(id);
476
+ this.#pendingUiResources.delete(id);
477
+ }
383
478
  }
384
479
  setResponseFeedback(responseId, feedback) {
385
480
  this.#messageStateController.setResponseFeedback(responseId, feedback);
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ export * from './ai-agent-info';
5
5
  export * from './ai-agent-selector';
6
6
  export * from './ai-tool-call-indicator';
7
7
  export * from './ai-artifact';
8
+ export * from './ai-artifact-card';
9
+ export * from './ai-artifact-card-group';
8
10
  export * from './ai-attachment';
9
11
  export * from './ai-button';
10
12
  export * from './ai-chain-of-thought';
@@ -40,6 +42,7 @@ export * from './ai-response-message';
40
42
  export * from './ai-sidebar';
41
43
  export * from './ai-sidebar-chat';
42
44
  export * from './ai-slash-command-menu';
45
+ export * from './ai-steps';
43
46
  export * from './ai-suggestions';
44
47
  export * from './ai-thinking-indicator';
45
48
  export * from './ai-threads';