@tylertech/forge-ai 0.15.2 → 0.15.3-dev.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 (88) hide show
  1. package/custom-elements.json +4501 -1781
  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.d.ts +2 -2
  13. package/dist/ai-assistant-response/ai-assistant-response.mjs +26 -2
  14. package/dist/ai-canvas/ai-canvas.d.ts +43 -0
  15. package/dist/ai-canvas/ai-canvas.mjs +95 -0
  16. package/dist/ai-canvas/ai-canvas.scss.mjs +4 -0
  17. package/dist/ai-canvas/index.d.ts +1 -0
  18. package/dist/ai-canvas/index.mjs +5 -0
  19. package/dist/ai-chatbot/ag-ui-adapter.d.ts +2 -2
  20. package/dist/ai-chatbot/ag-ui-adapter.mjs +1 -1
  21. package/dist/ai-chatbot/agent-adapter.d.ts +111 -4
  22. package/dist/ai-chatbot/agent-adapter.mjs +22 -0
  23. package/dist/ai-chatbot/agent-runner.d.ts +2 -2
  24. package/dist/ai-chatbot/ai-chatbot-base.d.ts +29 -2
  25. package/dist/ai-chatbot/ai-chatbot-base.mjs +147 -3
  26. package/dist/ai-chatbot/ai-chatbot-tool-call.d.ts +2 -2
  27. package/dist/ai-chatbot/ai-chatbot-tool-call.mjs +88 -10
  28. package/dist/ai-chatbot/ai-chatbot.d.ts +24 -1
  29. package/dist/ai-chatbot/ai-chatbot.mjs +102 -2
  30. package/dist/ai-chatbot/ai-chatbot.scss.mjs +1 -1
  31. package/dist/ai-chatbot/create-tool-renderer.d.ts +5 -5
  32. package/dist/ai-chatbot/index.d.ts +3 -2
  33. package/dist/ai-chatbot/index.mjs +3 -0
  34. package/dist/ai-chatbot/message-state-controller.d.ts +10 -1
  35. package/dist/ai-chatbot/message-state-controller.mjs +78 -20
  36. package/dist/ai-chatbot/tool-canvas.d.ts +60 -0
  37. package/dist/ai-chatbot/tool-canvas.mjs +128 -0
  38. package/dist/ai-chatbot/types.d.ts +325 -15
  39. package/dist/ai-chatbot/utils.d.ts +8 -3
  40. package/dist/ai-chatbot/utils.mjs +7 -1
  41. package/dist/ai-chatbot-launcher/ai-chatbot-launcher.d.ts +40 -2
  42. package/dist/ai-chatbot-launcher/ai-chatbot-launcher.mjs +224 -7
  43. package/dist/ai-chatbot-launcher/ai-chatbot-launcher.scss.mjs +1 -1
  44. package/dist/ai-embedded-chat/ai-embedded-chat.mjs +1 -0
  45. package/dist/ai-message-thread/ai-message-thread.d.ts +3 -2
  46. package/dist/ai-message-thread/ai-message-thread.mjs +36 -3
  47. package/dist/ai-steps/ai-steps.d.ts +67 -0
  48. package/dist/ai-steps/ai-steps.mjs +163 -0
  49. package/dist/ai-steps/ai-steps.scss.mjs +4 -0
  50. package/dist/ai-steps/index.d.ts +1 -0
  51. package/dist/ai-steps/index.mjs +5 -0
  52. package/dist/ai-tool-call-indicator/ai-tool-call-indicator.d.ts +2 -2
  53. package/dist/ai-tool-call-indicator/ai-tool-call-indicator.mjs +1 -1
  54. package/dist/core/canvas-controller.d.ts +81 -0
  55. package/dist/core/canvas-controller.mjs +221 -0
  56. package/dist/core/chatbot-core-controller.d.ts +4 -2
  57. package/dist/core/chatbot-core-controller.mjs +121 -4
  58. package/dist/core/index.d.ts +2 -1
  59. package/dist/core/index.mjs +2 -0
  60. package/dist/core/resize-controller.d.ts +5 -0
  61. package/dist/core/resize-controller.mjs +7 -5
  62. package/dist/index.d.ts +4 -0
  63. package/dist/index.mjs +15 -0
  64. package/dist/tools/ai-data-table/ai-data-table-definition.mjs +1 -0
  65. package/dist/tools/ai-mcp-app/ai-mcp-app.d.ts +70 -0
  66. package/dist/tools/ai-mcp-app/ai-mcp-app.mjs +589 -0
  67. package/dist/tools/ai-mcp-app/ai-mcp-app.scss.mjs +4 -0
  68. package/dist/tools/ai-mcp-app/index.d.ts +3 -0
  69. package/dist/tools/ai-mcp-app/index.mjs +6 -0
  70. package/dist/tools/ai-mcp-app/mcp-app-bridge-controller.d.ts +60 -0
  71. package/dist/tools/ai-mcp-app/mcp-app-bridge-controller.mjs +147 -0
  72. package/dist/tools/ai-mcp-app/mcp-app-bridge-types.d.ts +124 -0
  73. package/dist/tools/ai-mcp-app/mcp-app-bridge.d.ts +41 -0
  74. package/dist/tools/ai-mcp-app/mcp-app-bridge.mjs +293 -0
  75. package/dist/tools/ai-mcp-app/mcp-app-context.d.ts +46 -0
  76. package/dist/tools/ai-mcp-app/mcp-app-context.mjs +5 -0
  77. package/dist/tools/ai-steps/ai-steps-definition.d.ts +2 -0
  78. package/dist/tools/ai-steps/ai-steps-definition.mjs +51 -0
  79. package/dist/tools/ai-steps/ai-steps.d.ts +28 -0
  80. package/dist/tools/ai-steps/ai-steps.mjs +49 -0
  81. package/dist/tools/ai-steps/ai-steps.scss.mjs +4 -0
  82. package/dist/tools/ai-steps/index.d.ts +2 -0
  83. package/dist/tools/ai-steps/index.mjs +6 -0
  84. package/dist/tools/index.d.ts +1 -0
  85. package/dist/tools/index.mjs +5 -1
  86. package/dist/utils/inert-outside.d.ts +7 -0
  87. package/dist/utils/inert-outside.mjs +28 -0
  88. package/package.json +4 -1
