@tylertech/forge-ai 0.15.3-dev.0 → 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.
- package/custom-elements.json +7733 -6803
- package/dist/ai-assistant-response/ai-assistant-response.d.ts +2 -2
- package/dist/ai-assistant-response/ai-assistant-response.mjs +4 -3
- package/dist/ai-canvas/ai-canvas.d.ts +43 -0
- package/dist/ai-canvas/ai-canvas.mjs +95 -0
- package/dist/ai-canvas/ai-canvas.scss.mjs +4 -0
- package/dist/ai-canvas/index.d.ts +1 -0
- package/dist/ai-canvas/index.mjs +5 -0
- package/dist/ai-chatbot/ag-ui-adapter.d.ts +2 -2
- package/dist/ai-chatbot/agent-adapter.d.ts +5 -5
- package/dist/ai-chatbot/agent-runner.d.ts +2 -2
- package/dist/ai-chatbot/ai-chatbot-base.d.ts +3 -2
- package/dist/ai-chatbot/ai-chatbot-base.mjs +11 -0
- package/dist/ai-chatbot/ai-chatbot-tool-call.d.ts +2 -2
- package/dist/ai-chatbot/ai-chatbot-tool-call.mjs +88 -10
- package/dist/ai-chatbot/ai-chatbot.d.ts +24 -1
- package/dist/ai-chatbot/ai-chatbot.mjs +102 -2
- package/dist/ai-chatbot/ai-chatbot.scss.mjs +1 -1
- package/dist/ai-chatbot/create-tool-renderer.d.ts +5 -5
- package/dist/ai-chatbot/index.d.ts +2 -1
- package/dist/ai-chatbot/index.mjs +3 -0
- package/dist/ai-chatbot/message-state-controller.d.ts +2 -2
- package/dist/ai-chatbot/message-state-controller.mjs +11 -11
- package/dist/ai-chatbot/tool-canvas.d.ts +60 -0
- package/dist/ai-chatbot/tool-canvas.mjs +128 -0
- package/dist/ai-chatbot/types.d.ts +128 -16
- package/dist/ai-chatbot/utils.d.ts +8 -3
- package/dist/ai-chatbot/utils.mjs +4 -0
- package/dist/ai-chatbot-launcher/ai-chatbot-launcher.d.ts +40 -2
- package/dist/ai-chatbot-launcher/ai-chatbot-launcher.mjs +224 -5
- package/dist/ai-chatbot-launcher/ai-chatbot-launcher.scss.mjs +1 -1
- package/dist/ai-embedded-chat/ai-embedded-chat.mjs +1 -0
- package/dist/ai-message-thread/ai-message-thread.d.ts +2 -2
- package/dist/ai-message-thread/ai-message-thread.mjs +3 -2
- package/dist/ai-tool-call-indicator/ai-tool-call-indicator.d.ts +2 -2
- package/dist/core/canvas-controller.d.ts +81 -0
- package/dist/core/canvas-controller.mjs +221 -0
- package/dist/core/chatbot-core-controller.d.ts +2 -2
- package/dist/core/chatbot-core-controller.mjs +44 -22
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.mjs +2 -0
- package/dist/core/resize-controller.d.ts +5 -0
- package/dist/core/resize-controller.mjs +7 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +6 -0
- package/dist/tools/ai-data-table/ai-data-table-definition.mjs +1 -0
- package/dist/tools/ai-mcp-app/ai-mcp-app.d.ts +2 -1
- package/dist/tools/ai-mcp-app/ai-mcp-app.mjs +16 -1
- package/dist/tools/ai-mcp-app/ai-mcp-app.scss.mjs +1 -1
- package/dist/tools/ai-mcp-app/mcp-app-bridge-controller.d.ts +3 -4
- package/dist/tools/ai-mcp-app/mcp-app-bridge-controller.mjs +12 -21
- package/dist/tools/ai-mcp-app/mcp-app-bridge-types.d.ts +2 -1
- package/dist/tools/ai-steps/ai-steps-definition.mjs +1 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
|
4
4
|
import { when } from "lit/directives/when.js";
|
|
5
5
|
import { keyed } from "lit/directives/keyed.js";
|
|
6
6
|
import { MarkdownStreamController } from "../ai-chatbot/markdown-stream-controller.mjs";
|
|
7
|
+
import { rendersAsMcpAppWidget } from "../ai-chatbot/utils.mjs";
|
|
7
8
|
import "../ai-assistant-response/ai-assistant-response.mjs";
|
|
8
9
|
import "../ai-client-message/ai-client-message.mjs";
|
|
9
10
|
import "../ai-empty-state/ai-empty-state.mjs";
|
|
@@ -196,7 +197,7 @@ handleUserEdit_fn = function(messageId, content) {
|
|
|
196
197
|
});
|
|
197
198
|
};
|
|
198
199
|
renderToolCall_fn = function(toolCall) {
|
|
199
|
-
if (toolCall
|
|
200
|
+
if (rendersAsMcpAppWidget(toolCall)) {
|
|
200
201
|
return keyed(toolCall.id, html`<forge-ai-mcp-app .toolCall=${toolCall}></forge-ai-mcp-app>`);
|
|
201
202
|
}
|
|
202
203
|
const toolDefinition = this.tools?.get(toolCall.name);
|
|
@@ -257,7 +258,7 @@ messages_get = function() {
|
|
|
257
258
|
return true;
|
|
258
259
|
}
|
|
259
260
|
if (item.type === "toolCall") {
|
|
260
|
-
if (item.data
|
|
261
|
+
if (rendersAsMcpAppWidget(item.data)) {
|
|
261
262
|
return true;
|
|
262
263
|
}
|
|
263
264
|
const toolDef = this.tools?.get(item.data.name);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LitElement, nothing, PropertyValues, TemplateResult, CSSResult } from 'lit';
|
|
2
|
-
import { ToolCall,
|
|
2
|
+
import { ToolCall, AnyToolDefinition } from '../ai-chatbot/types.js';
|
|
3
3
|
declare global {
|
|
4
4
|
interface HTMLElementTagNameMap {
|
|
5
5
|
'forge-ai-tool-call-indicator': AiToolCallIndicatorComponent;
|
|
@@ -20,7 +20,7 @@ export declare class AiToolCallIndicatorComponent extends LitElement {
|
|
|
20
20
|
#private;
|
|
21
21
|
static styles: CSSResult;
|
|
22
22
|
toolCalls: ToolCall[];
|
|
23
|
-
tools?: Map<string,
|
|
23
|
+
tools?: Map<string, AnyToolDefinition>;
|
|
24
24
|
debugMode: boolean;
|
|
25
25
|
private _expanded;
|
|
26
26
|
private _expandedRows;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
import { AnyToolDefinition, ChatMessage, ToolCall } from '../ai-chatbot/types.js';
|
|
3
|
+
import { ForgeAiCanvasCloseRequestEventData, ForgeAiCanvasOpenRequestEventData } from '../ai-chatbot/tool-canvas.js';
|
|
4
|
+
export interface OpenCanvasOptions {
|
|
5
|
+
/** Identifies the artifact; tool-driven opens use the tool call id. */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Overrides `canvasTitle` while this content is shown. */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Content mounted in the canvas body. Omit to show the `canvas` slot instead. */
|
|
10
|
+
content?: Node;
|
|
11
|
+
}
|
|
12
|
+
export interface CanvasOpenEventData {
|
|
13
|
+
id?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
/** Present when the canvas was opened for a tool call. */
|
|
16
|
+
toolCall?: ToolCall;
|
|
17
|
+
}
|
|
18
|
+
/** `auto`: opened by `canvas.open: 'auto'` rather than a user or API request. */
|
|
19
|
+
export type CanvasOpenReason = 'auto' | 'request';
|
|
20
|
+
export interface CanvasContent {
|
|
21
|
+
id?: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
node?: Node;
|
|
24
|
+
toolCall?: ToolCall;
|
|
25
|
+
/** Set for tool-driven content so it can follow the tool call as it updates. */
|
|
26
|
+
toolDefinition?: AnyToolDefinition;
|
|
27
|
+
/** Title supplied with the open request; otherwise the title is re-derived from the tool call. */
|
|
28
|
+
titleOverride?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface CanvasControllerConfig {
|
|
31
|
+
/** Cancelable event dispatched on the host before the canvas opens or swaps content. */
|
|
32
|
+
openEvent: string;
|
|
33
|
+
/** Event dispatched on the host when the canvas closes; cancelable except when the host forces it. */
|
|
34
|
+
closeEvent: string;
|
|
35
|
+
dispatchHostEvent: (config: {
|
|
36
|
+
type: string;
|
|
37
|
+
detail?: unknown;
|
|
38
|
+
cancelable?: boolean;
|
|
39
|
+
}) => CustomEvent;
|
|
40
|
+
getToolCalls: () => ToolCall[];
|
|
41
|
+
getTools: () => Map<string, AnyToolDefinition>;
|
|
42
|
+
/** Whether the host can show the canvas right now. Defaults to always. */
|
|
43
|
+
canOpen?: () => boolean;
|
|
44
|
+
/** Called once the canvas has gone from closed to open, not when its content is swapped. */
|
|
45
|
+
onOpen?: (config: {
|
|
46
|
+
reason: CanvasOpenReason;
|
|
47
|
+
}) => void;
|
|
48
|
+
/** Called after the canvas is hidden for any reason. */
|
|
49
|
+
onClose?: () => void;
|
|
50
|
+
/** Called when the requested tool call is already showing, so the host can draw attention to it. */
|
|
51
|
+
onRefocus?: () => void;
|
|
52
|
+
/** Resolves once the host's collapse presentation has finished; hidden content is released after it. */
|
|
53
|
+
collapsed?: () => Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Owns the artifact canvas state shared by every chatbot form factor: what is shown, the cancelable
|
|
57
|
+
* open/close events, tool call tracking, auto-open, and the `canvas` controls injected into renderers.
|
|
58
|
+
* The host decides how that state is presented (a side panel, a modal, ...).
|
|
59
|
+
*/
|
|
60
|
+
export declare class CanvasController implements ReactiveController {
|
|
61
|
+
#private;
|
|
62
|
+
constructor(host: ReactiveControllerHost & HTMLElement, config: CanvasControllerConfig);
|
|
63
|
+
hostConnected(): void;
|
|
64
|
+
get isOpen(): boolean;
|
|
65
|
+
get content(): CanvasContent | null;
|
|
66
|
+
open(options?: OpenCanvasOptions): boolean;
|
|
67
|
+
/** Closes the canvas. Returns false if it was not open or the close event was prevented. */
|
|
68
|
+
close(): boolean;
|
|
69
|
+
/** Closes without asking, for when the host can no longer show the canvas (e.g. it left its conversation view). */
|
|
70
|
+
forceClose(): void;
|
|
71
|
+
/** Forgets the canvas entirely, for when the thread it belonged to is gone. */
|
|
72
|
+
reset(): void;
|
|
73
|
+
/** Tool calls restored from history are marked as seen so loading a thread never auto-opens the canvas. */
|
|
74
|
+
markSeen(messages: ChatMessage[]): void;
|
|
75
|
+
readonly handleOpenRequest: (evt: CustomEvent<ForgeAiCanvasOpenRequestEventData>) => void;
|
|
76
|
+
readonly handleCloseRequest: (evt: CustomEvent<ForgeAiCanvasCloseRequestEventData>) => void;
|
|
77
|
+
/** The tool call object is replaced on every args or status update, so open content follows it by id. */
|
|
78
|
+
syncToolCall(): void;
|
|
79
|
+
/** Opens tools configured with `canvas.open: 'auto'` once their renderer shows. Call after render, not during it. */
|
|
80
|
+
autoOpenPending(): void;
|
|
81
|
+
}
|
|
@@ -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,
|
|
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,
|
|
23
|
+
get tools(): Map<string, AnyToolDefinition>;
|
|
24
24
|
get pendingAttachments(): FileAttachment[];
|
|
25
25
|
get completedAttachments(): FileAttachment[];
|
|
26
26
|
sendMessage(config: {
|
|
@@ -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 NO_UI_RESOURCE = { uiResource: void 0, uiResourceStatus: void 0 };
|
|
5
6
|
const UNSETTLED_TOOL_CALL_STATUSES = ["pending", "parsing", "executing"];
|
|
6
7
|
class ChatbotCoreController {
|
|
7
8
|
#host;
|
|
@@ -15,10 +16,14 @@ class ChatbotCoreController {
|
|
|
15
16
|
#mcpAppToolCallIds = /* @__PURE__ */ new Set();
|
|
16
17
|
/** Agent tool calls marked complete on their `tool-call` chunk, before any result arrived. */
|
|
17
18
|
#presumedCompleteToolCallIds = /* @__PURE__ */ new Set();
|
|
18
|
-
/**
|
|
19
|
+
/** UI resource fields (resolve status or resolved resource) whose tool call is not in state yet. */
|
|
19
20
|
#pendingUiResources = /* @__PURE__ */ new Map();
|
|
20
|
-
/**
|
|
21
|
-
|
|
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();
|
|
22
27
|
constructor(host, config) {
|
|
23
28
|
this.#host = host;
|
|
24
29
|
this.#callbacks = config.callbacks;
|
|
@@ -310,10 +315,11 @@ class ChatbotCoreController {
|
|
|
310
315
|
this.#messageStateController.completeToolCallInResponse(event.toolCallId, event.result, event);
|
|
311
316
|
}
|
|
312
317
|
/**
|
|
313
|
-
*
|
|
314
|
-
* the tool call's chunks, mid-run, after
|
|
315
|
-
* tool calls restored from history. A resolve
|
|
316
|
-
*
|
|
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.
|
|
317
323
|
*/
|
|
318
324
|
async #handleMcpUiResource(event) {
|
|
319
325
|
const adapter = this.#adapter;
|
|
@@ -321,7 +327,8 @@ class ChatbotCoreController {
|
|
|
321
327
|
return;
|
|
322
328
|
}
|
|
323
329
|
const { toolCallId, resourceUri } = event;
|
|
324
|
-
const
|
|
330
|
+
const token = Symbol(toolCallId);
|
|
331
|
+
this.#uiResourceResolves.set(toolCallId, token);
|
|
325
332
|
this.#mcpAppToolCallIds.add(toolCallId);
|
|
326
333
|
if (this.#presumedCompleteToolCallIds.delete(toolCallId)) {
|
|
327
334
|
this.#messageStateController.updateToolCallInResponse(toolCallId, {
|
|
@@ -329,29 +336,43 @@ class ChatbotCoreController {
|
|
|
329
336
|
endTimestamp: void 0
|
|
330
337
|
});
|
|
331
338
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
} catch (error) {
|
|
336
|
-
console.warn(`[forge-ai-mcp-app] Failed to resolve UI resource for tool call ${toolCallId}:`, error);
|
|
339
|
+
this.#stampUiResource(toolCallId, { uiResourceStatus: "loading" });
|
|
340
|
+
const fields = await this.#resolveUiResource(adapter, { resourceUri, toolCallId });
|
|
341
|
+
if (this.#uiResourceResolves.get(toolCallId) !== token) {
|
|
337
342
|
return;
|
|
338
343
|
}
|
|
339
|
-
|
|
340
|
-
|
|
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" };
|
|
341
358
|
}
|
|
359
|
+
}
|
|
360
|
+
#stampUiResource(toolCallId, fields) {
|
|
342
361
|
if (this.#messageStateController.getToolCall(toolCallId)) {
|
|
343
|
-
this.#messageStateController.updateToolCallInResponse(toolCallId,
|
|
362
|
+
this.#messageStateController.updateToolCallInResponse(toolCallId, fields);
|
|
363
|
+
} else if (fields.uiResource || fields.uiResourceStatus) {
|
|
364
|
+
this.#pendingUiResources.set(toolCallId, fields);
|
|
344
365
|
} else {
|
|
345
|
-
this.#pendingUiResources.
|
|
366
|
+
this.#pendingUiResources.delete(toolCallId);
|
|
346
367
|
}
|
|
347
368
|
}
|
|
348
369
|
#takePendingUiResource(toolCallId) {
|
|
349
|
-
const
|
|
350
|
-
if (!
|
|
370
|
+
const fields = this.#pendingUiResources.get(toolCallId);
|
|
371
|
+
if (!fields) {
|
|
351
372
|
return {};
|
|
352
373
|
}
|
|
353
374
|
this.#pendingUiResources.delete(toolCallId);
|
|
354
|
-
return
|
|
375
|
+
return fields;
|
|
355
376
|
}
|
|
356
377
|
async #sendToolResult(toolCallId, result) {
|
|
357
378
|
const toolCall = this.#messageStateController.getToolCall(toolCallId);
|
|
@@ -445,10 +466,10 @@ class ChatbotCoreController {
|
|
|
445
466
|
return this.#messageStateController.getToolCall(id);
|
|
446
467
|
}
|
|
447
468
|
clearMessages() {
|
|
448
|
-
this.#threadGeneration++;
|
|
449
469
|
this.#mcpAppToolCallIds.clear();
|
|
450
470
|
this.#presumedCompleteToolCallIds.clear();
|
|
451
471
|
this.#pendingUiResources.clear();
|
|
472
|
+
this.#uiResourceResolves.clear();
|
|
452
473
|
this.#messageStateController.clearMessages();
|
|
453
474
|
this.#fileUploadManager.clear();
|
|
454
475
|
}
|
|
@@ -456,10 +477,10 @@ class ChatbotCoreController {
|
|
|
456
477
|
return this.#messageStateController.getMessages();
|
|
457
478
|
}
|
|
458
479
|
setMessages(messages) {
|
|
459
|
-
this.#threadGeneration++;
|
|
460
480
|
this.#mcpAppToolCallIds.clear();
|
|
461
481
|
this.#presumedCompleteToolCallIds.clear();
|
|
462
482
|
this.#pendingUiResources.clear();
|
|
483
|
+
this.#uiResourceResolves.clear();
|
|
463
484
|
this.#messageStateController.setMessages(messages);
|
|
464
485
|
}
|
|
465
486
|
updateMessageContent(id, content) {
|
|
@@ -474,6 +495,7 @@ class ChatbotCoreController {
|
|
|
474
495
|
this.#mcpAppToolCallIds.delete(id);
|
|
475
496
|
this.#presumedCompleteToolCallIds.delete(id);
|
|
476
497
|
this.#pendingUiResources.delete(id);
|
|
498
|
+
this.#uiResourceResolves.delete(id);
|
|
477
499
|
}
|
|
478
500
|
}
|
|
479
501
|
setResponseFeedback(responseId, feedback) {
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/core/index.mjs
CHANGED
|
@@ -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
|
|
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
|
@@ -9,6 +9,7 @@ export * from './ai-artifact-card';
|
|
|
9
9
|
export * from './ai-artifact-card-group';
|
|
10
10
|
export * from './ai-attachment';
|
|
11
11
|
export * from './ai-button';
|
|
12
|
+
export * from './ai-canvas';
|
|
12
13
|
export * from './ai-chain-of-thought';
|
|
13
14
|
export * from './ai-chat-header';
|
|
14
15
|
export * from './ai-chat-interface';
|
package/dist/index.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { AiArtifactCardComponent, AiArtifactCardComponentTagName } from "./ai-ar
|
|
|
9
9
|
import { AiArtifactCardGroupComponent, AiArtifactCardGroupComponentTagName } from "./ai-artifact-card-group/ai-artifact-card-group.mjs";
|
|
10
10
|
import { AiAttachmentComponent, AiAttachmentComponentTagName } from "./ai-attachment/ai-attachment.mjs";
|
|
11
11
|
import { AiButtonComponent, AiButtonComponentTagName } from "./ai-button/ai-button.mjs";
|
|
12
|
+
import { AiCanvasComponent, AiCanvasComponentTagName } from "./ai-canvas/ai-canvas.mjs";
|
|
12
13
|
import { AiChainOfThoughtComponent, AiChainOfThoughtComponentTagName } from "./ai-chain-of-thought/ai-chain-of-thought.mjs";
|
|
13
14
|
import { ThoughtBaseComponent, ThoughtBaseComponentTagName } from "./ai-chain-of-thought/thought-base/thought-base.mjs";
|
|
14
15
|
import { ThoughtSearchResultComponent, ThoughtSearchResultComponentTagName } from "./ai-chain-of-thought/thought-search-result/thought-search-result.mjs";
|
|
@@ -25,6 +26,7 @@ import { HttpAgentWithCredentials } from "./ai-chatbot/http-agent-with-credentia
|
|
|
25
26
|
import { EventEmitter, SubscriptionManager } from "./ai-chatbot/event-emitter.mjs";
|
|
26
27
|
import { AgentRunner } from "./ai-chatbot/agent-runner.mjs";
|
|
27
28
|
import { createToolRenderer } from "./ai-chatbot/create-tool-renderer.mjs";
|
|
29
|
+
import { requestCanvasClose, requestCanvasOpen } from "./ai-chatbot/tool-canvas.mjs";
|
|
28
30
|
import { MessageStateController } from "./ai-chatbot/message-state-controller.mjs";
|
|
29
31
|
import { FileUploadManager } from "./ai-chatbot/file-upload-manager.mjs";
|
|
30
32
|
import { MarkdownStreamController } from "./ai-chatbot/markdown-stream-controller.mjs";
|
|
@@ -92,6 +94,8 @@ export {
|
|
|
92
94
|
AiAttachmentComponentTagName,
|
|
93
95
|
AiButtonComponent,
|
|
94
96
|
AiButtonComponentTagName,
|
|
97
|
+
AiCanvasComponent,
|
|
98
|
+
AiCanvasComponentTagName,
|
|
95
99
|
AiChainOfThoughtComponent,
|
|
96
100
|
AiChainOfThoughtComponentTagName,
|
|
97
101
|
AiChatHeaderComponent,
|
|
@@ -203,6 +207,8 @@ export {
|
|
|
203
207
|
generateId,
|
|
204
208
|
renderInlineMarkdown,
|
|
205
209
|
renderMarkdown,
|
|
210
|
+
requestCanvasClose,
|
|
211
|
+
requestCanvasOpen,
|
|
206
212
|
toRequestMessages,
|
|
207
213
|
toRequestToolCall
|
|
208
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",
|
|
@@ -28,7 +28,8 @@ declare global {
|
|
|
28
28
|
* Renders an MCP-app UI resource stamped onto a {@link ToolCall} as a sandboxed,
|
|
29
29
|
* cross-origin widget. The iframe + bridge are mounted once and survive every `toolCall`
|
|
30
30
|
* delta (Lit reuses this node at its keyed position), so args stream live without
|
|
31
|
-
* remounting.
|
|
31
|
+
* remounting. While `uiResourceStatus` is `loading` it renders a skeleton placeholder in the
|
|
32
|
+
* same position, and connects once `uiResource` is stamped onto the same element.
|
|
32
33
|
*
|
|
33
34
|
* The cross-origin sandbox is mandatory: when no `sandboxUrl` is available the element
|
|
34
35
|
* renders an error artifact rather than an insecure same-origin iframe.
|