@tylertech/forge-ai 0.15.1 → 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 (71) hide show
  1. package/custom-elements.json +8152 -6309
  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 +14 -4
  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-sidebar/ai-sidebar.d.ts +17 -6
  27. package/dist/ai-sidebar/ai-sidebar.mjs +78 -36
  28. package/dist/ai-sidebar/ai-sidebar.scss.mjs +1 -1
  29. package/dist/ai-sidebar/index.d.ts +1 -0
  30. package/dist/ai-sidebar-chat/ai-sidebar-chat.d.ts +14 -1
  31. package/dist/ai-sidebar-chat/ai-sidebar-chat.mjs +18 -14
  32. package/dist/ai-steps/ai-steps.d.ts +67 -0
  33. package/dist/ai-steps/ai-steps.mjs +163 -0
  34. package/dist/ai-steps/ai-steps.scss.mjs +4 -0
  35. package/dist/ai-steps/index.d.ts +1 -0
  36. package/dist/ai-steps/index.mjs +5 -0
  37. package/dist/ai-tool-call-indicator/ai-tool-call-indicator.mjs +1 -1
  38. package/dist/core/chatbot-core-controller.d.ts +2 -0
  39. package/dist/core/chatbot-core-controller.mjs +99 -4
  40. package/dist/core/resize-controller.d.ts +1 -0
  41. package/dist/core/resize-controller.mjs +11 -2
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.mjs +9 -0
  44. package/dist/tools/ai-mcp-app/ai-mcp-app.d.ts +69 -0
  45. package/dist/tools/ai-mcp-app/ai-mcp-app.mjs +574 -0
  46. package/dist/tools/ai-mcp-app/ai-mcp-app.scss.mjs +4 -0
  47. package/dist/tools/ai-mcp-app/index.d.ts +3 -0
  48. package/dist/tools/ai-mcp-app/index.mjs +6 -0
  49. package/dist/tools/ai-mcp-app/mcp-app-bridge-controller.d.ts +61 -0
  50. package/dist/tools/ai-mcp-app/mcp-app-bridge-controller.mjs +156 -0
  51. package/dist/tools/ai-mcp-app/mcp-app-bridge-types.d.ts +123 -0
  52. package/dist/tools/ai-mcp-app/mcp-app-bridge.d.ts +41 -0
  53. package/dist/tools/ai-mcp-app/mcp-app-bridge.mjs +293 -0
  54. package/dist/tools/ai-mcp-app/mcp-app-context.d.ts +46 -0
  55. package/dist/tools/ai-mcp-app/mcp-app-context.mjs +5 -0
  56. package/dist/tools/ai-steps/ai-steps-definition.d.ts +2 -0
  57. package/dist/tools/ai-steps/ai-steps-definition.mjs +50 -0
  58. package/dist/tools/ai-steps/ai-steps.d.ts +28 -0
  59. package/dist/tools/ai-steps/ai-steps.mjs +49 -0
  60. package/dist/tools/ai-steps/ai-steps.scss.mjs +4 -0
  61. package/dist/tools/ai-steps/index.d.ts +2 -0
  62. package/dist/tools/ai-steps/index.mjs +6 -0
  63. package/dist/tools/index.d.ts +1 -0
  64. package/dist/tools/index.mjs +5 -1
  65. package/dist/utils/index.d.ts +1 -0
  66. package/dist/utils/index.mjs +5 -1
  67. package/dist/utils/inert-outside.d.ts +7 -0
  68. package/dist/utils/inert-outside.mjs +28 -0
  69. package/dist/utils/storage-utils.d.ts +26 -0
  70. package/dist/utils/storage-utils.mjs +32 -0
  71. package/package.json +4 -1
@@ -1,5 +1,7 @@
1
+ import { provide } from "@lit/context";
1
2
  import { LitElement } from "lit";
2
3
  import { property } from "lit/decorators.js";
4
+ import { mcpAppHostContext } from "../tools/ai-mcp-app/mcp-app-context.mjs";
3
5
  import { ChatbotCoreController } from "../core/chatbot-core-controller.mjs";
4
6
  import { fileAttachmentsToContextItems, generateId, formatChatTranscript, downloadFile, createChatExportFilename } from "./utils.mjs";
5
7
  var __defProp = Object.defineProperty;
@@ -11,6 +13,34 @@ var __decorateClass = (decorators, target, key, kind) => {
11
13
  if (result) __defProp(target, key, result);
12
14
  return result;
13
15
  };