@@ -0,0 +1,221 @@
1
+ import { ContextProvider } from "@lit/context";
2
+ import { toolCanvasHostContext, resolveToolCanvasTitle, createToolCanvas } from "../ai-chatbot/tool-canvas.mjs";
3
+ import { shouldShowToolRenderer } from "../ai-chatbot/utils.mjs";
4
+ class CanvasController {
5
+ constructor(host, config) {
6
+ this.#autoOpenedToolCallIds = /* @__PURE__ */ new Set();
7
+ this.#isOpen = false;
8
+ this.#content = null;
9
+ this.handleOpenRequest = (evt) => {
10
+ evt.stopPropagation();
11
+ const toolCall = this.#findToolCall(evt.detail.toolCallId);
12
+ const toolDefinition = this.#config.getTools().get(evt.detail.toolName);
13
+ if (!toolCall || !toolDefinition) {
14
+ return;
15
+ }
16
+ evt.detail.handled = this.#openToolCall({ toolCall, toolDefinition, title: evt.detail.title });
17
+ };
18
+ this.handleCloseRequest = (evt) => {
19
+ evt.stopPropagation();
20
+ evt.detail.handled = this.#closeToolCall(evt.detail.toolCallId);
21
+ };
22
+ this.#host = host;
23
+ this.#config = config;
24
+ host.addController(this);
25
+ this.#provider = new ContextProvider(host, {
26
+ context: toolCanvasHostContext,
27
+ initialValue: { presentation: "inline-expandable" }
28
+ });
29
+ }
30
+ #host;
31
+ #config;
32
+ #provider;
33
+ #autoOpenedToolCallIds;
34
+ #isOpen;
35
+ #content;
36
+ hostConnected() {
37
+ }
38
+ get isOpen() {
39
+ return this.#isOpen;
40
+ }
41
+ get content() {
42
+ return this.#content;
43
+ }
44
+ open(options = {}) {
45
+ return this.#commitOpen({ id: options.id, title: options.title, node: options.content });
46
+ }
47
+ /** Closes the canvas. Returns false if it was not open or the close event was prevented. */
48
+ close() {
49
+ if (!this.#isOpen) {
50
+ return false;
51
+ }
52
+ const event = this.#config.dispatchHostEvent({ type: this.#config.closeEvent, cancelable: true });
53
+ if (event.defaultPrevented) {
54
+ return false;
55
+ }
56
+ this.#hide();
57
+ return true;
58
+ }
59
+ /** Closes without asking, for when the host can no longer show the canvas (e.g. it left its conversation view). */
60
+ forceClose() {
61
+ if (!this.#isOpen) {
62
+ return;
63
+ }
64
+ this.#hide();
65
+ this.#config.dispatchHostEvent({ type: this.#config.closeEvent });
66
+ }
67
+ /** Forgets the canvas entirely, for when the thread it belonged to is gone. */
68
+ reset() {
69
+ this.forceClose();
70
+ this.#content = null;
71
+ this.#autoOpenedToolCallIds.clear();
72
+ this.#host.requestUpdate();
73
+ }
74
+ /** Tool calls restored from history are marked as seen so loading a thread never auto-opens the canvas. */
75
+ markSeen(messages) {
76
+ for (const message of messages) {
77
+ for (const toolCall of message.toolCalls ?? []) {
78
+ this.#autoOpenedToolCallIds.add(toolCall.id);
79
+ }
80
+ }
81
+ }
82
+ /** The tool call object is replaced on every args or status update, so open content follows it by id. */
83
+ syncToolCall() {
84
+ const content = this.#content;
85
+ if (!this.#isOpen || !content?.toolCall || !content.toolDefinition) {
86
+ return;
87
+ }
88
+ const latest = this.#findToolCall(content.toolCall.id);
89
+ if (!latest || latest === content.toolCall) {
90
+ return;
91
+ }
92
+ const { toolDefinition } = content;
93
+ const renderer = toolDefinition.canvas?.renderer ?? toolDefinition.renderer;
94
+ let node = content.node;
95
+ if (renderer?.elementTag && node) {
96
+ node.toolCall = latest;
97
+ } else {
98
+ node = this.#createRenderer(latest, toolDefinition);
99
+ }
100
+ this.#content = {
101
+ ...content,
102
+ node,
103
+ toolCall: latest,
104
+ title: content.titleOverride ?? resolveToolCanvasTitle(latest, toolDefinition)
105
+ };
106
+ }
107
+ /** Opens tools configured with `canvas.open: 'auto'` once their renderer shows. Call after render, not during it. */
108
+ autoOpenPending() {
109
+ const tools = this.#config.getTools();
110
+ if (![...tools.values()].some((tool) => tool.canvas?.open === "auto")) {
111
+ return;
112
+ }
113
+ for (const toolCall of this.#config.getToolCalls()) {
114
+ const toolDefinition = tools.get(toolCall.name);
115
+ if (toolDefinition?.canvas?.open !== "auto" || this.#autoOpenedToolCallIds.has(toolCall.id) || !shouldShowToolRenderer(toolCall, toolDefinition)) {
116
+ continue;
117
+ }
118
+ this.#autoOpenedToolCallIds.add(toolCall.id);
119
+ this.#openToolCall({ toolCall, toolDefinition, reason: "auto" });
120
+ }
121
+ }
122
+ #findToolCall(id) {
123
+ return this.#config.getToolCalls().find((toolCall) => toolCall.id === id);
124
+ }
125
+ #commitOpen(content, reason = "request") {
126
+ if (this.#config.canOpen && !this.#config.canOpen()) {
127
+ return false;
128
+ }
129
+ const event = this.#config.dispatchHostEvent({
130
+ type: this.#config.openEvent,
131
+ detail: { id: content.id, title: content.title, toolCall: content.toolCall },
132
+ cancelable: true
133
+ });
134
+ if (event.defaultPrevented) {
135
+ return false;
136
+ }
137
+ this.#content = content;
138
+ if (!this.#isOpen) {
139
+ this.#isOpen = true;
140
+ this.#config.onOpen?.({ reason });
141
+ }
142
+ this.#syncProvider();
143
+ this.#host.requestUpdate();
144
+ return true;
145
+ }
146
+ // Content stays mounted while the host collapses, then is released so hidden iframes and timers stop.
147
+ #hide() {
148
+ this.#isOpen = false;
149
+ this.#config.onClose?.();
150
+ this.#syncProvider();
151
+ this.#host.requestUpdate();
152
+ void this.#releaseAfterCollapse();
153
+ }
154
+ async #releaseAfterCollapse() {
155
+ const content = this.#content;
156
+ await this.#host.updateComplete;
157
+ await this.#config.collapsed?.();
158
+ if (!this.#isOpen && this.#content === content) {
159
+ this.#content = null;
160
+ this.#host.requestUpdate();
161
+ }
162
+ }
163
+ #syncProvider() {
164
+ this.#provider.setValue({
165
+ presentation: "inline-expandable",
166
+ openToolCallId: this.#isOpen ? this.#content?.toolCall?.id : void 0
167
+ });
168
+ }
169
+ #closeToolCall(toolCallId) {
170
+ if (this.#content?.toolCall?.id !== toolCallId) {
171
+ return false;
172
+ }
173
+ return this.close();
174
+ }
175
+ #openToolCall({
176
+ toolCall,
177
+ toolDefinition,
178
+ title,
179
+ reason
180
+ }) {
181
+ if (this.#isOpen && this.#content?.toolCall?.id === toolCall.id) {
182
+ this.#config.onRefocus?.();
183
+ return true;
184
+ }
185
+ return this.#commitOpen(
186
+ {
187
+ id: toolCall.id,
188
+ title: title ?? resolveToolCanvasTitle(toolCall, toolDefinition),
189
+ node: this.#createRenderer(toolCall, toolDefinition),
190
+ toolCall,
191
+ toolDefinition,
192
+ titleOverride: title
193
+ },
194
+ reason
195
+ );
196
+ }
197
+ #createControls(toolCall, toolDefinition) {
198
+ return createToolCanvas({
199
+ available: true,
200
+ // This instance only exists to be shown in the canvas, and is created before the canvas finishes opening.
201
+ isOpen: () => true,
202
+ open: (options) => this.#openToolCall({ toolCall, toolDefinition, title: options?.title }),
203
+ close: () => this.#closeToolCall(toolCall.id)
204
+ });
205
+ }
206
+ #createRenderer(toolCall, toolDefinition) {
207
+ const renderer = toolDefinition.canvas?.renderer ?? toolDefinition.renderer;
208
+ const canvas = this.#createControls(toolCall, toolDefinition);
209
+ if (renderer?.elementTag) {
210
+ const element = document.createElement(renderer.elementTag);
211
+ element.toolCall = toolCall;
212
+ element.presentation = "canvas";
213
+ element.canvas = canvas;
214
+ return element;
215
+ }
216
+ return renderer?.render?.(toolCall, { presentation: "canvas", canvas });
217
+ }
218
+ }
219
+ export {
220
+ CanvasController
221
+ };
@@ -1,6 +1,6 @@
1
1
  import { ReactiveController, ReactiveControllerHost } from 'lit';
