@tambo-ai/react 0.67.0 → 0.68.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.
- package/LICENSE +21 -0
- package/README.md +2 -4
- package/dist/context-helpers/current-interactables-context-helper.d.ts.map +1 -1
- package/dist/context-helpers/current-interactables-context-helper.js +4 -1
- package/dist/context-helpers/current-interactables-context-helper.js.map +1 -1
- package/dist/hoc/with-tambo-interactable.d.ts +50 -4
- package/dist/hoc/with-tambo-interactable.d.ts.map +1 -1
- package/dist/hoc/with-tambo-interactable.js +20 -5
- package/dist/hoc/with-tambo-interactable.js.map +1 -1
- package/dist/hooks/use-component-state.d.ts +3 -8
- package/dist/hooks/use-component-state.d.ts.map +1 -1
- package/dist/hooks/use-component-state.js +8 -0
- package/dist/hooks/use-component-state.js.map +1 -1
- package/dist/hooks/use-component-state.test.js +37 -0
- package/dist/hooks/use-component-state.test.js.map +1 -1
- package/dist/hooks/use-tambo-threads.d.ts +3 -8
- package/dist/hooks/use-tambo-threads.d.ts.map +1 -1
- package/dist/hooks/use-tambo-threads.js +5 -3
- package/dist/hooks/use-tambo-threads.js.map +1 -1
- package/dist/hooks/use-tambo-threads.test.js +12 -2
- package/dist/hooks/use-tambo-threads.test.js.map +1 -1
- package/dist/mcp/mcp-constants.d.ts +19 -0
- package/dist/mcp/mcp-constants.d.ts.map +1 -0
- package/dist/mcp/mcp-constants.js +21 -0
- package/dist/mcp/mcp-constants.js.map +1 -0
- package/dist/mcp/mcp-hooks.d.ts +32 -3
- package/dist/mcp/mcp-hooks.d.ts.map +1 -1
- package/dist/mcp/mcp-hooks.js +40 -29
- package/dist/mcp/mcp-hooks.js.map +1 -1
- package/dist/mcp/mcp-hooks.test.js +8 -5
- package/dist/mcp/mcp-hooks.test.js.map +1 -1
- package/dist/mcp/tambo-mcp-provider.d.ts +7 -0
- package/dist/mcp/tambo-mcp-provider.d.ts.map +1 -1
- package/dist/mcp/tambo-mcp-provider.js +202 -155
- package/dist/mcp/tambo-mcp-provider.js.map +1 -1
- package/dist/model/component-metadata.d.ts +1 -1
- package/dist/model/component-metadata.d.ts.map +1 -1
- package/dist/model/component-metadata.js.map +1 -1
- package/dist/model/tambo-interactable.d.ts +7 -5
- package/dist/model/tambo-interactable.d.ts.map +1 -1
- package/dist/model/tambo-interactable.js.map +1 -1
- package/dist/providers/__tests__/thread-input-resource-resolution.test.d.ts +2 -0
- package/dist/providers/__tests__/thread-input-resource-resolution.test.d.ts.map +1 -0
- package/dist/providers/__tests__/thread-input-resource-resolution.test.js +592 -0
- package/dist/providers/__tests__/thread-input-resource-resolution.test.js.map +1 -0
- package/dist/providers/tambo-interactable-provider-partial-updates.test.js +22 -21
- package/dist/providers/tambo-interactable-provider-partial-updates.test.js.map +1 -1
- package/dist/providers/tambo-interactable-provider.d.ts +3 -2
- package/dist/providers/tambo-interactable-provider.d.ts.map +1 -1
- package/dist/providers/tambo-interactable-provider.js +98 -14
- package/dist/providers/tambo-interactable-provider.js.map +1 -1
- package/dist/providers/tambo-interactable-provider.test.js +242 -0
- package/dist/providers/tambo-interactable-provider.test.js.map +1 -1
- package/dist/providers/tambo-provider.d.ts +1 -2
- package/dist/providers/tambo-provider.d.ts.map +1 -1
- package/dist/providers/tambo-provider.js +10 -8
- package/dist/providers/tambo-provider.js.map +1 -1
- package/dist/providers/tambo-stubs.d.ts.map +1 -1
- package/dist/providers/tambo-stubs.js +1 -0
- package/dist/providers/tambo-stubs.js.map +1 -1
- package/dist/providers/tambo-stubs.test.js +1 -1
- package/dist/providers/tambo-stubs.test.js.map +1 -1
- package/dist/providers/tambo-thread-input-provider.d.ts +1 -6
- package/dist/providers/tambo-thread-input-provider.d.ts.map +1 -1
- package/dist/providers/tambo-thread-input-provider.js +25 -8
- package/dist/providers/tambo-thread-input-provider.js.map +1 -1
- package/dist/providers/tambo-thread-provider.d.ts +5 -0
- package/dist/providers/tambo-thread-provider.d.ts.map +1 -1
- package/dist/providers/tambo-thread-provider.js +5 -2
- package/dist/providers/tambo-thread-provider.js.map +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +2 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/json-schema.d.ts +7 -0
- package/dist/schema/json-schema.d.ts.map +1 -1
- package/dist/schema/json-schema.js +11 -0
- package/dist/schema/json-schema.js.map +1 -1
- package/dist/schema/json-schema.test.d.ts +2 -0
- package/dist/schema/json-schema.test.d.ts.map +1 -0
- package/dist/schema/json-schema.test.js +204 -0
- package/dist/schema/json-schema.test.js.map +1 -0
- package/dist/setupTests.js +3 -0
- package/dist/setupTests.js.map +1 -1
- package/dist/util/message-builder.d.ts +3 -1
- package/dist/util/message-builder.d.ts.map +1 -1
- package/dist/util/message-builder.js +20 -3
- package/dist/util/message-builder.js.map +1 -1
- package/dist/util/message-builder.test.js +269 -0
- package/dist/util/message-builder.test.js.map +1 -1
- package/dist/util/resource-content-resolver.d.ts +20 -0
- package/dist/util/resource-content-resolver.d.ts.map +1 -0
- package/dist/util/resource-content-resolver.js +93 -0
- package/dist/util/resource-content-resolver.js.map +1 -0
- package/dist/util/resource-content-resolver.test.d.ts +2 -0
- package/dist/util/resource-content-resolver.test.d.ts.map +1 -0
- package/dist/util/resource-content-resolver.test.js +254 -0
- package/dist/util/resource-content-resolver.test.js.map +1 -0
- package/esm/context-helpers/current-interactables-context-helper.d.ts.map +1 -1
- package/esm/context-helpers/current-interactables-context-helper.js +4 -1
- package/esm/context-helpers/current-interactables-context-helper.js.map +1 -1
- package/esm/hoc/with-tambo-interactable.d.ts +50 -4
- package/esm/hoc/with-tambo-interactable.d.ts.map +1 -1
- package/esm/hoc/with-tambo-interactable.js +20 -5
- package/esm/hoc/with-tambo-interactable.js.map +1 -1
- package/esm/hooks/use-component-state.d.ts +3 -8
- package/esm/hooks/use-component-state.d.ts.map +1 -1
- package/esm/hooks/use-component-state.js +8 -0
- package/esm/hooks/use-component-state.js.map +1 -1
- package/esm/hooks/use-component-state.test.js +37 -0
- package/esm/hooks/use-component-state.test.js.map +1 -1
- package/esm/hooks/use-tambo-threads.d.ts +3 -8
- package/esm/hooks/use-tambo-threads.d.ts.map +1 -1
- package/esm/hooks/use-tambo-threads.js +5 -3
- package/esm/hooks/use-tambo-threads.js.map +1 -1
- package/esm/hooks/use-tambo-threads.test.js +12 -2
- package/esm/hooks/use-tambo-threads.test.js.map +1 -1
- package/esm/mcp/mcp-constants.d.ts +19 -0
- package/esm/mcp/mcp-constants.d.ts.map +1 -0
- package/esm/mcp/mcp-constants.js +18 -0
- package/esm/mcp/mcp-constants.js.map +1 -0
- package/esm/mcp/mcp-hooks.d.ts +32 -3
- package/esm/mcp/mcp-hooks.d.ts.map +1 -1
- package/esm/mcp/mcp-hooks.js +40 -30
- package/esm/mcp/mcp-hooks.js.map +1 -1
- package/esm/mcp/mcp-hooks.test.js +8 -5
- package/esm/mcp/mcp-hooks.test.js.map +1 -1
- package/esm/mcp/tambo-mcp-provider.d.ts +7 -0
- package/esm/mcp/tambo-mcp-provider.d.ts.map +1 -1
- package/esm/mcp/tambo-mcp-provider.js +201 -154
- package/esm/mcp/tambo-mcp-provider.js.map +1 -1
- package/esm/model/component-metadata.d.ts +1 -1
- package/esm/model/component-metadata.d.ts.map +1 -1
- package/esm/model/component-metadata.js.map +1 -1
- package/esm/model/tambo-interactable.d.ts +7 -5
- package/esm/model/tambo-interactable.d.ts.map +1 -1
- package/esm/model/tambo-interactable.js.map +1 -1
- package/esm/providers/__tests__/thread-input-resource-resolution.test.d.ts +2 -0
- package/esm/providers/__tests__/thread-input-resource-resolution.test.d.ts.map +1 -0
- package/esm/providers/__tests__/thread-input-resource-resolution.test.js +587 -0
- package/esm/providers/__tests__/thread-input-resource-resolution.test.js.map +1 -0
- package/esm/providers/tambo-interactable-provider-partial-updates.test.js +22 -21
- package/esm/providers/tambo-interactable-provider-partial-updates.test.js.map +1 -1
- package/esm/providers/tambo-interactable-provider.d.ts +3 -2
- package/esm/providers/tambo-interactable-provider.d.ts.map +1 -1
- package/esm/providers/tambo-interactable-provider.js +98 -14
- package/esm/providers/tambo-interactable-provider.js.map +1 -1
- package/esm/providers/tambo-interactable-provider.test.js +242 -0
- package/esm/providers/tambo-interactable-provider.test.js.map +1 -1
- package/esm/providers/tambo-provider.d.ts +1 -2
- package/esm/providers/tambo-provider.d.ts.map +1 -1
- package/esm/providers/tambo-provider.js +11 -9
- package/esm/providers/tambo-provider.js.map +1 -1
- package/esm/providers/tambo-stubs.d.ts.map +1 -1
- package/esm/providers/tambo-stubs.js +1 -0
- package/esm/providers/tambo-stubs.js.map +1 -1
- package/esm/providers/tambo-stubs.test.js +1 -1
- package/esm/providers/tambo-stubs.test.js.map +1 -1
- package/esm/providers/tambo-thread-input-provider.d.ts +1 -6
- package/esm/providers/tambo-thread-input-provider.d.ts.map +1 -1
- package/esm/providers/tambo-thread-input-provider.js +25 -8
- package/esm/providers/tambo-thread-input-provider.js.map +1 -1
- package/esm/providers/tambo-thread-provider.d.ts +5 -0
- package/esm/providers/tambo-thread-provider.d.ts.map +1 -1
- package/esm/providers/tambo-thread-provider.js +5 -2
- package/esm/providers/tambo-thread-provider.js.map +1 -1
- package/esm/schema/index.d.ts +1 -1
- package/esm/schema/index.d.ts.map +1 -1
- package/esm/schema/index.js +1 -1
- package/esm/schema/index.js.map +1 -1
- package/esm/schema/json-schema.d.ts +7 -0
- package/esm/schema/json-schema.d.ts.map +1 -1
- package/esm/schema/json-schema.js +10 -0
- package/esm/schema/json-schema.js.map +1 -1
- package/esm/schema/json-schema.test.d.ts +2 -0
- package/esm/schema/json-schema.test.d.ts.map +1 -0
- package/esm/schema/json-schema.test.js +202 -0
- package/esm/schema/json-schema.test.js.map +1 -0
- package/esm/setupTests.js +3 -0
- package/esm/setupTests.js.map +1 -1
- package/esm/util/message-builder.d.ts +3 -1
- package/esm/util/message-builder.d.ts.map +1 -1
- package/esm/util/message-builder.js +20 -3
- package/esm/util/message-builder.js.map +1 -1
- package/esm/util/message-builder.test.js +269 -0
- package/esm/util/message-builder.test.js.map +1 -1
- package/esm/util/resource-content-resolver.d.ts +20 -0
- package/esm/util/resource-content-resolver.d.ts.map +1 -0
- package/esm/util/resource-content-resolver.js +89 -0
- package/esm/util/resource-content-resolver.js.map +1 -0
- package/esm/util/resource-content-resolver.test.d.ts +2 -0
- package/esm/util/resource-content-resolver.test.d.ts.map +1 -0
- package/esm/util/resource-content-resolver.test.js +252 -0
- package/esm/util/resource-content-resolver.test.js.map +1 -0
- package/package.json +8 -6
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const mcp_constants_1 = require("../mcp/mcp-constants");
|
|
4
|
+
const mcp_client_1 = require("../mcp/mcp-client");
|
|
5
|
+
const resource_content_resolver_1 = require("./resource-content-resolver");
|
|
6
|
+
describe("extractResourceUris", () => {
|
|
7
|
+
it("should extract a single resource URI", () => {
|
|
8
|
+
const text = "Check @registry:file:///path/to/doc.txt";
|
|
9
|
+
const result = (0, resource_content_resolver_1.extractResourceUris)(text);
|
|
10
|
+
expect(result).toEqual(["registry:file:///path/to/doc.txt"]);
|
|
11
|
+
});
|
|
12
|
+
it("should extract multiple resource URIs", () => {
|
|
13
|
+
const text = "Check @registry:file:///doc1.txt and @server-a:file:///doc2.txt";
|
|
14
|
+
const result = (0, resource_content_resolver_1.extractResourceUris)(text);
|
|
15
|
+
expect(result).toEqual([
|
|
16
|
+
"registry:file:///doc1.txt",
|
|
17
|
+
"server-a:file:///doc2.txt",
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
it("should extract resource URIs with internal server prefix", () => {
|
|
21
|
+
const text = "Check @tambo-abc123:tambo:test://resource/1";
|
|
22
|
+
const result = (0, resource_content_resolver_1.extractResourceUris)(text);
|
|
23
|
+
expect(result).toEqual(["tambo-abc123:tambo:test://resource/1"]);
|
|
24
|
+
});
|
|
25
|
+
it("should extract resource URIs with hyphens in server key", () => {
|
|
26
|
+
const text = "@my-mcp-server:file:///path/file.txt";
|
|
27
|
+
const result = (0, resource_content_resolver_1.extractResourceUris)(text);
|
|
28
|
+
expect(result).toEqual(["my-mcp-server:file:///path/file.txt"]);
|
|
29
|
+
});
|
|
30
|
+
it("should return empty array for text without resource references", () => {
|
|
31
|
+
const text = "Just some regular text without @mentions";
|
|
32
|
+
const result = (0, resource_content_resolver_1.extractResourceUris)(text);
|
|
33
|
+
expect(result).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
it("should not extract malformed references without colon", () => {
|
|
36
|
+
const text = "@server-without-colon is not a resource";
|
|
37
|
+
const result = (0, resource_content_resolver_1.extractResourceUris)(text);
|
|
38
|
+
expect(result).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
it("should handle URIs with multiple colons", () => {
|
|
41
|
+
const text = "@server:http://example.com:8080/path";
|
|
42
|
+
const result = (0, resource_content_resolver_1.extractResourceUris)(text);
|
|
43
|
+
expect(result).toEqual(["server:http://example.com:8080/path"]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe("resolveResourceContents", () => {
|
|
47
|
+
const createMockConnectedServer = (serverKey, readResource, serverType = mcp_constants_1.ServerType.BROWSER_SIDE) => ({
|
|
48
|
+
key: `mcp-${serverKey}`,
|
|
49
|
+
serverKey,
|
|
50
|
+
url: `https://${serverKey}.example.com`,
|
|
51
|
+
name: `MCP Server ${serverKey}`,
|
|
52
|
+
transport: mcp_client_1.MCPTransport.HTTP,
|
|
53
|
+
serverType,
|
|
54
|
+
client: {
|
|
55
|
+
client: {
|
|
56
|
+
readResource,
|
|
57
|
+
listResources: jest.fn().mockResolvedValue({ resources: [] }),
|
|
58
|
+
listTools: jest.fn().mockResolvedValue({ tools: [] }),
|
|
59
|
+
listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
|
|
60
|
+
getPrompt: jest.fn().mockResolvedValue(null),
|
|
61
|
+
callTool: jest.fn().mockResolvedValue(null),
|
|
62
|
+
},
|
|
63
|
+
close: jest.fn().mockResolvedValue(undefined),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const createMockInternalServer = (serverKey) => ({
|
|
67
|
+
key: serverKey,
|
|
68
|
+
serverKey,
|
|
69
|
+
url: "https://api.tambo.ai/mcp",
|
|
70
|
+
name: "__tambo_internal_mcp_server__",
|
|
71
|
+
transport: mcp_client_1.MCPTransport.HTTP,
|
|
72
|
+
serverType: mcp_constants_1.ServerType.TAMBO_INTERNAL,
|
|
73
|
+
connectionError: undefined, // No client for internal servers
|
|
74
|
+
});
|
|
75
|
+
const createMockResourceSource = (getResource) => ({
|
|
76
|
+
listResources: jest.fn().mockResolvedValue([]),
|
|
77
|
+
getResource,
|
|
78
|
+
});
|
|
79
|
+
it("should resolve registry resources via resourceSource", async () => {
|
|
80
|
+
const mockGetResource = jest.fn().mockResolvedValue({
|
|
81
|
+
contents: [
|
|
82
|
+
{
|
|
83
|
+
uri: "file:///local/doc.txt",
|
|
84
|
+
mimeType: "text/plain",
|
|
85
|
+
text: "Registry document content",
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
const resourceSource = createMockResourceSource(mockGetResource);
|
|
90
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["registry:file:///local/doc.txt"], [], // Registry resources don't need a server - they're handled directly
|
|
91
|
+
resourceSource);
|
|
92
|
+
expect(mockGetResource).toHaveBeenCalledWith("file:///local/doc.txt");
|
|
93
|
+
expect(result.size).toBe(1);
|
|
94
|
+
expect(result.get("registry:file:///local/doc.txt")).toEqual({
|
|
95
|
+
contents: [
|
|
96
|
+
{
|
|
97
|
+
uri: "file:///local/doc.txt",
|
|
98
|
+
mimeType: "text/plain",
|
|
99
|
+
text: "Registry document content",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
it("should resolve client-side MCP resources via mcpServer", async () => {
|
|
105
|
+
const mockReadResource = jest.fn().mockResolvedValue({
|
|
106
|
+
contents: [
|
|
107
|
+
{
|
|
108
|
+
uri: "file:///mcp/file.txt",
|
|
109
|
+
mimeType: "text/plain",
|
|
110
|
+
text: "MCP server content",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
const mcpServer = createMockConnectedServer("linear", mockReadResource);
|
|
115
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["linear:file:///mcp/file.txt"], [mcpServer], undefined);
|
|
116
|
+
expect(mockReadResource).toHaveBeenCalledWith({
|
|
117
|
+
uri: "file:///mcp/file.txt",
|
|
118
|
+
});
|
|
119
|
+
expect(result.size).toBe(1);
|
|
120
|
+
expect(result.get("linear:file:///mcp/file.txt")).toEqual({
|
|
121
|
+
contents: [
|
|
122
|
+
{
|
|
123
|
+
uri: "file:///mcp/file.txt",
|
|
124
|
+
mimeType: "text/plain",
|
|
125
|
+
text: "MCP server content",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
it("should skip internal server resources (serverType: TAMBO_INTERNAL)", async () => {
|
|
131
|
+
const mockGetResource = jest.fn();
|
|
132
|
+
const resourceSource = createMockResourceSource(mockGetResource);
|
|
133
|
+
const internalServer = createMockInternalServer("tambo-abc123");
|
|
134
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["tambo-abc123:tambo:test://resource/1"], [internalServer], resourceSource);
|
|
135
|
+
// Should not call getResource for internal server resources
|
|
136
|
+
expect(mockGetResource).not.toHaveBeenCalled();
|
|
137
|
+
// Should return empty map since internal resources are skipped
|
|
138
|
+
expect(result.size).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
it("should resolve multiple resources in parallel", async () => {
|
|
141
|
+
const mockGetResource = jest.fn().mockResolvedValue({
|
|
142
|
+
contents: [{ uri: "file:///doc.txt", text: "registry content" }],
|
|
143
|
+
});
|
|
144
|
+
const mockReadResource = jest.fn().mockResolvedValue({
|
|
145
|
+
contents: [{ uri: "file:///mcp.txt", text: "mcp content" }],
|
|
146
|
+
});
|
|
147
|
+
const resourceSource = createMockResourceSource(mockGetResource);
|
|
148
|
+
const mcpServer = createMockConnectedServer("mcp-server", mockReadResource);
|
|
149
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["registry:file:///doc.txt", "mcp-server:file:///mcp.txt"], [mcpServer], // Only MCP server needed - registry handled directly
|
|
150
|
+
resourceSource);
|
|
151
|
+
expect(result.size).toBe(2);
|
|
152
|
+
expect(result.has("registry:file:///doc.txt")).toBe(true);
|
|
153
|
+
expect(result.has("mcp-server:file:///mcp.txt")).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
it("should handle mixed internal and client-side resources", async () => {
|
|
156
|
+
const mockGetResource = jest.fn().mockResolvedValue({
|
|
157
|
+
contents: [{ uri: "file:///doc.txt", text: "registry content" }],
|
|
158
|
+
});
|
|
159
|
+
const resourceSource = createMockResourceSource(mockGetResource);
|
|
160
|
+
const internalServer = createMockInternalServer("tambo-abc123");
|
|
161
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)([
|
|
162
|
+
"registry:file:///doc.txt", // client-side registry
|
|
163
|
+
"tambo-abc123:tambo:test://resource/1", // internal - should skip
|
|
164
|
+
], [internalServer], // Only internal server needed - registry handled directly
|
|
165
|
+
resourceSource);
|
|
166
|
+
// Only the registry resource should be resolved
|
|
167
|
+
expect(mockGetResource).toHaveBeenCalledTimes(1);
|
|
168
|
+
expect(result.size).toBe(1);
|
|
169
|
+
expect(result.has("registry:file:///doc.txt")).toBe(true);
|
|
170
|
+
expect(result.has("tambo-abc123:tambo:test://resource/1")).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
it("should gracefully handle registry resource fetch failure", async () => {
|
|
173
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
174
|
+
const mockGetResource = jest
|
|
175
|
+
.fn()
|
|
176
|
+
.mockRejectedValue(new Error("Resource not found"));
|
|
177
|
+
const resourceSource = createMockResourceSource(mockGetResource);
|
|
178
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["registry:file:///missing.txt"], [], // Registry resources don't need a server
|
|
179
|
+
resourceSource);
|
|
180
|
+
expect(consoleSpy).toHaveBeenCalledWith("Failed to fetch resource content for registry:file:///missing.txt:", expect.any(Error));
|
|
181
|
+
expect(result.size).toBe(0);
|
|
182
|
+
consoleSpy.mockRestore();
|
|
183
|
+
});
|
|
184
|
+
it("should gracefully handle MCP resource fetch failure", async () => {
|
|
185
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
186
|
+
const mockReadResource = jest
|
|
187
|
+
.fn()
|
|
188
|
+
.mockRejectedValue(new Error("MCP server error"));
|
|
189
|
+
const mcpServer = createMockConnectedServer("linear", mockReadResource);
|
|
190
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["linear:file:///missing.txt"], [mcpServer], undefined);
|
|
191
|
+
expect(consoleSpy).toHaveBeenCalledWith("Failed to fetch resource content for linear:file:///missing.txt:", expect.any(Error));
|
|
192
|
+
expect(result.size).toBe(0);
|
|
193
|
+
consoleSpy.mockRestore();
|
|
194
|
+
});
|
|
195
|
+
it("should warn when no resourceSource available for registry resource", async () => {
|
|
196
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
197
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["registry:file:///doc.txt"], [], // Registry resources don't need a server
|
|
198
|
+
undefined);
|
|
199
|
+
expect(consoleSpy).toHaveBeenCalledWith("No resource source available to resolve registry resource: registry:file:///doc.txt");
|
|
200
|
+
expect(result.size).toBe(0);
|
|
201
|
+
consoleSpy.mockRestore();
|
|
202
|
+
});
|
|
203
|
+
it("should warn when no server found for resource", async () => {
|
|
204
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
205
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["unknown-server:file:///doc.txt"], [], // no servers
|
|
206
|
+
undefined);
|
|
207
|
+
expect(consoleSpy).toHaveBeenCalledWith("No server found for resource: unknown-server:file:///doc.txt");
|
|
208
|
+
expect(result.size).toBe(0);
|
|
209
|
+
consoleSpy.mockRestore();
|
|
210
|
+
});
|
|
211
|
+
it("should handle resource content with blob data", async () => {
|
|
212
|
+
const mockGetResource = jest.fn().mockResolvedValue({
|
|
213
|
+
contents: [
|
|
214
|
+
{
|
|
215
|
+
uri: "file:///image.png",
|
|
216
|
+
mimeType: "image/png",
|
|
217
|
+
blob: "base64encodeddata",
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
const resourceSource = createMockResourceSource(mockGetResource);
|
|
222
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["registry:file:///image.png"], [], // Registry resources don't need a server
|
|
223
|
+
resourceSource);
|
|
224
|
+
expect(result.get("registry:file:///image.png")).toEqual({
|
|
225
|
+
contents: [
|
|
226
|
+
{
|
|
227
|
+
uri: "file:///image.png",
|
|
228
|
+
mimeType: "image/png",
|
|
229
|
+
blob: "base64encodeddata",
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
it("should handle URIs with no colon (invalid format)", async () => {
|
|
235
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["invalid-uri-no-colon"], [], undefined);
|
|
236
|
+
expect(result.size).toBe(0);
|
|
237
|
+
});
|
|
238
|
+
it("should handle null content from getResource", async () => {
|
|
239
|
+
const mockGetResource = jest.fn().mockResolvedValue(null);
|
|
240
|
+
const resourceSource = createMockResourceSource(mockGetResource);
|
|
241
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["registry:file:///doc.txt"], [], // Registry resources don't need a server
|
|
242
|
+
resourceSource);
|
|
243
|
+
expect(mockGetResource).toHaveBeenCalled();
|
|
244
|
+
expect(result.size).toBe(0);
|
|
245
|
+
});
|
|
246
|
+
it("should handle null content from readResource", async () => {
|
|
247
|
+
const mockReadResource = jest.fn().mockResolvedValue(null);
|
|
248
|
+
const mcpServer = createMockConnectedServer("linear", mockReadResource);
|
|
249
|
+
const result = await (0, resource_content_resolver_1.resolveResourceContents)(["linear:file:///doc.txt"], [mcpServer], undefined);
|
|
250
|
+
expect(mockReadResource).toHaveBeenCalled();
|
|
251
|
+
expect(result.size).toBe(0);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
//# sourceMappingURL=resource-content-resolver.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-content-resolver.test.js","sourceRoot":"","sources":["../../src/util/resource-content-resolver.test.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReadResourceResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { ServerType } from \"../mcp/mcp-constants\";\nimport type { McpServer } from \"../mcp/tambo-mcp-provider\";\nimport type { ResourceSource } from \"../model/resource-info\";\nimport { MCPTransport } from \"../mcp/mcp-client\";\n\nimport {\n resolveResourceContents,\n extractResourceUris,\n} from \"./resource-content-resolver\";\n\ndescribe(\"extractResourceUris\", () => {\n it(\"should extract a single resource URI\", () => {\n const text = \"Check @registry:file:///path/to/doc.txt\";\n const result = extractResourceUris(text);\n expect(result).toEqual([\"registry:file:///path/to/doc.txt\"]);\n });\n\n it(\"should extract multiple resource URIs\", () => {\n const text =\n \"Check @registry:file:///doc1.txt and @server-a:file:///doc2.txt\";\n const result = extractResourceUris(text);\n expect(result).toEqual([\n \"registry:file:///doc1.txt\",\n \"server-a:file:///doc2.txt\",\n ]);\n });\n\n it(\"should extract resource URIs with internal server prefix\", () => {\n const text = \"Check @tambo-abc123:tambo:test://resource/1\";\n const result = extractResourceUris(text);\n expect(result).toEqual([\"tambo-abc123:tambo:test://resource/1\"]);\n });\n\n it(\"should extract resource URIs with hyphens in server key\", () => {\n const text = \"@my-mcp-server:file:///path/file.txt\";\n const result = extractResourceUris(text);\n expect(result).toEqual([\"my-mcp-server:file:///path/file.txt\"]);\n });\n\n it(\"should return empty array for text without resource references\", () => {\n const text = \"Just some regular text without @mentions\";\n const result = extractResourceUris(text);\n expect(result).toEqual([]);\n });\n\n it(\"should not extract malformed references without colon\", () => {\n const text = \"@server-without-colon is not a resource\";\n const result = extractResourceUris(text);\n expect(result).toEqual([]);\n });\n\n it(\"should handle URIs with multiple colons\", () => {\n const text = \"@server:http://example.com:8080/path\";\n const result = extractResourceUris(text);\n expect(result).toEqual([\"server:http://example.com:8080/path\"]);\n });\n});\n\ndescribe(\"resolveResourceContents\", () => {\n const createMockConnectedServer = (\n serverKey: string,\n readResource: jest.Mock,\n serverType: ServerType = ServerType.BROWSER_SIDE,\n ): McpServer =>\n ({\n key: `mcp-${serverKey}`,\n serverKey,\n url: `https://${serverKey}.example.com`,\n name: `MCP Server ${serverKey}`,\n transport: MCPTransport.HTTP,\n serverType,\n client: {\n client: {\n readResource,\n listResources: jest.fn().mockResolvedValue({ resources: [] }),\n listTools: jest.fn().mockResolvedValue({ tools: [] }),\n listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),\n getPrompt: jest.fn().mockResolvedValue(null),\n callTool: jest.fn().mockResolvedValue(null),\n },\n close: jest.fn().mockResolvedValue(undefined),\n } as unknown as McpServer[\"client\"],\n }) as McpServer;\n\n const createMockInternalServer = (serverKey: string): McpServer =>\n ({\n key: serverKey,\n serverKey,\n url: \"https://api.tambo.ai/mcp\",\n name: \"__tambo_internal_mcp_server__\",\n transport: MCPTransport.HTTP,\n serverType: ServerType.TAMBO_INTERNAL,\n connectionError: undefined, // No client for internal servers\n }) as unknown as McpServer;\n\n const createMockResourceSource = (\n getResource: jest.Mock,\n ): ResourceSource => ({\n listResources: jest.fn().mockResolvedValue([]),\n getResource,\n });\n\n it(\"should resolve registry resources via resourceSource\", async () => {\n const mockGetResource = jest.fn().mockResolvedValue({\n contents: [\n {\n uri: \"file:///local/doc.txt\",\n mimeType: \"text/plain\",\n text: \"Registry document content\",\n },\n ],\n } satisfies ReadResourceResult);\n\n const resourceSource = createMockResourceSource(mockGetResource);\n\n const result = await resolveResourceContents(\n [\"registry:file:///local/doc.txt\"],\n [], // Registry resources don't need a server - they're handled directly\n resourceSource,\n );\n\n expect(mockGetResource).toHaveBeenCalledWith(\"file:///local/doc.txt\");\n expect(result.size).toBe(1);\n expect(result.get(\"registry:file:///local/doc.txt\")).toEqual({\n contents: [\n {\n uri: \"file:///local/doc.txt\",\n mimeType: \"text/plain\",\n text: \"Registry document content\",\n },\n ],\n });\n });\n\n it(\"should resolve client-side MCP resources via mcpServer\", async () => {\n const mockReadResource = jest.fn().mockResolvedValue({\n contents: [\n {\n uri: \"file:///mcp/file.txt\",\n mimeType: \"text/plain\",\n text: \"MCP server content\",\n },\n ],\n } satisfies ReadResourceResult);\n\n const mcpServer = createMockConnectedServer(\"linear\", mockReadResource);\n\n const result = await resolveResourceContents(\n [\"linear:file:///mcp/file.txt\"],\n [mcpServer],\n undefined,\n );\n\n expect(mockReadResource).toHaveBeenCalledWith({\n uri: \"file:///mcp/file.txt\",\n });\n expect(result.size).toBe(1);\n expect(result.get(\"linear:file:///mcp/file.txt\")).toEqual({\n contents: [\n {\n uri: \"file:///mcp/file.txt\",\n mimeType: \"text/plain\",\n text: \"MCP server content\",\n },\n ],\n });\n });\n\n it(\"should skip internal server resources (serverType: TAMBO_INTERNAL)\", async () => {\n const mockGetResource = jest.fn();\n const resourceSource = createMockResourceSource(mockGetResource);\n const internalServer = createMockInternalServer(\"tambo-abc123\");\n\n const result = await resolveResourceContents(\n [\"tambo-abc123:tambo:test://resource/1\"],\n [internalServer],\n resourceSource,\n );\n\n // Should not call getResource for internal server resources\n expect(mockGetResource).not.toHaveBeenCalled();\n // Should return empty map since internal resources are skipped\n expect(result.size).toBe(0);\n });\n\n it(\"should resolve multiple resources in parallel\", async () => {\n const mockGetResource = jest.fn().mockResolvedValue({\n contents: [{ uri: \"file:///doc.txt\", text: \"registry content\" }],\n });\n const mockReadResource = jest.fn().mockResolvedValue({\n contents: [{ uri: \"file:///mcp.txt\", text: \"mcp content\" }],\n });\n\n const resourceSource = createMockResourceSource(mockGetResource);\n const mcpServer = createMockConnectedServer(\"mcp-server\", mockReadResource);\n\n const result = await resolveResourceContents(\n [\"registry:file:///doc.txt\", \"mcp-server:file:///mcp.txt\"],\n [mcpServer], // Only MCP server needed - registry handled directly\n resourceSource,\n );\n\n expect(result.size).toBe(2);\n expect(result.has(\"registry:file:///doc.txt\")).toBe(true);\n expect(result.has(\"mcp-server:file:///mcp.txt\")).toBe(true);\n });\n\n it(\"should handle mixed internal and client-side resources\", async () => {\n const mockGetResource = jest.fn().mockResolvedValue({\n contents: [{ uri: \"file:///doc.txt\", text: \"registry content\" }],\n });\n const resourceSource = createMockResourceSource(mockGetResource);\n const internalServer = createMockInternalServer(\"tambo-abc123\");\n\n const result = await resolveResourceContents(\n [\n \"registry:file:///doc.txt\", // client-side registry\n \"tambo-abc123:tambo:test://resource/1\", // internal - should skip\n ],\n [internalServer], // Only internal server needed - registry handled directly\n resourceSource,\n );\n\n // Only the registry resource should be resolved\n expect(mockGetResource).toHaveBeenCalledTimes(1);\n expect(result.size).toBe(1);\n expect(result.has(\"registry:file:///doc.txt\")).toBe(true);\n expect(result.has(\"tambo-abc123:tambo:test://resource/1\")).toBe(false);\n });\n\n it(\"should gracefully handle registry resource fetch failure\", async () => {\n const consoleSpy = jest.spyOn(console, \"warn\").mockImplementation();\n const mockGetResource = jest\n .fn()\n .mockRejectedValue(new Error(\"Resource not found\"));\n const resourceSource = createMockResourceSource(mockGetResource);\n\n const result = await resolveResourceContents(\n [\"registry:file:///missing.txt\"],\n [], // Registry resources don't need a server\n resourceSource,\n );\n\n expect(consoleSpy).toHaveBeenCalledWith(\n \"Failed to fetch resource content for registry:file:///missing.txt:\",\n expect.any(Error),\n );\n expect(result.size).toBe(0);\n\n consoleSpy.mockRestore();\n });\n\n it(\"should gracefully handle MCP resource fetch failure\", async () => {\n const consoleSpy = jest.spyOn(console, \"warn\").mockImplementation();\n const mockReadResource = jest\n .fn()\n .mockRejectedValue(new Error(\"MCP server error\"));\n const mcpServer = createMockConnectedServer(\"linear\", mockReadResource);\n\n const result = await resolveResourceContents(\n [\"linear:file:///missing.txt\"],\n [mcpServer],\n undefined,\n );\n\n expect(consoleSpy).toHaveBeenCalledWith(\n \"Failed to fetch resource content for linear:file:///missing.txt:\",\n expect.any(Error),\n );\n expect(result.size).toBe(0);\n\n consoleSpy.mockRestore();\n });\n\n it(\"should warn when no resourceSource available for registry resource\", async () => {\n const consoleSpy = jest.spyOn(console, \"warn\").mockImplementation();\n\n const result = await resolveResourceContents(\n [\"registry:file:///doc.txt\"],\n [], // Registry resources don't need a server\n undefined, // no resourceSource\n );\n\n expect(consoleSpy).toHaveBeenCalledWith(\n \"No resource source available to resolve registry resource: registry:file:///doc.txt\",\n );\n expect(result.size).toBe(0);\n\n consoleSpy.mockRestore();\n });\n\n it(\"should warn when no server found for resource\", async () => {\n const consoleSpy = jest.spyOn(console, \"warn\").mockImplementation();\n\n const result = await resolveResourceContents(\n [\"unknown-server:file:///doc.txt\"],\n [], // no servers\n undefined,\n );\n\n expect(consoleSpy).toHaveBeenCalledWith(\n \"No server found for resource: unknown-server:file:///doc.txt\",\n );\n expect(result.size).toBe(0);\n\n consoleSpy.mockRestore();\n });\n\n it(\"should handle resource content with blob data\", async () => {\n const mockGetResource = jest.fn().mockResolvedValue({\n contents: [\n {\n uri: \"file:///image.png\",\n mimeType: \"image/png\",\n blob: \"base64encodeddata\",\n },\n ],\n } satisfies ReadResourceResult);\n\n const resourceSource = createMockResourceSource(mockGetResource);\n\n const result = await resolveResourceContents(\n [\"registry:file:///image.png\"],\n [], // Registry resources don't need a server\n resourceSource,\n );\n\n expect(result.get(\"registry:file:///image.png\")).toEqual({\n contents: [\n {\n uri: \"file:///image.png\",\n mimeType: \"image/png\",\n blob: \"base64encodeddata\",\n },\n ],\n });\n });\n\n it(\"should handle URIs with no colon (invalid format)\", async () => {\n const result = await resolveResourceContents(\n [\"invalid-uri-no-colon\"],\n [],\n undefined,\n );\n\n expect(result.size).toBe(0);\n });\n\n it(\"should handle null content from getResource\", async () => {\n const mockGetResource = jest.fn().mockResolvedValue(null);\n const resourceSource = createMockResourceSource(mockGetResource);\n\n const result = await resolveResourceContents(\n [\"registry:file:///doc.txt\"],\n [], // Registry resources don't need a server\n resourceSource,\n );\n\n expect(mockGetResource).toHaveBeenCalled();\n expect(result.size).toBe(0);\n });\n\n it(\"should handle null content from readResource\", async () => {\n const mockReadResource = jest.fn().mockResolvedValue(null);\n const mcpServer = createMockConnectedServer(\"linear\", mockReadResource);\n\n const result = await resolveResourceContents(\n [\"linear:file:///doc.txt\"],\n [mcpServer],\n undefined,\n );\n\n expect(mockReadResource).toHaveBeenCalled();\n expect(result.size).toBe(0);\n });\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"current-interactables-context-helper.d.ts","sourceRoot":"","sources":["../../src/context-helpers/current-interactables-context-helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAI/C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC3C,YAAY,GAAG,EAAE,KAChB,
|
|
1
|
+
{"version":3,"file":"current-interactables-context-helper.d.ts","sourceRoot":"","sources":["../../src/context-helpers/current-interactables-context-helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAI/C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC3C,YAAY,GAAG,EAAE,KAChB,eAwBF,CAAC"}
|
|
@@ -33,7 +33,7 @@ export const createInteractablesContextHelper = (components) => {
|
|
|
33
33
|
return null; // No interactable components on the page
|
|
34
34
|
}
|
|
35
35
|
return {
|
|
36
|
-
description: "These are the interactable components currently visible on the page that you can read and modify. Each component has an id, componentName, current props, current state,and optional
|
|
36
|
+
description: "These are the interactable components currently visible on the page that you can read and modify. Each component has an id, componentName, current props, current state, and optional schemas. You can use tools to update these components' props and state on behalf of the user. Don't tell the user the ID of the components, only the name, unless they ask for it.",
|
|
37
37
|
components: components.map((component) => ({
|
|
38
38
|
id: component.id,
|
|
39
39
|
componentName: component.name,
|
|
@@ -43,6 +43,9 @@ export const createInteractablesContextHelper = (components) => {
|
|
|
43
43
|
? "Available - use component-specific update tools"
|
|
44
44
|
: "Not specified",
|
|
45
45
|
state: component.state,
|
|
46
|
+
stateSchema: component.stateSchema
|
|
47
|
+
? "Available - use component-specific update tools"
|
|
48
|
+
: "Not specified",
|
|
46
49
|
})),
|
|
47
50
|
};
|
|
48
51
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"current-interactables-context-helper.js","sourceRoot":"","sources":["../../src/context-helpers/current-interactables-context-helper.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAoB,GAAG,EAAE;IACrE,mFAAmF;IACnF,wEAAwE;IACxE,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAC9C,UAAiB,EACA,EAAE;IACnB,OAAO,GAAG,EAAE;QACV,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1D,OAAO,IAAI,CAAC,CAAC,yCAAyC;QACxD,CAAC;QAED,OAAO;YACL,WAAW,EACT,
|
|
1
|
+
{"version":3,"file":"current-interactables-context-helper.js","sourceRoot":"","sources":["../../src/context-helpers/current-interactables-context-helper.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAoB,GAAG,EAAE;IACrE,mFAAmF;IACnF,wEAAwE;IACxE,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAC9C,UAAiB,EACA,EAAE;IACnB,OAAO,GAAG,EAAE;QACV,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1D,OAAO,IAAI,CAAC,CAAC,yCAAyC;QACxD,CAAC;QAED,OAAO;YACL,WAAW,EACT,0WAA0W;YAC5W,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;gBACzC,EAAE,EAAE,SAAS,CAAC,EAAE;gBAChB,aAAa,EAAE,SAAS,CAAC,IAAI;gBAC7B,WAAW,EAAE,SAAS,CAAC,WAAW;gBAClC,KAAK,EAAE,SAAS,CAAC,KAAK;gBACtB,WAAW,EAAE,SAAS,CAAC,WAAW;oBAChC,CAAC,CAAC,iDAAiD;oBACnD,CAAC,CAAC,eAAe;gBACnB,KAAK,EAAE,SAAS,CAAC,KAAK;gBACtB,WAAW,EAAE,SAAS,CAAC,WAAW;oBAChC,CAAC,CAAC,iDAAiD;oBACnD,CAAC,CAAC,eAAe;aACpB,CAAC,CAAC;SACJ,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { ContextHelperFn } from \"./types\";\n\n/**\n * Prebuilt context helper that provides information about all interactable components currently on the page.\n * This gives the AI awareness of what components it can interact with and their current state.\n * @returns an object with description and components, or null to skip including this context.\n * To disable this helper, override it with a function that returns null:\n * @example\n * ```tsx\n * // To disable the default interactables context\n * const { addContextHelper } = useTamboContextHelpers();\n * addContextHelper(\"interactables\", () => null);\n *\n * // To customize the context\n * addContextHelper(\"interactables\", () => ({\n * description: \"Custom description\",\n * components: getCustomComponentsSubset()\n * }));\n * ```\n */\nexport const currentInteractablesContextHelper: ContextHelperFn = () => {\n // This will be provided by the interactable provider when it registers this helper\n // Since we're provider-only now, this function gets replaced at runtime\n return null;\n};\n\n/**\n * Creates an interactables context helper with access to the current components.\n * This is used internally by TamboInteractableProvider.\n * @param components Array of interactable components\n * @returns Context helper function\n */\nexport const createInteractablesContextHelper = (\n components: any[],\n): ContextHelperFn => {\n return () => {\n if (!Array.isArray(components) || components.length === 0) {\n return null; // No interactable components on the page\n }\n\n return {\n description:\n \"These are the interactable components currently visible on the page that you can read and modify. Each component has an id, componentName, current props, current state, and optional schemas. You can use tools to update these components' props and state on behalf of the user. Don't tell the user the ID of the components, only the name, unless they ask for it.\",\n components: components.map((component) => ({\n id: component.id,\n componentName: component.name,\n description: component.description,\n props: component.props,\n propsSchema: component.propsSchema\n ? \"Available - use component-specific update tools\"\n : \"Not specified\",\n state: component.state,\n stateSchema: component.stateSchema\n ? \"Available - use component-specific update tools\"\n : \"Not specified\",\n })),\n };\n };\n};\n"]}
|
|
@@ -1,14 +1,47 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { SupportedSchema } from "../schema";
|
|
3
|
-
export interface InteractableConfig {
|
|
3
|
+
export interface InteractableConfig<Props = Record<string, unknown>, State = Record<string, unknown>> {
|
|
4
|
+
/**
|
|
5
|
+
* The name of the component, used for identification in Tambo.
|
|
6
|
+
*/
|
|
4
7
|
componentName: string;
|
|
8
|
+
/**
|
|
9
|
+
* A brief description of the component's purpose and functionality. LLM will
|
|
10
|
+
* use this to understand how to interact with it.
|
|
11
|
+
*/
|
|
5
12
|
description: string;
|
|
6
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Optional schema for component props. If provided, prop updates will be
|
|
15
|
+
* validated against this schema.
|
|
16
|
+
*/
|
|
17
|
+
propsSchema?: SupportedSchema<Props>;
|
|
18
|
+
/**
|
|
19
|
+
* Optional schema for component state. If provided, state updates will be
|
|
20
|
+
* validated against this schema.
|
|
21
|
+
*/
|
|
22
|
+
stateSchema?: SupportedSchema<State>;
|
|
7
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Props injected by withTamboInteractable HOC. These can be passed to the wrapped
|
|
26
|
+
* component to customize interactable behavior.
|
|
27
|
+
*/
|
|
8
28
|
export interface WithTamboInteractableProps {
|
|
29
|
+
/**
|
|
30
|
+
* Optional ID to use for this interactable component instance.
|
|
31
|
+
* If not provided, a unique ID will be generated automatically.
|
|
32
|
+
*/
|
|
9
33
|
interactableId?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Callback fired when the component has been registered as interactable.
|
|
36
|
+
* @param id - The assigned interactable component ID
|
|
37
|
+
*/
|
|
10
38
|
onInteractableReady?: (id: string) => void;
|
|
11
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Callback fired when the component's serializable props are updated by Tambo
|
|
41
|
+
* through a tool call. Note: Only serializable props are tracked.
|
|
42
|
+
* @param newProps - The updated serializable props
|
|
43
|
+
*/
|
|
44
|
+
onPropsUpdate?: (newProps: Record<string, unknown>) => void;
|
|
12
45
|
}
|
|
13
46
|
/**
|
|
14
47
|
* Higher-Order Component that makes any component interactable by tambo.
|
|
@@ -17,6 +50,16 @@ export interface WithTamboInteractableProps {
|
|
|
17
50
|
* @returns A new component that is automatically registered as interactable
|
|
18
51
|
* @example
|
|
19
52
|
* ```tsx
|
|
53
|
+
* const MyNote: React.FC<{ title: string; content: string }> = ({ title, content }) => {
|
|
54
|
+
* const [isPinned, setIsPinned] = useTamboComponentState("isPinned", false);
|
|
55
|
+
* return (
|
|
56
|
+
* <div style={{ border: isPinned ? "2px solid gold" : "1px solid gray", order: isPinned ? -1 : 0 }}>
|
|
57
|
+
* <h2>{title}</h2>
|
|
58
|
+
* <p>{content}</p>
|
|
59
|
+
* </div>
|
|
60
|
+
* );
|
|
61
|
+
* };
|
|
62
|
+
*
|
|
20
63
|
* const MyInteractableNote = withTamboInteractable(MyNote, {
|
|
21
64
|
* componentName: "MyNote",
|
|
22
65
|
* description: "A note component",
|
|
@@ -24,11 +67,14 @@ export interface WithTamboInteractableProps {
|
|
|
24
67
|
* title: z.string(),
|
|
25
68
|
* content: z.string(),
|
|
26
69
|
* }),
|
|
70
|
+
* stateSchema: z.object({
|
|
71
|
+
* isPinned: z.boolean(),
|
|
72
|
+
* }),
|
|
27
73
|
* });
|
|
28
74
|
*
|
|
29
75
|
* // Usage
|
|
30
76
|
* <MyInteractableNote title="My Note" content="This is my note" />
|
|
31
77
|
* ```
|
|
32
78
|
*/
|
|
33
|
-
export declare function withTamboInteractable<
|
|
79
|
+
export declare function withTamboInteractable<ComponentProps extends object>(WrappedComponent: React.ComponentType<ComponentProps>, config: InteractableConfig): React.FC<ComponentProps & WithTamboInteractableProps>;
|
|
34
80
|
//# sourceMappingURL=with-tambo-interactable.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"with-tambo-interactable.d.ts","sourceRoot":"","sources":["../../src/hoc/with-tambo-interactable.tsx"],"names":[],"mappings":"AACA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAIxE,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,WAAW,kBAAkB;
|
|
1
|
+
{"version":3,"file":"with-tambo-interactable.d.ts","sourceRoot":"","sources":["../../src/hoc/with-tambo-interactable.tsx"],"names":[],"mappings":"AACA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAIxE,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,MAAM,WAAW,kBAAkB,CACjC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAE/B;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC;IACrC;;;OAGG;IACH,WAAW,CAAC,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC;CACtC;AAED;;;GAGG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC7D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,qBAAqB,CAAC,cAAc,SAAS,MAAM,EACjE,gBAAgB,EAAE,KAAK,CAAC,aAAa,CAAC,cAAc,CAAC,EACrD,MAAM,EAAE,kBAAkB,yDAsH3B"}
|
|
@@ -9,6 +9,16 @@ import { useTamboInteractable } from "../providers/tambo-interactable-provider";
|
|
|
9
9
|
* @returns A new component that is automatically registered as interactable
|
|
10
10
|
* @example
|
|
11
11
|
* ```tsx
|
|
12
|
+
* const MyNote: React.FC<{ title: string; content: string }> = ({ title, content }) => {
|
|
13
|
+
* const [isPinned, setIsPinned] = useTamboComponentState("isPinned", false);
|
|
14
|
+
* return (
|
|
15
|
+
* <div style={{ border: isPinned ? "2px solid gold" : "1px solid gray", order: isPinned ? -1 : 0 }}>
|
|
16
|
+
* <h2>{title}</h2>
|
|
17
|
+
* <p>{content}</p>
|
|
18
|
+
* </div>
|
|
19
|
+
* );
|
|
20
|
+
* };
|
|
21
|
+
*
|
|
12
22
|
* const MyInteractableNote = withTamboInteractable(MyNote, {
|
|
13
23
|
* componentName: "MyNote",
|
|
14
24
|
* description: "A note component",
|
|
@@ -16,6 +26,9 @@ import { useTamboInteractable } from "../providers/tambo-interactable-provider";
|
|
|
16
26
|
* title: z.string(),
|
|
17
27
|
* content: z.string(),
|
|
18
28
|
* }),
|
|
29
|
+
* stateSchema: z.object({
|
|
30
|
+
* isPinned: z.boolean(),
|
|
31
|
+
* }),
|
|
19
32
|
* });
|
|
20
33
|
*
|
|
21
34
|
* // Usage
|
|
@@ -28,9 +41,10 @@ export function withTamboInteractable(WrappedComponent, config) {
|
|
|
28
41
|
const { addInteractableComponent, updateInteractableComponentProps, getInteractableComponent, } = useTamboInteractable();
|
|
29
42
|
const [interactableId, setInteractableId] = useState(null);
|
|
30
43
|
const isInitialized = useRef(false);
|
|
31
|
-
const
|
|
32
|
-
// Extract interactable-specific props
|
|
33
|
-
const {
|
|
44
|
+
const lastSerializedProps = useRef({});
|
|
45
|
+
// Extract interactable-specific props from component props
|
|
46
|
+
const { interactableId: _providedId, // Reserved for future use
|
|
47
|
+
onInteractableReady, onPropsUpdate, ...componentProps } = props;
|
|
34
48
|
// Get the current interactable component to track prop updates
|
|
35
49
|
const currentInteractable = interactableId
|
|
36
50
|
? getInteractableComponent(interactableId)
|
|
@@ -47,6 +61,7 @@ export function withTamboInteractable(WrappedComponent, config) {
|
|
|
47
61
|
component: WrappedComponent,
|
|
48
62
|
props: componentProps,
|
|
49
63
|
propsSchema: config.propsSchema,
|
|
64
|
+
stateSchema: config.stateSchema,
|
|
50
65
|
});
|
|
51
66
|
setInteractableId(id);
|
|
52
67
|
onInteractableReady?.(id);
|
|
@@ -61,12 +76,12 @@ export function withTamboInteractable(WrappedComponent, config) {
|
|
|
61
76
|
useEffect(() => {
|
|
62
77
|
if (interactableId && isInitialized.current) {
|
|
63
78
|
// Only update if the props are different from what we last sent
|
|
64
|
-
const lastPropsString = JSON.stringify(
|
|
79
|
+
const lastPropsString = JSON.stringify(lastSerializedProps.current);
|
|
65
80
|
const currentPropsString = JSON.stringify(componentProps);
|
|
66
81
|
if (lastPropsString !== currentPropsString) {
|
|
67
82
|
updateInteractableComponentProps(interactableId, componentProps);
|
|
68
83
|
onPropsUpdate?.(componentProps);
|
|
69
|
-
|
|
84
|
+
lastSerializedProps.current = componentProps;
|
|
70
85
|
}
|
|
71
86
|
}
|
|
72
87
|
}, [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"with-tambo-interactable.js","sourceRoot":"","sources":["../../src/hoc/with-tambo-interactable.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { TamboMessageProvider } from \"../hooks/use-current-message\";\nimport { TamboThreadMessage } from \"../model/generate-component-response\";\nimport { useTamboInteractable } from \"../providers/tambo-interactable-provider\";\nimport { SupportedSchema } from \"../schema\";\n\nexport interface InteractableConfig {\n componentName: string;\n description: string;\n propsSchema?: SupportedSchema;\n}\n\nexport interface WithTamboInteractableProps {\n interactableId?: string;\n onInteractableReady?: (id: string) => void;\n onPropsUpdate?: (newProps: Record<string, any>) => void;\n}\n\n/**\n * Higher-Order Component that makes any component interactable by tambo.\n * @param WrappedComponent - The component to make interactable\n * @param config - Configuration for the interactable component\n * @returns A new component that is automatically registered as interactable\n * @example\n * ```tsx\n * const MyInteractableNote = withTamboInteractable(MyNote, {\n * componentName: \"MyNote\",\n * description: \"A note component\",\n * propsSchema: z.object({\n * title: z.string(),\n * content: z.string(),\n * }),\n * });\n *\n * // Usage\n * <MyInteractableNote title=\"My Note\" content=\"This is my note\" />\n * ```\n */\nexport function withTamboInteractable<P extends object>(\n WrappedComponent: React.ComponentType<P>,\n config: InteractableConfig,\n) {\n const displayName =\n WrappedComponent.displayName ?? WrappedComponent.name ?? \"Component\";\n\n const TamboInteractableWrapper: React.FC<P & WithTamboInteractableProps> = (\n props,\n ) => {\n const {\n addInteractableComponent,\n updateInteractableComponentProps,\n getInteractableComponent,\n } = useTamboInteractable();\n\n const [interactableId, setInteractableId] = useState<string | null>(null);\n const isInitialized = useRef(false);\n const lastParentProps = useRef<Record<string, any>>({});\n\n // Extract interactable-specific props\n const { onInteractableReady, onPropsUpdate, ...componentProps } = props;\n\n // Get the current interactable component to track prop updates\n const currentInteractable = interactableId\n ? getInteractableComponent(interactableId)\n : null;\n\n // Use the props from the interactable component if available, otherwise use the passed props\n // We need to be careful not to create a loop, so we only use stored props if they're different from passed props\n const effectiveProps = currentInteractable?.props ?? componentProps;\n\n // Memoize the registration function\n const registerComponent = useCallback(() => {\n if (!isInitialized.current) {\n const id = addInteractableComponent({\n name: config.componentName,\n description: config.description,\n component: WrappedComponent,\n props: componentProps,\n propsSchema: config.propsSchema,\n });\n\n setInteractableId(id);\n onInteractableReady?.(id);\n isInitialized.current = true;\n }\n }, [addInteractableComponent, componentProps, onInteractableReady]);\n\n // Register the component as interactable on mount (only once)\n useEffect(() => {\n registerComponent();\n }, [registerComponent]);\n\n // Update the interactable component when props change from parent\n useEffect(() => {\n if (interactableId && isInitialized.current) {\n // Only update if the props are different from what we last sent\n const lastPropsString = JSON.stringify(lastParentProps.current);\n const currentPropsString = JSON.stringify(componentProps);\n\n if (lastPropsString !== currentPropsString) {\n updateInteractableComponentProps(interactableId, componentProps);\n onPropsUpdate?.(componentProps);\n lastParentProps.current = componentProps;\n }\n }\n }, [\n interactableId,\n componentProps,\n updateInteractableComponentProps,\n onPropsUpdate,\n ]);\n\n // If the interactable ID is not yet set, render the component without provider\n if (!interactableId) {\n return <WrappedComponent {...(effectiveProps as P)} />;\n }\n\n // Create a minimal message with interactable metadata\n // This allows useTamboCurrentComponent to work with standalone interactable components\n const minimalMessage: TamboThreadMessage = {\n id: interactableId,\n role: \"assistant\" as const,\n content: [],\n threadId: \"\",\n createdAt: new Date().toISOString(),\n component: {\n componentName: config.componentName,\n componentState: {},\n message: \"\",\n props: effectiveProps,\n },\n componentState: {},\n };\n\n // Wrap with TamboMessageProvider including interactable metadata\n return (\n <TamboMessageProvider\n message={minimalMessage}\n interactableMetadata={{\n id: interactableId,\n componentName: config.componentName,\n description: config.description,\n }}\n >\n <WrappedComponent {...(effectiveProps as P)} />\n </TamboMessageProvider>\n );\n };\n\n TamboInteractableWrapper.displayName = `withTamboInteractable(${displayName})`;\n\n return TamboInteractableWrapper;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"with-tambo-interactable.js","sourceRoot":"","sources":["../../src/hoc/with-tambo-interactable.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { TamboMessageProvider } from \"../hooks/use-current-message\";\nimport { TamboThreadMessage } from \"../model/generate-component-response\";\nimport { useTamboInteractable } from \"../providers/tambo-interactable-provider\";\nimport { SupportedSchema } from \"../schema\";\n\nexport interface InteractableConfig<\n Props = Record<string, unknown>,\n State = Record<string, unknown>,\n> {\n /**\n * The name of the component, used for identification in Tambo.\n */\n componentName: string;\n /**\n * A brief description of the component's purpose and functionality. LLM will\n * use this to understand how to interact with it.\n */\n description: string;\n /**\n * Optional schema for component props. If provided, prop updates will be\n * validated against this schema.\n */\n propsSchema?: SupportedSchema<Props>;\n /**\n * Optional schema for component state. If provided, state updates will be\n * validated against this schema.\n */\n stateSchema?: SupportedSchema<State>;\n}\n\n/**\n * Props injected by withTamboInteractable HOC. These can be passed to the wrapped\n * component to customize interactable behavior.\n */\nexport interface WithTamboInteractableProps {\n /**\n * Optional ID to use for this interactable component instance.\n * If not provided, a unique ID will be generated automatically.\n */\n interactableId?: string;\n /**\n * Callback fired when the component has been registered as interactable.\n * @param id - The assigned interactable component ID\n */\n onInteractableReady?: (id: string) => void;\n /**\n * Callback fired when the component's serializable props are updated by Tambo\n * through a tool call. Note: Only serializable props are tracked.\n * @param newProps - The updated serializable props\n */\n onPropsUpdate?: (newProps: Record<string, unknown>) => void;\n}\n\n/**\n * Higher-Order Component that makes any component interactable by tambo.\n * @param WrappedComponent - The component to make interactable\n * @param config - Configuration for the interactable component\n * @returns A new component that is automatically registered as interactable\n * @example\n * ```tsx\n * const MyNote: React.FC<{ title: string; content: string }> = ({ title, content }) => {\n * const [isPinned, setIsPinned] = useTamboComponentState(\"isPinned\", false);\n * return (\n * <div style={{ border: isPinned ? \"2px solid gold\" : \"1px solid gray\", order: isPinned ? -1 : 0 }}>\n * <h2>{title}</h2>\n * <p>{content}</p>\n * </div>\n * );\n * };\n *\n * const MyInteractableNote = withTamboInteractable(MyNote, {\n * componentName: \"MyNote\",\n * description: \"A note component\",\n * propsSchema: z.object({\n * title: z.string(),\n * content: z.string(),\n * }),\n * stateSchema: z.object({\n * isPinned: z.boolean(),\n * }),\n * });\n *\n * // Usage\n * <MyInteractableNote title=\"My Note\" content=\"This is my note\" />\n * ```\n */\nexport function withTamboInteractable<ComponentProps extends object>(\n WrappedComponent: React.ComponentType<ComponentProps>,\n config: InteractableConfig,\n) {\n const displayName =\n WrappedComponent.displayName ?? WrappedComponent.name ?? \"Component\";\n\n const TamboInteractableWrapper: React.FC<\n ComponentProps & WithTamboInteractableProps\n > = (props) => {\n const {\n addInteractableComponent,\n updateInteractableComponentProps,\n getInteractableComponent,\n } = useTamboInteractable();\n\n const [interactableId, setInteractableId] = useState<string | null>(null);\n const isInitialized = useRef(false);\n const lastSerializedProps = useRef<Record<string, unknown>>({});\n\n // Extract interactable-specific props from component props\n const {\n interactableId: _providedId, // Reserved for future use\n onInteractableReady,\n onPropsUpdate,\n ...componentProps\n } = props;\n\n // Get the current interactable component to track prop updates\n const currentInteractable = interactableId\n ? getInteractableComponent(interactableId)\n : null;\n\n // Use the props from the interactable component if available, otherwise use the passed props\n // We need to be careful not to create a loop, so we only use stored props if they're different from passed props\n const effectiveProps = currentInteractable?.props ?? componentProps;\n\n // Memoize the registration function\n const registerComponent = useCallback(() => {\n if (!isInitialized.current) {\n const id = addInteractableComponent({\n name: config.componentName,\n description: config.description,\n component: WrappedComponent,\n props: componentProps,\n propsSchema: config.propsSchema,\n stateSchema: config.stateSchema,\n });\n\n setInteractableId(id);\n onInteractableReady?.(id);\n isInitialized.current = true;\n }\n }, [addInteractableComponent, componentProps, onInteractableReady]);\n\n // Register the component as interactable on mount (only once)\n useEffect(() => {\n registerComponent();\n }, [registerComponent]);\n\n // Update the interactable component when props change from parent\n useEffect(() => {\n if (interactableId && isInitialized.current) {\n // Only update if the props are different from what we last sent\n const lastPropsString = JSON.stringify(lastSerializedProps.current);\n const currentPropsString = JSON.stringify(componentProps);\n\n if (lastPropsString !== currentPropsString) {\n updateInteractableComponentProps(interactableId, componentProps);\n onPropsUpdate?.(componentProps);\n lastSerializedProps.current = componentProps;\n }\n }\n }, [\n interactableId,\n componentProps,\n updateInteractableComponentProps,\n onPropsUpdate,\n ]);\n\n // If the interactable ID is not yet set, render the component without provider\n if (!interactableId) {\n return <WrappedComponent {...(effectiveProps as ComponentProps)} />;\n }\n\n // Create a minimal message with interactable metadata\n // This allows useTamboCurrentComponent to work with standalone interactable components\n const minimalMessage: TamboThreadMessage = {\n id: interactableId,\n role: \"assistant\" as const,\n content: [],\n threadId: \"\",\n createdAt: new Date().toISOString(),\n component: {\n componentName: config.componentName,\n componentState: {},\n message: \"\",\n props: effectiveProps,\n },\n componentState: {},\n };\n\n // Wrap with TamboMessageProvider including interactable metadata\n return (\n <TamboMessageProvider\n message={minimalMessage}\n interactableMetadata={{\n id: interactableId,\n componentName: config.componentName,\n description: config.description,\n }}\n >\n <WrappedComponent {...(effectiveProps as ComponentProps)} />\n </TamboMessageProvider>\n );\n };\n\n TamboInteractableWrapper.displayName = `withTamboInteractable(${displayName})`;\n\n return TamboInteractableWrapper;\n}\n"]}
|
|
@@ -11,11 +11,11 @@ type StateUpdateResult<T> = [currentState: T, setState: (newState: T) => void];
|
|
|
11
11
|
* @param initialValue - Optional initial value for the state, used if no componentState value exists in the Tambo message containing this hook usage.
|
|
12
12
|
* @param setFromProp - Optional value used to set the state value, only while no componentState value exists in the Tambo message containing this hook usage. Use this to allow streaming updates from a prop to the state value.
|
|
13
13
|
* @param debounceTime - Optional debounce time in milliseconds (default: 500ms) to limit API calls.
|
|
14
|
-
* @returns A tuple
|
|
15
|
-
* - The current state value
|
|
16
|
-
* - A setter function to update the state (updates UI immediately, debounces server sync)
|
|
14
|
+
* @returns A tuple of [currentState, setState] similar to React's useState
|
|
17
15
|
* @example
|
|
16
|
+
* ```tsx
|
|
18
17
|
* const [count, setCount] = useTamboComponentState("counter", 0);
|
|
18
|
+
* ```
|
|
19
19
|
*
|
|
20
20
|
* Use `setFromProp` to seed state from streamed props. During streaming,
|
|
21
21
|
* state updates as new prop values arrive. Once streaming completes,
|
|
@@ -23,11 +23,6 @@ type StateUpdateResult<T> = [currentState: T, setState: (newState: T) => void];
|
|
|
23
23
|
*
|
|
24
24
|
* Pair with `useTamboStreamStatus` to disable inputs while streaming.
|
|
25
25
|
* @see {@link https://docs.tambo.co/concepts/streaming/streaming-best-practices}
|
|
26
|
-
* @param keyName - Unique key within the message's componentState
|
|
27
|
-
* @param initialValue - Default value if no componentState exists
|
|
28
|
-
* @param setFromProp - Seeds state from props (updates during streaming, then user edits take over)
|
|
29
|
-
* @param debounceTime - Server sync debounce in ms (default: 500)
|
|
30
|
-
* @returns A tuple of [currentState, setState] similar to React's useState
|
|
31
26
|
*/
|
|
32
27
|
export declare function useTamboComponentState<S = undefined>(keyName: string, initialValue?: S, setFromProp?: S, debounceTime?: number): StateUpdateResult<S | undefined>;
|
|
33
28
|
export declare function useTamboComponentState<S>(keyName: string, initialValue: S, setFromProp?: S, debounceTime?: number): StateUpdateResult<S>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-component-state.d.ts","sourceRoot":"","sources":["../../src/hooks/use-component-state.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-component-state.d.ts","sourceRoot":"","sources":["../../src/hooks/use-component-state.tsx"],"names":[],"mappings":"AAQA,KAAK,iBAAiB,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,GAAG,SAAS,EAClD,OAAO,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,CAAC,EAChB,WAAW,CAAC,EAAE,CAAC,EACf,YAAY,CAAC,EAAE,MAAM,GACpB,iBAAiB,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;AACpC,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,CAAC,EACf,WAAW,CAAC,EAAE,CAAC,EACf,YAAY,CAAC,EAAE,MAAM,GACpB,iBAAiB,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { deepEqual } from "fast-equals";
|
|
2
3
|
import { useCallback, useContext, useEffect, useState } from "react";
|
|
3
4
|
import { useDebouncedCallback } from "use-debounce";
|
|
4
5
|
import { useTamboClient, useTamboThread } from "..";
|
|
@@ -98,6 +99,13 @@ export function useTamboComponentState(keyName, initialValue, setFromProp, debou
|
|
|
98
99
|
setInteractableState,
|
|
99
100
|
componentId,
|
|
100
101
|
]);
|
|
102
|
+
// Sync from interactable provider to local state when state changes externally (e.g., from Tambo tool call)
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!componentId)
|
|
105
|
+
return;
|
|
106
|
+
// only update if different
|
|
107
|
+
setLocalState((prev) => deepEqual(prev, interactableState) ? prev : interactableState);
|
|
108
|
+
}, [componentId, interactableState]);
|
|
101
109
|
// For editable fields that are set from a prop to allow streaming updates, don't overwrite a fetched state value set from the thread message with prop value on initial load.
|
|
102
110
|
useEffect(() => {
|
|
103
111
|
if (setFromProp !== undefined && !initializedFromThreadMessage) {
|