16
+ function mcpAppHostCapabilities(adapter) {
17
+ return {
18
+ openLinks: {},
19
+ logging: {},
20
+ message: { text: {} },
21
+ ...typeof adapter?.callMcpTool === "function" ? { serverTools: {} } : {},
22
+ ...typeof adapter?.updateMcpModelContext === "function" ? { updateModelContext: { text: {}, structuredContent: {} } } : {}
23
+ };
24
+ }
25
+ function mcpAppEnvironment() {
26
+ return {
27
+ availableDisplayModes: ["inline", "fullscreen", "pip"],
28
+ locale: navigator.language,
29
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
30
+ userAgent: navigator.userAgent,
31
+ platform: "web",
32
+ deviceCapabilities: {
33
+ touch: window.matchMedia("(pointer: coarse)").matches || navigator.maxTouchPoints > 0,
34
+ hover: window.matchMedia("(hover: hover)").matches
35
+ },
36
+ safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 }
37
+ };
38
+ }
39
+ function mcpMessageText(content) {
40
+ return content.filter(
41
+ (block) => !!block && typeof block === "object" && block.type === "text" && typeof block.text === "string"
42
+ ).map((block) => block.text).join("\n\n");
43
+ }
14
44
  class AiChatbotBase extends LitElement {
15
45
  constructor() {
16
46
  super(...arguments);
@@ -29,6 +59,74 @@ class AiChatbotBase extends LitElement {
29
59
  this.clearOption = "on";
30
60
  this.agents = [];
31
61
  this.contextItems = [];
62
+ this.#themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
63
+ this.#mcpAppViews = /* @__PURE__ */ new Map();
64
+ this._mcpAppHost = {
65
+ callTool: (params) => this._coreController?.adapter?.callMcpTool?.(params) ?? Promise.reject(new Error("tools/call is not supported by this host")),
66
+ readResource: (params) => this._coreController?.adapter?.readMcpResource?.(params) ?? Promise.reject(new Error("resources/read is not supported by this host")),
67
+ updateModelContext: (params) => this._coreController?.adapter?.updateMcpModelContext?.(params) ?? Promise.resolve(),
68
+ draftMessage: ({ content }) => this.#draftMcpAppMessage(content),
69
+ teardownView: ({ toolCallId }) => this._teardownMcpAppViews({ toolCallIds: [toolCallId] }),
70
+ registerView: (view) => {
71
+ this.#mcpAppViews.set(view.toolCallId, view);
72
+ return () => {
73
+ if (this.#mcpAppViews.get(view.toolCallId) === view) {
74
+ this.#mcpAppViews.delete(view.toolCallId);
75
+ }
76
+ };
77
+ },
78
+ hostContext: { theme: this.#effectiveTheme, ...mcpAppEnvironment() },
79
+ sandboxUrl: void 0,
80
+ hostCapabilities: mcpAppHostCapabilities(void 0)
81
+ };
82
+ this.#handleThemeChange = () => {
83
+ this.#refreshMcpAppHostContext();
84
+ };
85
+ }
86
+ #themeMediaQuery;
87
+ get #effectiveTheme() {
88
+ return this.mcpAppTheme ?? (this.#themeMediaQuery.matches ? "dark" : "light");
89
+ }
90
+ #mcpAppViews;
91
+ /**
92
+ * Places a widget's `ui/message` text in the prompt for the user to review and send. A
93
+ * widget never starts a model turn on its own. Refused when the message has no text.
94
+ */
95
+ async #draftMcpAppMessage(content) {
96
+ const text = mcpMessageText(content);
97
+ const prompt = this._promptRef.value;
98
+ if (!text.trim() || !prompt) {
99
+ return { isError: true };
100
+ }
101
+ prompt.value = text;
102
+ prompt.focus();
103
+ return {};
104
+ }
105
+ /**
106
+ * Sends `ui/resource-teardown` to mounted MCP-app widgets (all, or only `toolCallIds`) and
107
+ * releases them immediately, without waiting for acks. The adapter then drops any state it
108
+ * holds for them (e.g. a pending model context).
109
+ */
110
+ _teardownMcpAppViews({ toolCallIds } = {}) {
111
+ const views = [...this.#mcpAppViews.values()].filter((view) => toolCallIds?.includes(view.toolCallId) ?? true);
112
+ for (const view of views) {
113
+ this.#mcpAppViews.delete(view.toolCallId);
114
+ view.teardown();
115
+ this._coreController?.adapter?.disposeMcpAppView?.({ toolCallId: view.toolCallId });
116
+ }
117
+ }
118
+ /** Tears down widgets rendered at or after `index`, then removes those items. */
119
+ #removeMessageItemsFrom(index) {
120
+ this._teardownMcpAppViews({ toolCallIds: this._coreController.getToolCallIdsFrom(index) });
121
+ this._coreController.removeMessageItemsFrom(index);
122
+ }
123
+ #handleThemeChange;
124
+ /** A new host context object makes every widget re-read its Forge styles and notify its View. */
125
+ #refreshMcpAppHostContext() {
126
+ this._mcpAppHost = {
127
+ ...this._mcpAppHost,
128
+ hostContext: { ...this._mcpAppHost.hostContext, theme: this.#effectiveTheme }
129
+ };
32
130
  }
33
131
  get _isStreaming() {
34
132
  return this._coreController?.isStreaming ?? false;
@@ -66,12 +164,26 @@ class AiChatbotBase extends LitElement {
66
164
  if (this.adapter) {
67
165
  this._coreController.adapter = this.adapter;
68
166
  }
167
+ this.#themeMediaQuery.addEventListener("change", this.#handleThemeChange);
69
168
  this._onConnected();
70
169
  }
170
+ disconnectedCallback() {
171
+ super.disconnectedCallback();
172
+ this.#themeMediaQuery.removeEventListener("change", this.#handleThemeChange);
173
+ }
71
174
  willUpdate(changedProperties) {
72
175
  if (changedProperties.has("adapter") && this._coreController) {
73
176
  this._coreController.adapter = this.adapter;
74
177
  }
178
+ if (changedProperties.has("adapter")) {
179
+ this._mcpAppHost = { ...this._mcpAppHost, hostCapabilities: mcpAppHostCapabilities(this.adapter) };
180
+ }
181
+ if (changedProperties.has("mcpAppSandboxUrl")) {
182
+ this._mcpAppHost = { ...this._mcpAppHost, sandboxUrl: this.mcpAppSandboxUrl };
183
+ }
184
+ if (changedProperties.has("mcpAppTheme")) {
185
+ this.#refreshMcpAppHostContext();
186
+ }
75
187
  }
76
188
  _onConnected() {
77
189
  }
@@ -239,7 +351,7 @@ class AiChatbotBase extends LitElement {
239
351
  }
240
352
  const responseIndex = messageIndex + 1;
241
353
  if (responseIndex < this._messageItems.length) {
242
- this._coreController.removeMessageItemsFrom(responseIndex);
354
+ this.#removeMessageItemsFrom(responseIndex);
243
355
  }
244
356
  adapter.sendMessage(this.getMessages());
245
357
  }
@@ -256,7 +368,7 @@ class AiChatbotBase extends LitElement {
256
368
  this._coreController.updateMessageContent(messageId, content);
257
369
  const responseIndex = messageIndex + 1;
258
370
  if (responseIndex < this._messageItems.length) {
259
- this._coreController.removeMessageItemsFrom(responseIndex);
371
+ this.#removeMessageItemsFrom(responseIndex);
260
372
  }
261
373
  adapter.sendMessage(this.getMessages());
262
374
  }
@@ -272,7 +384,7 @@ class AiChatbotBase extends LitElement {
272
384
  if (responseIndex === -1) {
273
385
  return;
274
386
  }
275
- this._coreController.removeMessageItemsFrom(responseIndex);
387
+ this.#removeMessageItemsFrom(responseIndex);
276
388
  adapter.sendMessage(this.getMessages());
277
389
  }
278
390
  _handleFeedback(evt, type) {
@@ -348,6 +460,7 @@ class AiChatbotBase extends LitElement {
348
460
  if (event.defaultPrevented) {
349
461
  return false;
350
462
  }
463
+ this._teardownMcpAppViews();
351
464
  this._coreController.clearMessages();
352
465
  return true;
353
466
  }
@@ -365,6 +478,7 @@ class AiChatbotBase extends LitElement {
365
478
  * resetting thread IDs or closing panels.
366
479
  */
367
480
  startNewChat() {
481
+ this._teardownMcpAppViews();
368
482
  this._coreController.clearMessages();
369
483
  }
370
484
  getMessages() {
@@ -431,6 +545,14 @@ class AiChatbotBase extends LitElement {
431
545
  abort() {
432
546
  this._handleStop();
433
547
  }
548
+ /**
549
+ * Re-sends the Forge-derived `styles.variables` to every MCP-app widget. Call after
550
+ * changing Forge tokens without a light/dark switch (e.g. a brand change); for light/dark,
551
+ * set {@link mcpAppTheme} instead.
552
+ */
553
+ refreshMcpAppStyles() {
554
+ this.#refreshMcpAppHostContext();
555
+ }
434
556
  async scrollToBottom({ behavior } = {}) {
435
557
  await this._messageThreadRef.value?.updateComplete;
436
558
  await new Promise(requestAnimationFrame);
@@ -448,6 +570,7 @@ class AiChatbotBase extends LitElement {
448
570
  return this.agents.find((a) => a.id === this.selectedAgentId);
449
571
  }
450
572
  async setThreadState(threadState) {
573
+ this._teardownMcpAppViews();
451
574
  this.setMessages(threadState.messages);
452
575
  this.selectedAgentId = threadState.selectedAgentId;
453
576
  if (threadState.threadId && this._coreController.adapter) {
@@ -535,6 +658,16 @@ __decorateClass([
535
658
  __decorateClass([
536
659
  property({ attribute: false })
537
660
  ], AiChatbotBase.prototype, "contextItems");
661
+ __decorateClass([
662
+ property({ attribute: "mcp-app-sandbox-url" })
663
+ ], AiChatbotBase.prototype, "mcpAppSandboxUrl");
664
+ __decorateClass([
665
+ property({ attribute: "mcp-app-theme" })
666
+ ], AiChatbotBase.prototype, "mcpAppTheme");
667
+ __decorateClass([
668
+ provide({ context: mcpAppHostContext }),
669
+ property({ attribute: false })
670
+ ], AiChatbotBase.prototype, "_mcpAppHost");
538
671
  export {
539
672
  AiChatbotBase
540
673
  };
@@ -3,8 +3,8 @@ export { AiChatbotComponent, AiChatbotComponentTagName } from './ai-chatbot.js';
3
3
  export type { ForgeAiChatbotMessageEventData, ForgeAiChatbotToolCallEventData, ForgeAiChatbotErrorEventData, ForgeAiChatbotResponseFeedbackEventData, ForgeAiChatbotAgentChangeEventData, ForgeAiChatbotFileSelectEventData, ForgeAiChatbotFileRemoveEventData } from './ai-chatbot.js';
4
4
  export { AiChatbotToolCallComponent, AiChatbotToolCallComponentTagName } from './ai-chatbot-tool-call.js';
5
5
  export type { Suggestion } from '../ai-suggestions/index.js';
6
- export { AgentAdapter, type MessageStartEvent, type MessageDeltaEvent, type MessageEndEvent, type ToolCallEvent, type ToolCallStartEvent, type ToolCallArgsEvent, type ToolCallEndEvent, type FileUploadEvent, type FileRemoveEvent, type AdapterState, type ErrorEvent, type CustomAgentEvent, type RawAgentEvent, type RunStartedAgentEvent, type StepStartedAgentEvent, type StepFinishedAgentEvent, type StateSnapshotAgentEvent, type StateDeltaAgentEvent, type ActivitySnapshotAgentEvent, type ActivityDeltaAgentEvent } from './agent-adapter.js';
7
- export type { Agent, ChatMessage, ClientMessageAction, ClientMessageInput, ClientMessageKind, ContextItem, ToolDefinition, ToolCall, ToolRenderer, IToolRenderer, HandlerContext, ToolStartContext, ToolDeltaContext, ToolEndContext, FileAttachment, UploadedFileMetadata, FileUploadCallbacks, FileUploadHandler, FileRemoveCallbacks, ThreadState, FeedbackType, ResponseFeedback, MessageItem, ForgeAiChatbotContextRemoveEventData, ForgeAiChatbotThreadSelectEventData, ForgeAiChatbotThreadSearchEventData, ForgeAiChatbotThreadLoadMoreEventData, ForgeAiChatbotThreadRenameEventData, ForgeAiChatbotThreadDeleteEventData } from './types.js';
6
+ export { AgentAdapter, type MessageStartEvent, type MessageDeltaEvent, type MessageEndEvent, type ToolCallEvent, type ToolCallStartEvent, type ToolCallArgsEvent, type ToolCallEndEvent, type FileUploadEvent, type FileRemoveEvent, type AdapterState, type ErrorEvent, type CustomAgentEvent, type RawAgentEvent, type RunStartedAgentEvent, type StepStartedAgentEvent, type StepFinishedAgentEvent, type StateSnapshotAgentEvent, type StateDeltaAgentEvent, type ActivitySnapshotAgentEvent, type ActivityDeltaAgentEvent, type McpUiResourceEvent, type McpToolCallParams, type McpResourceReadParams, type McpAppResourceResolveParams, type McpModelContextUpdateParams, type ToolResultEvent } from './agent-adapter.js';
7
+ export type { Agent, ChatMessage, ClientMessageAction, ClientMessageInput, ClientMessageKind, ContextItem, ToolDefinition, ToolCall, ToolCallStatus, ToolRenderer, IToolRenderer, HandlerContext, ToolStartContext, ToolDeltaContext, ToolEndContext, FileAttachment, UploadedFileMetadata, FileUploadCallbacks, FileUploadHandler, FileRemoveCallbacks, ThreadState, FeedbackType, ResponseFeedback, MessageItem, ForgeAiChatbotContextRemoveEventData, ForgeAiChatbotThreadSelectEventData, ForgeAiChatbotThreadSearchEventData, ForgeAiChatbotThreadLoadMoreEventData, ForgeAiChatbotThreadRenameEventData, ForgeAiChatbotThreadDeleteEventData, McpAppUiResource, McpAppResourceCsp, McpAppResourcePermissions, McpAppHostContext, McpAppHostStyles, McpAppHostCapabilities, McpAppContainerDimensions, McpAppContentModalities, McpAppToolInfo, McpAppSandboxCapabilities, McpAppDisplayMode, McpToolResult } from './types.js';
8
8
  export * from './ag-ui-adapter.js';
9
9
  export * from './http-agent-with-credentials.js';
10
10
  export * from './event-emitter.js';
@@ -34,6 +34,10 @@ export declare class MessageStateController implements ReactiveController {
34
34
  appendTextDelta(messageId: string, delta: string): void;
35
35
  markTextComplete(messageId: string): void;
36
36
  addToolCallToResponse(toolCall: ToolCall): void;
37
+ /**
38
+ * Applies `updates` to a tool call wherever it is rendered: the in-progress response,
39
+ * an already finalized/restored response, or a top-level tool-call item.
40
+ */
37
41
  updateToolCallInResponse(toolCallId: string, updates: Partial<ToolCall>): void;
38
42
  completeToolCallInResponse(toolCallId: string, result: unknown, event?: ToolResultEvent): void;
39
43
  tryFinalizeResponse(): void;
@@ -52,10 +56,15 @@ export declare class MessageStateController implements ReactiveController {
52
56
  updateMessageContent(id: string, content: string): void;
53
57
  removeMessageItem(index: number): void;
54
58
  clearMessages(): void;
55
- removeMessageItemsFrom(index: number): void;
59
+ /** Tool call ids rendered at or after `index`, inside assistant responses or as top-level items. */
60
+ getToolCallIdsFrom(index: number): string[];
61
+ /** Removes items from `index` onward and returns the ids of the tool calls they held. */
62
+ removeMessageItemsFrom(index: number): string[];
56
63
  /**
57
64
  * Reconstructs the message hierarchy for API compatibility.
58
65
  * Converts assistant responses back to ChatMessage format with tool calls.
66
+ * MCP-app `uiResource`s are stripped: they are re-resolved via the adapter on restore,
67
+ * never persisted or sent back to the agent.
59
68
  */
60
69
  getMessages(): ChatMessage[];
61
70
  setMessages(messages: ChatMessage[]): void;
@@ -1,4 +1,14 @@
1
1
  import { generateId } from "./utils.mjs";
2
+ function withoutUiResource(toolCall) {
3
+ if (!toolCall.uiResource) {
4
+ return toolCall;
5
+ }
6
+ const { uiResource: _uiResource, ...rest } = toolCall;
7
+ return rest;
8
+ }
9
+ function responseItemWithoutUiResource(item) {
10
+ return item.type === "toolCall" ? { type: "toolCall", data: withoutUiResource(item.data) } : item;
11
+ }
2
12
  class MessageStateController {
3
13
  constructor(_host, _config) {
4
14
  this._host = _host;
@@ -103,33 +113,61 @@ class MessageStateController {
103
113
  this.#updateResponseInItems();
104
114
  this.#notifyStateChange();
105
115
  }
116
+ /**
117
+ * Applies `updates` to a tool call wherever it is rendered: the in-progress response,
118
+ * an already finalized/restored response, or a top-level tool-call item.
119
+ */
106
120
  updateToolCallInResponse(toolCallId, updates) {
107
121
  const toolCall = this._toolCalls.get(toolCallId);
108
122
  if (!toolCall) {
109
123
  return;
110
124
  }
111
- const isCompleting = updates.status === "complete" || updates.status === "error";
125
+ const isFinishing = updates.status === "complete" || updates.status === "error" || updates.status === "cancelled";
112
126
  const updated = {
113
127
  ...toolCall,
114
128
  ...updates,
115
- ...isCompleting && !toolCall.endTimestamp ? { endTimestamp: Date.now() } : {}
129
+ ...isFinishing && !toolCall.endTimestamp ? { endTimestamp: Date.now() } : {}
116
130
  };
117
131
  this._toolCalls.set(toolCallId, updated);
118
- if (this._activeResponse) {
119
- for (const child of this._activeResponse.children) {
120
- if (child.type === "toolCall" && child.data.id === toolCallId) {
121
- child.data = updated;
122
- break;
123
- }
124
- }
125
- }
126
- this.#updateResponseInItems();
132
+ this.#replaceToolCall(updated);
127
133
  this.#notifyStateChange();
128
134
  }
129
135
  completeToolCallInResponse(toolCallId, result, event) {
130
136
  this.updateToolCallInResponse(toolCallId, {
131
137
  result,
132
- status: event?.isError === true ? "error" : "complete"
138
+ status: this.#resultStatus(event),
139
+ ...event ? {
140
+ structuredContent: event.structuredContent,
141
+ structuredContentOmitted: event.structuredContentOmitted,
142
+ mcpResult: event.mcpResult,
143
+ mcpResultOmitted: event.mcpResultOmitted
144
+ } : {}
145
+ });
146
+ }
147
+ #resultStatus(event) {
148
+ if (event?.cancelled) {
149
+ return "cancelled";
150
+ }
151
+ return event?.isError ? "error" : "complete";
152
+ }
153
+ #replaceToolCall(updated) {
154
+ const matches = (child) => child.type === "toolCall" && child.data.id === updated.id;
155
+ const replace = (children) => children.map((child) => matches(child) ? { type: "toolCall", data: updated } : child);
156
+ const activeResponse = this._activeResponse;
157
+ if (activeResponse?.children.some(matches)) {
158
+ activeResponse.children = replace(activeResponse.children);
159
+ }
160
+ this._messageItems = this._messageItems.map((item) => {
161
+ if (item.type === "toolCall" && item.data.id === updated.id) {
162
+ return { type: "toolCall", data: updated };
163
+ }
164
+ if (item.type !== "assistant" || !item.data.children.some(matches)) {
165
+ return item;
166
+ }
167
+ if (item.data.id === activeResponse?.id) {
168
+ return { type: "assistant", data: { ...activeResponse } };
169
+ }
170
+ return { type: "assistant", data: { ...item.data, children: replace(item.data.children) } };
133
171
  });
134
172
  }
135
173
  tryFinalizeResponse() {
@@ -242,14 +280,32 @@ class MessageStateController {
242
280
  this.#notifyStateChange();
243
281
  this._config.onThreadSettled?.();
244
282
  }
283
+ /** Tool call ids rendered at or after `index`, inside assistant responses or as top-level items. */
284
+ getToolCallIdsFrom(index) {
285
+ return this._messageItems.slice(index).flatMap((item) => {
286
+ if (item.type === "toolCall") {
287
+ return [item.data.id];
288
+ }
289
+ if (item.type === "assistant") {
290
+ return item.data.children.flatMap((child) => child.type === "toolCall" ? [child.data.id] : []);
291
+ }
292
+ return [];
293
+ });
294
+ }
295
+ /** Removes items from `index` onward and returns the ids of the tool calls they held. */
245
296
  removeMessageItemsFrom(index) {
297
+ const removedToolCallIds = this.getToolCallIdsFrom(index);
298
+ removedToolCallIds.forEach((id) => this._toolCalls.delete(id));
246
299
  this._messageItems = this._messageItems.slice(0, index);
247
300
  this._activeResponse = null;
248
301
  this.#notifyStateChange();
302
+ return removedToolCallIds;
249
303
  }
250
304
  /**
251
305
  * Reconstructs the message hierarchy for API compatibility.
252
306
  * Converts assistant responses back to ChatMessage format with tool calls.
307
+ * MCP-app `uiResource`s are stripped: they are re-resolved via the adapter on restore,
308
+ * never persisted or sent back to the agent.
253
309
  */
254
310
  getMessages() {
255
311
  const messages = [];
@@ -258,8 +314,9 @@ class MessageStateController {
258
314
  messages.push({ ...item.data, toolCalls: [] });
259
315
  } else if (item.type === "assistant") {
260
316
  const response = item.data;
261
- const textContent = response.children.filter((c) => c.type === "text").map((c) => c.content).join("");
262
- const toolCalls = response.children.filter((c) => c.type === "toolCall").map((c) => c.data);
317
+ const children = response.children.map(responseItemWithoutUiResource);
318
+ const textContent = children.filter((c) => c.type === "text").map((c) => c.content).join("");
319
+ const toolCalls = children.filter((c) => c.type === "toolCall").map((c) => c.data);
263
320
  const message = {
264
321
  id: response.id,
265
322
  role: "assistant",
@@ -268,13 +325,13 @@ class MessageStateController {
268
325
  status: response.status === "streaming" ? "streaming" : response.status === "error" ? "error" : "complete",
269
326
  toolCalls: toolCalls.length ? toolCalls : void 0,
270
327
  feedback: response.feedback,
271
- children: response.children
328
+ children
272
329
  };
273
330
  messages.push(message);
274
331
  } else if (item.type === "toolCall") {
275
332
  const lastMsg = messages[messages.length - 1];
276
333
  if (lastMsg && lastMsg.role === "assistant") {
277
- lastMsg.toolCalls = [...lastMsg.toolCalls || [], item.data];
334
+ lastMsg.toolCalls = [...lastMsg.toolCalls || [], withoutUiResource(item.data)];
278
335
  }
279
336
  }
280
337
  }
@@ -289,10 +346,11 @@ class MessageStateController {
289
346
  let children;
290
347
  if (msg.children?.length) {
291
348
  children = msg.children.map((child) => {
292
- if (child.type === "toolCall") {
293
- this._toolCalls.set(child.data.id, child.data);
349
+ const restored = responseItemWithoutUiResource(child);
350
+ if (restored.type === "toolCall") {
351
+ this._toolCalls.set(restored.data.id, restored.data);
294
352
  }
295
- return child;
353
+ return restored;
296
354
  });
297
355
  } else {
298
356
  children = [];
@@ -300,7 +358,7 @@ class MessageStateController {
300
358
  children.push({ type: "text", messageId: msg.id, content: msg.content, status: "complete" });
301
359
  }
302
360
  if (msg.toolCalls) {
303
- for (const toolCall of msg.toolCalls) {
361
+ for (const toolCall of msg.toolCalls.map(withoutUiResource)) {
304
362
  children.push({ type: "toolCall", data: toolCall });
305
363
  this._toolCalls.set(toolCall.id, toolCall);
306
364
  }
@@ -88,6 +88,15 @@ export interface ToolDefinition<THandlerArgs = Record<string, unknown>> {
88
88
  renderer?: ToolRenderer;
89
89
  /** If true, renderer shows immediately on tool call start (default: false). */
90
90
  renderOnStart?: boolean;
91
+ /**
92
+ * Opt-in marker declaring this tool is backed by an MCP-app UI resource. Presence
93
+ * signals the core controller to resolve + stamp a {@link McpAppUiResource} when the
94
+ * tool is called. `resourceUri` is informational; the resolved URI arrives on the
95
+ * `mcp-ui-resource` event.
96
+ */
97
+ mcpApp?: {
98
+ resourceUri?: string;
99
+ };
91
100
  /** Called when tool call starts (before args stream). */
92
101
  onStart?: (context: ToolStartContext) => void;
93
102
  /** Called during streaming with partial args. */
@@ -100,6 +109,176 @@ export interface ToolDefinition<THandlerArgs = Record<string, unknown>> {
100
109
  */
101
110
  handler?: (context: HandlerContext<THandlerArgs>) => Promise<string | Record<string, unknown> | void> | string | Record<string, unknown> | void;
102
111
  }
112
+ /**
113
+ * Content-Security-Policy domain allow-lists for an MCP-app resource, mirroring the
114
+ * spec's `_meta.ui.csp` shape (2026-01-26). The sandbox proxy composes these into the
115
+ * enforced CSP; when omitted a restrictive default block applies.
116
+ *
117
+ * Local type — intentionally NOT aliased from `@modelcontextprotocol/ext-apps` so the
118
+ * type layer stays bridge-agnostic.
119
+ */
120
+ export interface McpAppResourceCsp {
121
+ /** Domains the widget may `connect-src` to (fetch/XHR/WebSocket). */
122
+ connectDomains?: string[];
123
+ /** Domains the widget may load passive resources from (img/media/etc.). */
124
+ resourceDomains?: string[];
125
+ /** Domains allowed in `frame-src`; absent → `frame-src 'none'`. */
126
+ frameDomains?: string[];
127
+ /** Domains allowed in `base-uri`; absent → `base-uri 'self'`. */
128
+ baseUriDomains?: string[];
129
+ }
130
+ /**
131
+ * Iframe permission policy for an MCP-app resource, mirroring the spec's
132
+ * `_meta.ui.permissions` shape (2026-01-26): a key being present (with an empty object
133
+ * value) means the permission is requested. The sandbox maps requested keys onto the
134
+ * iframe `allow` attribute.
135
+ *
136
+ * Local type — not aliased from ext-apps.
137
+ */
138
+ export interface McpAppResourcePermissions {
139
+ camera?: Record<string, never>;
140
+ microphone?: Record<string, never>;
141
+ geolocation?: Record<string, never>;
142
+ clipboardWrite?: Record<string, never>;
143
+ }
144
+ /**
145
+ * The resolved UI resource stamped onto a {@link ToolCall} once a `mcp-ui-resource`
146
+ * event arrives. Presence of this on a tool call is the single render discriminator for
147
+ * mounting `<forge-ai-mcp-app>`.
148
+ *
149
+ * Returned by `AgentAdapter.resolveMcpAppResource`, which fills `csp`/`permissions` from
150
+ * the template response's `_meta.ui` (pass `{}` when the template declares none). The
151
+ * widget refuses to render when either is missing.
152
+ *
153
+ * Local type — not aliased from ext-apps.
154
+ */
155
+ export interface McpAppUiResource {
156
+ /** The complete standalone app HTML document, resolved server-side. */
157
+ html: string;
158
+ /** CSP domain allow-lists to enforce in the sandbox. Empty → restrictive default. */
159
+ csp: McpAppResourceCsp;
160
+ /** Iframe permission policy for the sandbox inner frame. */
161
+ permissions: McpAppResourcePermissions;
162
+ /** The concrete resolved `ui://` resource URI (never a template). */
163
+ resourceUri?: string;
164
+ /**
165
+ * Whether the widget prefers the host to draw a border/background around it.
166
+ * `true` → visible chrome, `false` → none, omitted → renderer default.
167
+ */
168
+ prefersBorder?: boolean;
169
+ }
170
+ /** Display modes an MCP-app widget can be presented in (spec 2026-01-26). */
171
+ export type McpAppDisplayMode = 'inline' | 'fullscreen' | 'pip';
172
+ /**
173
+ * The raw MCP `CallToolResult` (minus `structuredContent`) for an MCP-app tool call. The
174
+ * View's `ui/notifications/tool-result` is built from this plus
175
+ * {@link ToolCall.structuredContent} — never from the model-facing `result`.
176
+ */
177
+ export interface McpToolResult {
178
+ content: unknown[];
179
+ isError?: boolean;
180
+ _meta?: Record<string, unknown>;
181
+ }
182
+ /**
183
+ * Size of the container holding a widget, in CSS pixels (spec `containerDimensions`): a
184
+ * fixed `height` or a `maxHeight` bound, and a fixed `width` or a `maxWidth` bound.
185
+ */
186
+ export type McpAppContainerDimensions = ({
187
+ height: number;
188
+ } | {
189
+ maxHeight?: number;
190
+ }) & ({
191
+ width: number;
192
+ } | {
193
+ maxWidth?: number;
194
+ });
195
+ /** The tool call that instantiated a widget (spec `toolInfo`). */
196
+ export interface McpAppToolInfo {
197
+ id?: string | number;
198
+ tool: {
199
+ name: string;
200
+ inputSchema: {
201
+ type: 'object';
202
+ [key: string]: unknown;
203
+ };
204
+ [key: string]: unknown;
205
+ };
206
+ }
207
+ /**
208
+ * Theming sent to MCP-app widgets (spec `hostContext.styles`): spec CSS variables (e.g.
209
+ * `--color-background-primary`) resolved from Forge tokens. Font loading (`css.fonts`) is not sent.
210
+ */
211
+ export interface McpAppHostStyles {
212
+ variables?: Record<string, string>;
213
+ }
214
+ /**
215
+ * Host context projected into an MCP-app widget.
216
+ *
217
+ * Local type — not aliased from ext-apps.
218
+ */
219
+ export interface McpAppHostContext {
220
+ theme: 'light' | 'dark';
221
+ /** Spec CSS variables resolved from the widget's Forge tokens. */
222
+ styles?: McpAppHostStyles;
223
+ /** The tool call that instantiated the widget. */
224
+ toolInfo?: McpAppToolInfo;
225
+ /** Size of the container holding the widget. */
226
+ containerDimensions?: McpAppContainerDimensions;
227
+ /** The widget's current display mode. */
228
+ displayMode?: McpAppDisplayMode;
229
+ /** Display modes the host is able to present. */
230
+ availableDisplayModes?: McpAppDisplayMode[];
231
+ /** BCP 47 language tag. */
232
+ locale?: string;
233
+ /** IANA time zone. */
234
+ timeZone?: string;
235
+ userAgent?: string;
236
+ platform?: 'web' | 'desktop' | 'mobile';
237
+ deviceCapabilities?: {
238
+ touch?: boolean;
239
+ hover?: boolean;
240
+ };
241
+ safeAreaInsets?: {
242
+ top: number;
243
+ right: number;
244
+ bottom: number;
245
+ left: number;
246
+ };
247
+ }
248
+ /** Content-block types a host accepts for `ui/message` / `ui/update-model-context`. */
249
+ export interface McpAppContentModalities {
250
+ text?: Record<string, never>;
251
+ image?: Record<string, never>;
252
+ audio?: Record<string, never>;
253
+ resource?: Record<string, never>;
254
+ resourceLink?: Record<string, never>;
255
+ structuredContent?: Record<string, never>;
256
+ }
257
+ /** The sandbox policy the host enforces for a widget, echoed in its host capabilities. */
258
+ export interface McpAppSandboxCapabilities {
259
+ csp?: McpAppResourceCsp;
260
+ permissions?: McpAppResourcePermissions;
261
+ }
262
+ /**
263
+ * Host capability advertisement returned to the widget in `McpUiInitializeResult`.
264
+ * Spec-compliant widgets gate `ui/open-link` / `tools/call` / `resources/read` on these,
265
+ * so they must be declared, not left implicit.
266
+ *
267
+ * Local type — not aliased from ext-apps.
268
+ */
269
+ export interface McpAppHostCapabilities {
270
+ openLinks?: Record<string, never>;
271
+ serverTools?: {
272
+ listChanged?: boolean;
273
+ };
274
+ serverResources?: {
275
+ listChanged?: boolean;
276
+ };
277
+ logging?: Record<string, never>;
278
+ message?: McpAppContentModalities;
279
+ updateModelContext?: McpAppContentModalities;
280
+ sandbox?: McpAppSandboxCapabilities;
281
+ }
103
282
  export interface ChatMessage {
104
283
  id: string;
105
284
  role: 'user' | 'assistant' | 'system' | 'tool';
@@ -144,11 +323,30 @@ export interface ToolCall<TArgs = Record<string, unknown>> {
144
323
  args: TArgs;
145
324
  argsBuffer?: string;
146
325
  result?: unknown;
147
- status: 'pending' | 'parsing' | 'executing' | 'complete' | 'error';
326
+ status: ToolCallStatus;
148
327
  type: ToolType;
149
328
  startTimestamp?: number;
150
329
  endTimestamp?: number;
330
+ /**
331
+ * The resolved MCP-app UI resource. When present, this tool call renders as a
332
+ * sandboxed `<forge-ai-mcp-app>` widget instead of the standard tool-call UI. Its
333
+ * presence is the single render discriminator.
334
+ */
335
+ uiResource?: McpAppUiResource;
336
+ /** Companion structured payload for the widget, stripped from LLM context. */
337
+ structuredContent?: unknown;
338
+ /** `true` when `structuredContent` was dropped by the backend (size caps). */
339
+ structuredContentOmitted?: boolean;
340
+ /** Raw MCP result for an MCP-app tool call; the source of the View's `tool-result`. */
341
+ mcpResult?: McpToolResult;
342
+ /** `true` when `mcpResult` was dropped by the backend (size caps). */
343
+ mcpResultOmitted?: boolean;
151
344
  }
345
+ /**
346
+ * Tool call lifecycle. `cancelled` means the call ended without a result (run aborted,
347
+ * stream ended/errored, or the adapter reported `cancelled: true`).
348
+ */
349
+ export type ToolCallStatus = 'pending' | 'parsing' | 'executing' | 'complete' | 'error' | 'cancelled';
152
350
  export type ResponseItem = {
153
351
  type: 'text';
154
352
  messageId: string;
@@ -10,7 +10,7 @@ function shouldShowToolRenderer(toolCall, toolDefinition) {
10
10
  return !!toolDefinition.renderOnStart && (toolCall.status === "parsing" || toolCall.status === "executing");
11
11
  }
12
12
  function isToolCallSettled(toolCall, toolDefinition) {
13
- return toolCall.status === "complete" || toolCall.status === "error" || shouldShowToolRenderer(toolCall, toolDefinition);
13
+ return toolCall.status === "complete" || toolCall.status === "error" || toolCall.status === "cancelled" || shouldShowToolRenderer(toolCall, toolDefinition);
14
14
  }
15
15
  function generateId() {
16
16
  return v4();
@@ -49,6 +49,8 @@ function formatToolCallForExport(toolCall) {
49
49
  if (toolCall.status === "error") {
50
50
  const errorMsg = typeof toolCall.result === "object" && toolCall.result !== null && "error" in toolCall.result ? toolCall.result.error : "Unknown error";
51
51
  lines.push(` Error: ${errorMsg}`);
52
+ } else if (toolCall.status === "cancelled") {
53
+ lines.push(" Cancelled");
52
54
  } else if (toolCall.result !== void 0) {
53
55
  lines.push(` Result: ${JSON.stringify(toolCall.result)}`);
54
56
  }