2
2
  import { AgentAdapter } from '../ai-chatbot/agent-adapter.js';
3
- import { ChatMessage, ClientMessageInput, FileAttachment, FileUploadCallbacks, MessageItem, ThreadState, ToolCall, ToolDefinition } from '../ai-chatbot/types.js';
3
+ import { ChatMessage, ClientMessageInput, FileAttachment, FileUploadCallbacks, MessageItem, ThreadState, ToolCall, AnyToolDefinition } from '../ai-chatbot/types.js';
4
4
  export interface ChatbotCoreCallbacks {
5
5
  onRequestUpdate: () => void;
6
6
  onScrollToBottom: () => void;
@@ -20,7 +20,7 @@ export declare class ChatbotCoreController implements ReactiveController {
20
20
  get isStreaming(): boolean;
21
21
  get isUploading(): boolean;
22
22
  get hasMessages(): boolean;
23
- get tools(): Map<string, ToolDefinition>;
23
+ get tools(): Map<string, AnyToolDefinition>;
24
24
  get pendingAttachments(): FileAttachment[];
25
25
  get completedAttachments(): FileAttachment[];
26
26
  sendMessage(config: {
@@ -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,8 @@ 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 NO_UI_RESOURCE = { uiResource: void 0, uiResourceStatus: void 0 };
6
+ const UNSETTLED_TOOL_CALL_STATUSES = ["pending", "parsing", "executing"];
5
7
  class ChatbotCoreController {
6
8
  #host;
7
9
  #callbacks;
@@ -10,6 +12,18 @@ class ChatbotCoreController {
10
12
  #adapterSubscriptions;
11
13
  #adapter;
12
14
  #executingToolHandlers = 0;
15
+ /** Tool calls announced by an `mcp-ui-resource` event; they only settle via a tool result. */
16
+ #mcpAppToolCallIds = /* @__PURE__ */ new Set();
17
+ /** Agent tool calls marked complete on their `tool-call` chunk, before any result arrived. */
18
+ #presumedCompleteToolCallIds = /* @__PURE__ */ new Set();
19
+ /** UI resource fields (resolve status or resolved resource) whose tool call is not in state yet. */
20
+ #pendingUiResources = /* @__PURE__ */ new Map();
21
+ /**
22
+ * Token of the latest in-flight UI resource resolve per tool call. Removed when that resolve
23
+ * settles, or when the thread is cleared/replaced; a resolve whose token is no longer current
24
+ * is dropped, so the latest announcement wins and stale threads never get stamped.
25
+ */
26
+ #uiResourceResolves = /* @__PURE__ */ new Map();
13
27
  constructor(host, config) {
14
28
  this.#host = host;
15
29
  this.#callbacks = config.callbacks;
@@ -83,6 +97,7 @@ class ChatbotCoreController {
83
97
  this.#adapter.onToolCallEnd(this.#handleToolCallEnd.bind(this)),
84
98
  this.#adapter.onToolCall(this.#handleToolCall.bind(this)),
85
99
  this.#adapter.onToolCallResult(this.#handleToolCallResult.bind(this)),
100
+ this.#adapter.onMcpUiResource(this.#handleMcpUiResource.bind(this)),
86
101
  this.#adapter.onStepStarted(this.#handleStepStarted.bind(this)),
87
102
  this.#adapter.onStepFinished(this.#handleStepFinished.bind(this)),
88
103
  this.#adapter.onRunFinished(this.#handleRunFinished.bind(this)),
@@ -115,10 +130,29 @@ class ChatbotCoreController {
115
130
  #handleRunFinished() {
116
131
  this.#tryCompleteResponse();
117
132
  }
133
+ /**
134
+ * A run ended (finished, aborted, or errored) without delivering a result for an
135
+ * MCP-app tool call: mark it cancelled so the widget receives `tool-cancelled`.
136
+ */
137
+ #cancelUnsettledMcpAppToolCalls() {
138
+ for (const id of this.#mcpAppToolCallIds) {
139
+ const toolCall = this.#messageStateController.getToolCall(id);
140
+ if (toolCall && UNSETTLED_TOOL_CALL_STATUSES.includes(toolCall.status)) {
141
+ this.#messageStateController.updateToolCallInResponse(id, { status: "cancelled" });
142
+ }
143
+ }
144
+ this.#mcpAppToolCallIds.clear();
145
+ this.#presumedCompleteToolCallIds.clear();
146
+ }
147
+ /**
148
+ * Finalizes once the run is truly over. An executing client-tool handler or a running
149
+ * adapter means a continuation follows, so widgets stay open for it to settle (§2).
150
+ */
118
151
  #tryCompleteResponse() {
119
152
  if (this.#executingToolHandlers > 0 || this.#adapter?.isRunning) {
120
153
  return;
121
154
  }
155
+ this.#cancelUnsettledMcpAppToolCalls();
122
156
  this.#messageStateController.tryFinalizeResponse();
123
157
  const messages = this.getMessages();
124
158
  const lastMessage = messages[messages.length - 1];
@@ -134,7 +168,8 @@ class ChatbotCoreController {
134
168
  args: {},
135
169
  argsBuffer: "",
136
170
  status: "parsing",
137
- type: this.tools.has(event.name) ? "client" : "agent"
171
+ type: this.tools.has(event.name) ? "client" : "agent",
172
+ ...this.#takePendingUiResource(event.id)
138
173
  };
139
174
  this.#messageStateController.addToolCallToResponse(toolCall);
140
175
  const toolDef = this.tools.get(event.name);
@@ -195,7 +230,8 @@ class ChatbotCoreController {
195
230
  name: event.name,
196
231
  args: event.args,
197
232
  status: "executing",
198
- type: this.tools.has(event.name) ? "client" : "agent"
233
+ type: this.tools.has(event.name) ? "client" : "agent",
234
+ ...this.#takePendingUiResource(event.id)
199
235
  };
200
236
  this.#messageStateController.addToolCallToResponse(toolCall);
201
237
  }
@@ -211,7 +247,8 @@ class ChatbotCoreController {
211
247
  } else {
212
248
  this.#sendToolResult(event.id, this.#createToolResponse(event.name));
213
249
  }
214
- } else {
250
+ } else if (!this.#mcpAppToolCallIds.has(event.id)) {
251
+ this.#presumedCompleteToolCallIds.add(event.id);
215
252
  this.#messageStateController.updateToolCallInResponse(event.id, { status: "complete" });
216
253
  }
217
254
  }
@@ -249,6 +286,7 @@ class ChatbotCoreController {
249
286
  }
250
287
  }
251
288
  #handleError(event) {
289
+ this.#cancelUnsettledMcpAppToolCalls();
252
290
  const errorMessage = {
253
291
  id: generateId(),
254
292
  role: "assistant",
@@ -261,6 +299,7 @@ class ChatbotCoreController {
261
299
  this.#callbacks.onDispatchEvent("forge-ai-chatbot-error", { error: event.message });
262
300
  }
263
301
  #handleRunAborted() {
302
+ this.#cancelUnsettledMcpAppToolCalls();
264
303
  this.#messageStateController.tryFinalizeResponse();
265
304
  const id = this.#messageStateController.addClientMessage({ content: "Run cancelled" });
266
305
  const abortMessage = this.#messageStateController.getMessage(id);
@@ -272,8 +311,69 @@ class ChatbotCoreController {
272
311
  this.#callbacks.onRequestUpdate();
273
312
  }
274
313
  #handleToolCallResult(event) {
314
+ this.#presumedCompleteToolCallIds.delete(event.toolCallId);
275
315
  this.#messageStateController.completeToolCallInResponse(event.toolCallId, event.result, event);
276
316
  }
317
+ /**
318
+ * Marks the tool call as a loading MCP app, then resolves and stamps the UI resource.
319
+ * Works whether the event arrives before or after the tool call's chunks, mid-run, after
320
+ * the run ended, or after `setMessages` for tool calls restored from history. A resolve
321
+ * superseded by a newer announcement, or outliving its thread (cleared/replaced), is
322
+ * dropped; one that outlives its adapter falls back to a plain tool call.
323
+ */
324
+ async #handleMcpUiResource(event) {
325
+ const adapter = this.#adapter;
326
+ if (!adapter?.resolveMcpAppResource) {
327
+ return;
328
+ }
329
+ const { toolCallId, resourceUri } = event;
330
+ const token = Symbol(toolCallId);
331
+ this.#uiResourceResolves.set(toolCallId, token);
332
+ this.#mcpAppToolCallIds.add(toolCallId);
333
+ if (this.#presumedCompleteToolCallIds.delete(toolCallId)) {
334
+ this.#messageStateController.updateToolCallInResponse(toolCallId, {
335
+ status: "executing",
336
+ endTimestamp: void 0
337
+ });
338
+ }
339
+ this.#stampUiResource(toolCallId, { uiResourceStatus: "loading" });
340
+ const fields = await this.#resolveUiResource(adapter, { resourceUri, toolCallId });
341
+ if (this.#uiResourceResolves.get(toolCallId) !== token) {
342
+ return;
343
+ }
344
+ this.#uiResourceResolves.delete(toolCallId);
345
+ this.#stampUiResource(toolCallId, adapter === this.#adapter ? fields : NO_UI_RESOURCE);
346
+ }
347
+ /**
348
+ * A resolved resource replaces the status; `undefined` (MCP apps unavailable) clears both so
349
+ * the call renders as a plain tool call; a throw marks the widget as failed.
350
+ */
351
+ async #resolveUiResource(adapter, params) {
352
+ try {
353
+ const uiResource = await adapter.resolveMcpAppResource?.(params);
354
+ return uiResource ? { uiResource, uiResourceStatus: void 0 } : NO_UI_RESOURCE;
355
+ } catch (error) {
356
+ console.warn(`[forge-ai-mcp-app] Failed to resolve UI resource for tool call ${params.toolCallId}:`, error);
357
+ return { uiResource: void 0, uiResourceStatus: "error" };
358
+ }
359
+ }
360
+ #stampUiResource(toolCallId, fields) {
361
+ if (this.#messageStateController.getToolCall(toolCallId)) {
362
+ this.#messageStateController.updateToolCallInResponse(toolCallId, fields);
363
+ } else if (fields.uiResource || fields.uiResourceStatus) {
364
+ this.#pendingUiResources.set(toolCallId, fields);
365
+ } else {
366
+ this.#pendingUiResources.delete(toolCallId);
367
+ }
368
+ }
369
+ #takePendingUiResource(toolCallId) {
370
+ const fields = this.#pendingUiResources.get(toolCallId);
371
+ if (!fields) {
372
+ return {};
373
+ }
374
+ this.#pendingUiResources.delete(toolCallId);
375
+ return fields;
376
+ }
277
377
  async #sendToolResult(toolCallId, result) {
278
378
  const toolCall = this.#messageStateController.getToolCall(toolCallId);
279
379
  if (!toolCall || !this.#adapter) {
@@ -366,6 +466,10 @@ class ChatbotCoreController {
366
466
  return this.#messageStateController.getToolCall(id);
367
467
  }
368
468
  clearMessages() {
469
+ this.#mcpAppToolCallIds.clear();
470
+ this.#presumedCompleteToolCallIds.clear();
471
+ this.#pendingUiResources.clear();
472
+ this.#uiResourceResolves.clear();
369
473
  this.#messageStateController.clearMessages();
370
474
  this.#fileUploadManager.clear();
371
475
  }
@@ -373,13 +477,26 @@ class ChatbotCoreController {
373
477
  return this.#messageStateController.getMessages();
374
478
  }
375
479
  setMessages(messages) {
480
+ this.#mcpAppToolCallIds.clear();
481
+ this.#presumedCompleteToolCallIds.clear();
482
+ this.#pendingUiResources.clear();
483
+ this.#uiResourceResolves.clear();
376
484
  this.#messageStateController.setMessages(messages);
377
485
  }
378
486
  updateMessageContent(id, content) {
379
487
  this.#messageStateController.updateMessageContent(id, content);
380
488
  }
489
+ /** Tool call ids rendered at or after `index` in {@link messageItems}. */
490
+ getToolCallIdsFrom(index) {
491
+ return this.#messageStateController.getToolCallIdsFrom(index);
492
+ }
381
493
  removeMessageItemsFrom(index) {
382
- this.#messageStateController.removeMessageItemsFrom(index);
494
+ for (const id of this.#messageStateController.removeMessageItemsFrom(index)) {
495
+ this.#mcpAppToolCallIds.delete(id);
496
+ this.#presumedCompleteToolCallIds.delete(id);
497
+ this.#pendingUiResources.delete(id);
498
+ this.#uiResourceResolves.delete(id);
499
+ }
383
500
  }
384
501
  setResponseFeedback(responseId, feedback) {
385
502
  this.#messageStateController.setResponseFeedback(responseId, feedback);
@@ -1,3 +1,4 @@
1
+ export { CanvasController, type CanvasControllerConfig, type CanvasContent, type CanvasOpenEventData, type OpenCanvasOptions } from './canvas-controller.js';
1
2
  export { DragController, type DragControllerOptions } from './drag-controller.js';
2
- export { ResizeController, type ResizeControllerOptions } from './resize-controller.js';
3
+ export { ResizeController, type ResizeControllerOptions, type ResizeGrowDirection } from './resize-controller.js';
3
4
  export { ChatbotCoreController, type ChatbotCoreCallbacks, type ChatbotCoreControllerConfig } from './chatbot-core-controller.js';
@@ -1,7 +1,9 @@
1
+ import { CanvasController } from "./canvas-controller.mjs";
1
2
  import { DragController } from "./drag-controller.mjs";
2
3
  import { ResizeController } from "./resize-controller.mjs";
3
4
  import { ChatbotCoreController } from "./chatbot-core-controller.mjs";
4
5
  export {
6
+ CanvasController,
5
7
  ChatbotCoreController,
6
8
  DragController,
7
9
  ResizeController
@@ -1,5 +1,6 @@
1
1
  import { ReactiveController, ReactiveControllerHost } from 'lit';
2
2
  import { Ref } from 'lit/directives/ref.js';
3
+ export type ResizeGrowDirection = 'left' | 'right';
3
4
  export interface ResizeControllerOptions {
4
5
  targetElementRef: Ref<HTMLElement>;
5
6
  resizeHandleRef: Ref<HTMLElement>;
@@ -7,6 +8,10 @@ export interface ResizeControllerOptions {
7
8
  maxWidth?: number;
8
9
  defaultWidth?: number;
9
10
  keyboardStep?: number;
11
+ /** Screen direction the handle moves to make the width larger. Defaults to `'left'`, for a panel docked to the right. */
12
+ growDirection?: () => ResizeGrowDirection;
13
+ /** Widest the target may be given its container. Defaults to the window width minus a small buffer. */
14
+ getAvailableWidth?: () => number;
10
15
  onResize?: (width: number) => void;
11
16
  onResizeStart?: () => void;
12
17
  onResizeEnd?: () => void;
@@ -24,13 +24,14 @@ class ResizeController {
24
24
  };
25
25
  this.handleKeyDown = (event) => {
26
26
  const step = this.#options.keyboardStep;
27
+ const growSign = this.#options.growDirection() === "left" ? 1 : -1;
27
28
  let delta = 0;
28
29
  switch (event.key) {
29
30
  case "ArrowLeft":
30
- delta = step;
31
+ delta = step * growSign;
31
32
  break;
32
33
  case "ArrowRight":
33
- delta = -step;
34
+ delta = -step * growSign;
34
35
  break;
35
36
  default:
36
37
  return;
@@ -46,7 +47,7 @@ class ResizeController {
46
47
  return;
47
48
  }
48
49
  event.preventDefault();
49
- const deltaX = this.#dragStartX - event.clientX;
50
+ const deltaX = this.#options.growDirection() === "left" ? this.#dragStartX - event.clientX : event.clientX - this.#dragStartX;
50
51
  const newWidth = this.#dragStartWidth + deltaX;
51
52
  this.#currentWidth = this.#constrainWidth(newWidth);
52
53
  this.#applyWidth();
@@ -72,6 +73,8 @@ class ResizeController {
72
73
  maxWidth: 800,
73
74
  defaultWidth: 420,
74
75
  keyboardStep: 20,
76
+ growDirection: () => "left",
77
+ getAvailableWidth: () => window.innerWidth - RESIZE_SCREEN_BUFFER,
75
78
  onResize: () => {
76
79
  },
77
80
  onResizeStart: () => {
@@ -118,8 +121,7 @@ class ResizeController {
118
121
  }
119
122
  }
120
123
  #constrainWidth(width) {
121
- const availableWidth = window.innerWidth - RESIZE_SCREEN_BUFFER;
122
- const effectiveMaxWidth = Math.min(this.#options.maxWidth, availableWidth);
124
+ const effectiveMaxWidth = Math.min(this.#options.maxWidth, this.#options.getAvailableWidth());
123
125
  return Math.max(this.#options.minWidth, Math.min(width, effectiveMaxWidth));
124
126
  }
125
127
  #applyWidth() {
package/dist/index.d.ts CHANGED
@@ -5,8 +5,11 @@ 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';
12
+ export * from './ai-canvas';
10
13
  export * from './ai-chain-of-thought';
11
14
  export * from './ai-chat-header';
12
15
  export * from './ai-chat-interface';
@@ -40,6 +43,7 @@ export * from './ai-response-message';
40
43
  export * from './ai-sidebar';
41
44
  export * from './ai-sidebar-chat';
42
45
  export * from './ai-slash-command-menu';
46
+ export * from './ai-steps';
43
47
  export * from './ai-suggestions';
44
48
  export * from './ai-thinking-indicator';
45
49
  export * from './ai-threads';
package/dist/index.mjs CHANGED
@@ -5,8 +5,11 @@ import { AiAgentInfoComponent, AiAgentInfoComponentTagName } from "./ai-agent-in
5
5
  import { AiAgentSelectorComponent, AiAgentSelectorComponentTagName } from "./ai-agent-selector/ai-agent-selector.mjs";
6
6
  import { AiToolCallIndicatorComponent, AiToolCallIndicatorComponentTagName } from "./ai-tool-call-indicator/ai-tool-call-indicator.mjs";
7
7
  import { AiArtifactComponent, AiArtifactComponentTagName } from "./ai-artifact/ai-artifact.mjs";
8
+ import { AiArtifactCardComponent, AiArtifactCardComponentTagName } from "./ai-artifact-card/ai-artifact-card.mjs";
9
+ import { AiArtifactCardGroupComponent, AiArtifactCardGroupComponentTagName } from "./ai-artifact-card-group/ai-artifact-card-group.mjs";
8
10
  import { AiAttachmentComponent, AiAttachmentComponentTagName } from "./ai-attachment/ai-attachment.mjs";
9
11
  import { AiButtonComponent, AiButtonComponentTagName } from "./ai-button/ai-button.mjs";
12
+ import { AiCanvasComponent, AiCanvasComponentTagName } from "./ai-canvas/ai-canvas.mjs";
10
13
  import { AiChainOfThoughtComponent, AiChainOfThoughtComponentTagName } from "./ai-chain-of-thought/ai-chain-of-thought.mjs";
11
14
  import { ThoughtBaseComponent, ThoughtBaseComponentTagName } from "./ai-chain-of-thought/thought-base/thought-base.mjs";
12
15
  import { ThoughtSearchResultComponent, ThoughtSearchResultComponentTagName } from "./ai-chain-of-thought/thought-search-result/thought-search-result.mjs";
@@ -23,6 +26,7 @@ import { HttpAgentWithCredentials } from "./ai-chatbot/http-agent-with-credentia
23
26
  import { EventEmitter, SubscriptionManager } from "./ai-chatbot/event-emitter.mjs";
24
27
  import { AgentRunner } from "./ai-chatbot/agent-runner.mjs";
25
28
  import { createToolRenderer } from "./ai-chatbot/create-tool-renderer.mjs";
29
+ import { requestCanvasClose, requestCanvasOpen } from "./ai-chatbot/tool-canvas.mjs";
26
30
  import { MessageStateController } from "./ai-chatbot/message-state-controller.mjs";
27
31
  import { FileUploadManager } from "./ai-chatbot/file-upload-manager.mjs";
28
32
  import { MarkdownStreamController } from "./ai-chatbot/markdown-stream-controller.mjs";
@@ -61,6 +65,7 @@ import { AiResponseMessageComponent, AiResponseMessageComponentTagName } from ".
61
65
  import { AiSidebarComponent, AiSidebarComponentTagName } from "./ai-sidebar/ai-sidebar.mjs";
62
66
  import { AiSidebarChatComponent, AiSidebarChatComponentTagName } from "./ai-sidebar-chat/ai-sidebar-chat.mjs";
63
67
  import { AiSlashCommandMenuComponent, AiSlashCommandMenuComponentTagName } from "./ai-slash-command-menu/ai-slash-command-menu.mjs";
68
+ import { AiStepsComponent, AiStepsComponentTagName } from "./ai-steps/ai-steps.mjs";
64
69
  import { AiSuggestionsComponent, AiSuggestionsComponentTagName } from "./ai-suggestions/ai-suggestions.mjs";
65
70
  import { AiThinkingIndicatorComponent, AiThinkingIndicatorComponentTagName } from "./ai-thinking-indicator/ai-thinking-indicator.mjs";
66
71
  import { AiThreadsComponent, AiThreadsComponentTagName } from "./ai-threads/ai-threads.mjs";
@@ -77,6 +82,10 @@ export {
77
82
  AiAgentInfoComponentTagName,
78
83
  AiAgentSelectorComponent,
79
84
  AiAgentSelectorComponentTagName,
85
+ AiArtifactCardComponent,
86
+ AiArtifactCardComponentTagName,
87
+ AiArtifactCardGroupComponent,
88
+ AiArtifactCardGroupComponentTagName,
80
89
  AiArtifactComponent,
81
90
  AiArtifactComponentTagName,
82
91
  AiAssistantResponseComponent,
@@ -85,6 +94,8 @@ export {
85
94
  AiAttachmentComponentTagName,
86
95
  AiButtonComponent,
87
96
  AiButtonComponentTagName,
97
+ AiCanvasComponent,
98
+ AiCanvasComponentTagName,
88
99
  AiChainOfThoughtComponent,
89
100
  AiChainOfThoughtComponentTagName,
90
101
  AiChatHeaderComponent,
@@ -152,6 +163,8 @@ export {
152
163
  AiSidebarComponentTagName,
153
164
  AiSlashCommandMenuComponent,
154
165
  AiSlashCommandMenuComponentTagName,
166
+ AiStepsComponent,
167
+ AiStepsComponentTagName,
155
168
  AiSuggestionsComponent,
156
169
  AiSuggestionsComponentTagName,
157
170
  AiThinkingIndicatorComponent,
@@ -194,6 +207,8 @@ export {
194
207
  generateId,
195
208
  renderInlineMarkdown,
196
209
  renderMarkdown,
210
+ requestCanvasClose,
211
+ requestCanvasOpen,
197
212
  toRequestMessages,
198
213
  toRequestToolCall
199
214
  };
@@ -5,6 +5,7 @@ import "@ag-ui/client";
5
5
  import "uuid";
6
6
  import "../../utils/markdown.mjs";
7
7
  import { createToolRenderer } from "../../ai-chatbot/create-tool-renderer.mjs";
8
+ import "../../ai-chatbot/tool-canvas.mjs";
8
9
  const displayDataTableTool = {
9
10
  name: "displayDataTable",
10
11
  displayName: "Display Data Table",