@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.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -4
  3. package/dist/context-helpers/current-interactables-context-helper.d.ts.map +1 -1
  4. package/dist/context-helpers/current-interactables-context-helper.js +4 -1
  5. package/dist/context-helpers/current-interactables-context-helper.js.map +1 -1
  6. package/dist/hoc/with-tambo-interactable.d.ts +50 -4
  7. package/dist/hoc/with-tambo-interactable.d.ts.map +1 -1
  8. package/dist/hoc/with-tambo-interactable.js +20 -5
  9. package/dist/hoc/with-tambo-interactable.js.map +1 -1
  10. package/dist/hooks/use-component-state.d.ts +3 -8
  11. package/dist/hooks/use-component-state.d.ts.map +1 -1
  12. package/dist/hooks/use-component-state.js +8 -0
  13. package/dist/hooks/use-component-state.js.map +1 -1
  14. package/dist/hooks/use-component-state.test.js +37 -0
  15. package/dist/hooks/use-component-state.test.js.map +1 -1
  16. package/dist/hooks/use-tambo-threads.d.ts +3 -8
  17. package/dist/hooks/use-tambo-threads.d.ts.map +1 -1
  18. package/dist/hooks/use-tambo-threads.js +5 -3
  19. package/dist/hooks/use-tambo-threads.js.map +1 -1
  20. package/dist/hooks/use-tambo-threads.test.js +12 -2
  21. package/dist/hooks/use-tambo-threads.test.js.map +1 -1
  22. package/dist/mcp/mcp-constants.d.ts +19 -0
  23. package/dist/mcp/mcp-constants.d.ts.map +1 -0
  24. package/dist/mcp/mcp-constants.js +21 -0
  25. package/dist/mcp/mcp-constants.js.map +1 -0
  26. package/dist/mcp/mcp-hooks.d.ts +32 -3
  27. package/dist/mcp/mcp-hooks.d.ts.map +1 -1
  28. package/dist/mcp/mcp-hooks.js +40 -29
  29. package/dist/mcp/mcp-hooks.js.map +1 -1
  30. package/dist/mcp/mcp-hooks.test.js +8 -5
  31. package/dist/mcp/mcp-hooks.test.js.map +1 -1
  32. package/dist/mcp/tambo-mcp-provider.d.ts +7 -0
  33. package/dist/mcp/tambo-mcp-provider.d.ts.map +1 -1
  34. package/dist/mcp/tambo-mcp-provider.js +202 -155
  35. package/dist/mcp/tambo-mcp-provider.js.map +1 -1
  36. package/dist/model/component-metadata.d.ts +1 -1
  37. package/dist/model/component-metadata.d.ts.map +1 -1
  38. package/dist/model/component-metadata.js.map +1 -1
  39. package/dist/model/tambo-interactable.d.ts +7 -5
  40. package/dist/model/tambo-interactable.d.ts.map +1 -1
  41. package/dist/model/tambo-interactable.js.map +1 -1
  42. package/dist/providers/__tests__/thread-input-resource-resolution.test.d.ts +2 -0
  43. package/dist/providers/__tests__/thread-input-resource-resolution.test.d.ts.map +1 -0
  44. package/dist/providers/__tests__/thread-input-resource-resolution.test.js +592 -0
  45. package/dist/providers/__tests__/thread-input-resource-resolution.test.js.map +1 -0
  46. package/dist/providers/tambo-interactable-provider-partial-updates.test.js +22 -21
  47. package/dist/providers/tambo-interactable-provider-partial-updates.test.js.map +1 -1
  48. package/dist/providers/tambo-interactable-provider.d.ts +3 -2
  49. package/dist/providers/tambo-interactable-provider.d.ts.map +1 -1
  50. package/dist/providers/tambo-interactable-provider.js +98 -14
  51. package/dist/providers/tambo-interactable-provider.js.map +1 -1
  52. package/dist/providers/tambo-interactable-provider.test.js +242 -0
  53. package/dist/providers/tambo-interactable-provider.test.js.map +1 -1
  54. package/dist/providers/tambo-provider.d.ts +1 -2
  55. package/dist/providers/tambo-provider.d.ts.map +1 -1
  56. package/dist/providers/tambo-provider.js +10 -8
  57. package/dist/providers/tambo-provider.js.map +1 -1
  58. package/dist/providers/tambo-stubs.d.ts.map +1 -1
  59. package/dist/providers/tambo-stubs.js +1 -0
  60. package/dist/providers/tambo-stubs.js.map +1 -1
  61. package/dist/providers/tambo-stubs.test.js +1 -1
  62. package/dist/providers/tambo-stubs.test.js.map +1 -1
  63. package/dist/providers/tambo-thread-input-provider.d.ts +1 -6
  64. package/dist/providers/tambo-thread-input-provider.d.ts.map +1 -1
  65. package/dist/providers/tambo-thread-input-provider.js +25 -8
  66. package/dist/providers/tambo-thread-input-provider.js.map +1 -1
  67. package/dist/providers/tambo-thread-provider.d.ts +5 -0
  68. package/dist/providers/tambo-thread-provider.d.ts.map +1 -1
  69. package/dist/providers/tambo-thread-provider.js +5 -2
  70. package/dist/providers/tambo-thread-provider.js.map +1 -1
  71. package/dist/schema/index.d.ts +1 -1
  72. package/dist/schema/index.d.ts.map +1 -1
  73. package/dist/schema/index.js +2 -1
  74. package/dist/schema/index.js.map +1 -1
  75. package/dist/schema/json-schema.d.ts +7 -0
  76. package/dist/schema/json-schema.d.ts.map +1 -1
  77. package/dist/schema/json-schema.js +11 -0
  78. package/dist/schema/json-schema.js.map +1 -1
  79. package/dist/schema/json-schema.test.d.ts +2 -0
  80. package/dist/schema/json-schema.test.d.ts.map +1 -0
  81. package/dist/schema/json-schema.test.js +204 -0
  82. package/dist/schema/json-schema.test.js.map +1 -0
  83. package/dist/setupTests.js +3 -0
  84. package/dist/setupTests.js.map +1 -1
  85. package/dist/util/message-builder.d.ts +3 -1
  86. package/dist/util/message-builder.d.ts.map +1 -1
  87. package/dist/util/message-builder.js +20 -3
  88. package/dist/util/message-builder.js.map +1 -1
  89. package/dist/util/message-builder.test.js +269 -0
  90. package/dist/util/message-builder.test.js.map +1 -1
  91. package/dist/util/resource-content-resolver.d.ts +20 -0
  92. package/dist/util/resource-content-resolver.d.ts.map +1 -0
  93. package/dist/util/resource-content-resolver.js +93 -0
  94. package/dist/util/resource-content-resolver.js.map +1 -0
  95. package/dist/util/resource-content-resolver.test.d.ts +2 -0
  96. package/dist/util/resource-content-resolver.test.d.ts.map +1 -0
  97. package/dist/util/resource-content-resolver.test.js +254 -0
  98. package/dist/util/resource-content-resolver.test.js.map +1 -0
  99. package/esm/context-helpers/current-interactables-context-helper.d.ts.map +1 -1
  100. package/esm/context-helpers/current-interactables-context-helper.js +4 -1
  101. package/esm/context-helpers/current-interactables-context-helper.js.map +1 -1
  102. package/esm/hoc/with-tambo-interactable.d.ts +50 -4
  103. package/esm/hoc/with-tambo-interactable.d.ts.map +1 -1
  104. package/esm/hoc/with-tambo-interactable.js +20 -5
  105. package/esm/hoc/with-tambo-interactable.js.map +1 -1
  106. package/esm/hooks/use-component-state.d.ts +3 -8
  107. package/esm/hooks/use-component-state.d.ts.map +1 -1
  108. package/esm/hooks/use-component-state.js +8 -0
  109. package/esm/hooks/use-component-state.js.map +1 -1
  110. package/esm/hooks/use-component-state.test.js +37 -0
  111. package/esm/hooks/use-component-state.test.js.map +1 -1
  112. package/esm/hooks/use-tambo-threads.d.ts +3 -8
  113. package/esm/hooks/use-tambo-threads.d.ts.map +1 -1
  114. package/esm/hooks/use-tambo-threads.js +5 -3
  115. package/esm/hooks/use-tambo-threads.js.map +1 -1
  116. package/esm/hooks/use-tambo-threads.test.js +12 -2
  117. package/esm/hooks/use-tambo-threads.test.js.map +1 -1
  118. package/esm/mcp/mcp-constants.d.ts +19 -0
  119. package/esm/mcp/mcp-constants.d.ts.map +1 -0
  120. package/esm/mcp/mcp-constants.js +18 -0
  121. package/esm/mcp/mcp-constants.js.map +1 -0
  122. package/esm/mcp/mcp-hooks.d.ts +32 -3
  123. package/esm/mcp/mcp-hooks.d.ts.map +1 -1
  124. package/esm/mcp/mcp-hooks.js +40 -30
  125. package/esm/mcp/mcp-hooks.js.map +1 -1
  126. package/esm/mcp/mcp-hooks.test.js +8 -5
  127. package/esm/mcp/mcp-hooks.test.js.map +1 -1
  128. package/esm/mcp/tambo-mcp-provider.d.ts +7 -0
  129. package/esm/mcp/tambo-mcp-provider.d.ts.map +1 -1
  130. package/esm/mcp/tambo-mcp-provider.js +201 -154
  131. package/esm/mcp/tambo-mcp-provider.js.map +1 -1
  132. package/esm/model/component-metadata.d.ts +1 -1
  133. package/esm/model/component-metadata.d.ts.map +1 -1
  134. package/esm/model/component-metadata.js.map +1 -1
  135. package/esm/model/tambo-interactable.d.ts +7 -5
  136. package/esm/model/tambo-interactable.d.ts.map +1 -1
  137. package/esm/model/tambo-interactable.js.map +1 -1
  138. package/esm/providers/__tests__/thread-input-resource-resolution.test.d.ts +2 -0
  139. package/esm/providers/__tests__/thread-input-resource-resolution.test.d.ts.map +1 -0
  140. package/esm/providers/__tests__/thread-input-resource-resolution.test.js +587 -0
  141. package/esm/providers/__tests__/thread-input-resource-resolution.test.js.map +1 -0
  142. package/esm/providers/tambo-interactable-provider-partial-updates.test.js +22 -21
  143. package/esm/providers/tambo-interactable-provider-partial-updates.test.js.map +1 -1
  144. package/esm/providers/tambo-interactable-provider.d.ts +3 -2
  145. package/esm/providers/tambo-interactable-provider.d.ts.map +1 -1
  146. package/esm/providers/tambo-interactable-provider.js +98 -14
  147. package/esm/providers/tambo-interactable-provider.js.map +1 -1
  148. package/esm/providers/tambo-interactable-provider.test.js +242 -0
  149. package/esm/providers/tambo-interactable-provider.test.js.map +1 -1
  150. package/esm/providers/tambo-provider.d.ts +1 -2
  151. package/esm/providers/tambo-provider.d.ts.map +1 -1
  152. package/esm/providers/tambo-provider.js +11 -9
  153. package/esm/providers/tambo-provider.js.map +1 -1
  154. package/esm/providers/tambo-stubs.d.ts.map +1 -1
  155. package/esm/providers/tambo-stubs.js +1 -0
  156. package/esm/providers/tambo-stubs.js.map +1 -1
  157. package/esm/providers/tambo-stubs.test.js +1 -1
  158. package/esm/providers/tambo-stubs.test.js.map +1 -1
  159. package/esm/providers/tambo-thread-input-provider.d.ts +1 -6
  160. package/esm/providers/tambo-thread-input-provider.d.ts.map +1 -1
  161. package/esm/providers/tambo-thread-input-provider.js +25 -8
  162. package/esm/providers/tambo-thread-input-provider.js.map +1 -1
  163. package/esm/providers/tambo-thread-provider.d.ts +5 -0
  164. package/esm/providers/tambo-thread-provider.d.ts.map +1 -1
  165. package/esm/providers/tambo-thread-provider.js +5 -2
  166. package/esm/providers/tambo-thread-provider.js.map +1 -1
  167. package/esm/schema/index.d.ts +1 -1
  168. package/esm/schema/index.d.ts.map +1 -1
  169. package/esm/schema/index.js +1 -1
  170. package/esm/schema/index.js.map +1 -1
  171. package/esm/schema/json-schema.d.ts +7 -0
  172. package/esm/schema/json-schema.d.ts.map +1 -1
  173. package/esm/schema/json-schema.js +10 -0
  174. package/esm/schema/json-schema.js.map +1 -1
  175. package/esm/schema/json-schema.test.d.ts +2 -0
  176. package/esm/schema/json-schema.test.d.ts.map +1 -0
  177. package/esm/schema/json-schema.test.js +202 -0
  178. package/esm/schema/json-schema.test.js.map +1 -0
  179. package/esm/setupTests.js +3 -0
  180. package/esm/setupTests.js.map +1 -1
  181. package/esm/util/message-builder.d.ts +3 -1
  182. package/esm/util/message-builder.d.ts.map +1 -1
  183. package/esm/util/message-builder.js +20 -3
  184. package/esm/util/message-builder.js.map +1 -1
  185. package/esm/util/message-builder.test.js +269 -0
  186. package/esm/util/message-builder.test.js.map +1 -1
  187. package/esm/util/resource-content-resolver.d.ts +20 -0
  188. package/esm/util/resource-content-resolver.d.ts.map +1 -0
  189. package/esm/util/resource-content-resolver.js +89 -0
  190. package/esm/util/resource-content-resolver.js.map +1 -0
  191. package/esm/util/resource-content-resolver.test.d.ts +2 -0
  192. package/esm/util/resource-content-resolver.test.d.ts.map +1 -0
  193. package/esm/util/resource-content-resolver.test.js +252 -0
  194. package/esm/util/resource-content-resolver.test.js.map +1 -0
  195. package/package.json +8 -6
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Fractal Dynamics Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -241,7 +241,7 @@ const InteractableNote = withInteractable(Note, {
241
241
  Connect to Linear, Slack, databases, or your own MCP servers. Tambo supports the full MCP protocol: tools, prompts, elicitations, and sampling.
242
242
 
243
243
  ```tsx
244
- import { TamboMcpProvider, MCPTransport } from "@tambo-ai/react/mcp";
244
+ import { MCPTransport } from "@tambo-ai/react/mcp";
245
245
 
246
246
  const mcpServers = [
247
247
  {
@@ -252,9 +252,7 @@ const mcpServers = [
252
252
  ];
253
253
 
254
254
  <TamboProvider components={components} mcpServers={mcpServers}>
255
- <TamboMcpProvider>
256
- <App />
257
- </TamboMcpProvider>
255
+ <App />
258
256
  </TamboProvider>;
259
257
  ```
260
258
 
@@ -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,eAqBF,CAAC"}
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"}
@@ -37,7 +37,7 @@ const createInteractablesContextHelper = (components) => {
37
37
  return null; // No interactable components on the page
38
38
  }
39
39
  return {
40
- 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 schema. You can use tools to update these components on behalf of the user. Don't tell the user the ID of the components, only the name, unless they ask for it.",
40
+ 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.",
41
41
  components: components.map((component) => ({
42
42
  id: component.id,
43
43
  componentName: component.name,
@@ -47,6 +47,9 @@ const createInteractablesContextHelper = (components) => {
47
47
  ? "Available - use component-specific update tools"
48
48
  : "Not specified",
49
49
  state: component.state,
50
+ stateSchema: component.stateSchema
51
+ ? "Available - use component-specific update tools"
52
+ : "Not specified",
50
53
  })),
51
54
  };
52
55
  };
@@ -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;AACI,MAAM,iCAAiC,GAAoB,GAAG,EAAE;IACrE,mFAAmF;IACnF,wEAAwE;IACxE,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAJW,QAAA,iCAAiC,qCAI5C;AAEF;;;;;GAKG;AACI,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,uVAAuV;YACzV,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;aACvB,CAAC,CAAC;SACJ,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC,CAAC;AAvBW,QAAA,gCAAgC,oCAuB3C","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 schema. You can use tools to update these components 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 })),\n };\n };\n};\n"]}
1
+ {"version":3,"file":"current-interactables-context-helper.js","sourceRoot":"","sources":["../../src/context-helpers/current-interactables-context-helper.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;;;;GAiBG;AACI,MAAM,iCAAiC,GAAoB,GAAG,EAAE;IACrE,mFAAmF;IACnF,wEAAwE;IACxE,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAJW,QAAA,iCAAiC,qCAI5C;AAEF;;;;;GAKG;AACI,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;AA1BW,QAAA,gCAAgC,oCA0B3C","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
- propsSchema?: SupportedSchema;
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
- onPropsUpdate?: (newProps: Record<string, any>) => void;
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<P extends object>(WrappedComponent: React.ComponentType<P>, config: InteractableConfig): React.FC<P & WithTamboInteractableProps>;
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;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B;AAED,MAAM,WAAW,0BAA0B;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,CAAC;CACzD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,MAAM,EACpD,gBAAgB,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,EACxC,MAAM,EAAE,kBAAkB,4CAgH3B"}
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"}
@@ -45,6 +45,16 @@ const tambo_interactable_provider_1 = require("../providers/tambo-interactable-p
45
45
  * @returns A new component that is automatically registered as interactable
46
46
  * @example
47
47
  * ```tsx
48
+ * const MyNote: React.FC<{ title: string; content: string }> = ({ title, content }) => {
49
+ * const [isPinned, setIsPinned] = useTamboComponentState("isPinned", false);
50
+ * return (
51
+ * <div style={{ border: isPinned ? "2px solid gold" : "1px solid gray", order: isPinned ? -1 : 0 }}>
52
+ * <h2>{title}</h2>
53
+ * <p>{content}</p>
54
+ * </div>
55
+ * );
56
+ * };
57
+ *
48
58
  * const MyInteractableNote = withTamboInteractable(MyNote, {
49
59
  * componentName: "MyNote",
50
60
  * description: "A note component",
@@ -52,6 +62,9 @@ const tambo_interactable_provider_1 = require("../providers/tambo-interactable-p
52
62
  * title: z.string(),
53
63
  * content: z.string(),
54
64
  * }),
65
+ * stateSchema: z.object({
66
+ * isPinned: z.boolean(),
67
+ * }),
55
68
  * });
56
69
  *
57
70
  * // Usage
@@ -64,9 +77,10 @@ function withTamboInteractable(WrappedComponent, config) {
64
77
  const { addInteractableComponent, updateInteractableComponentProps, getInteractableComponent, } = (0, tambo_interactable_provider_1.useTamboInteractable)();
65
78
  const [interactableId, setInteractableId] = (0, react_1.useState)(null);
66
79
  const isInitialized = (0, react_1.useRef)(false);
67
- const lastParentProps = (0, react_1.useRef)({});
68
- // Extract interactable-specific props
69
- const { onInteractableReady, onPropsUpdate, ...componentProps } = props;
80
+ const lastSerializedProps = (0, react_1.useRef)({});
81
+ // Extract interactable-specific props from component props
82
+ const { interactableId: _providedId, // Reserved for future use
83
+ onInteractableReady, onPropsUpdate, ...componentProps } = props;
70
84
  // Get the current interactable component to track prop updates
71
85
  const currentInteractable = interactableId
72
86
  ? getInteractableComponent(interactableId)
@@ -83,6 +97,7 @@ function withTamboInteractable(WrappedComponent, config) {
83
97
  component: WrappedComponent,
84
98
  props: componentProps,
85
99
  propsSchema: config.propsSchema,
100
+ stateSchema: config.stateSchema,
86
101
  });
87
102
  setInteractableId(id);
88
103
  onInteractableReady?.(id);
@@ -97,12 +112,12 @@ function withTamboInteractable(WrappedComponent, config) {
97
112
  (0, react_1.useEffect)(() => {
98
113
  if (interactableId && isInitialized.current) {
99
114
  // Only update if the props are different from what we last sent
100
- const lastPropsString = JSON.stringify(lastParentProps.current);
115
+ const lastPropsString = JSON.stringify(lastSerializedProps.current);
101
116
  const currentPropsString = JSON.stringify(componentProps);
102
117
  if (lastPropsString !== currentPropsString) {
103
118
  updateInteractableComponentProps(interactableId, componentProps);
104
119
  onPropsUpdate?.(componentProps);
105
- lastParentProps.current = componentProps;
120
+ lastSerializedProps.current = componentProps;
106
121
  }
107
122
  }
108
123
  }, [
@@ -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 containing:
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":"AAOA,KAAK,iBAAiB,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;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
+ {"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"}
@@ -2,6 +2,7 @@
2
2
  "use client";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.useTamboComponentState = useTamboComponentState;
5
+ const fast_equals_1 = require("fast-equals");
5
6
  const react_1 = require("react");
6
7
  const use_debounce_1 = require("use-debounce");
7
8
  const __1 = require("..");
@@ -101,6 +102,13 @@ function useTamboComponentState(keyName, initialValue, setFromProp, debounceTime
101
102
  setInteractableState,
102
103
  componentId,
103
104
  ]);
105
+ // Sync from interactable provider to local state when state changes externally (e.g., from Tambo tool call)
106
+ (0, react_1.useEffect)(() => {
107
+ if (!componentId)
108
+ return;
109
+ // only update if different
110
+ setLocalState((prev) => (0, fast_equals_1.deepEqual)(prev, interactableState) ? prev : interactableState);
111
+ }, [componentId, interactableState]);
104
112
  // 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.
105
113
  (0, react_1.useEffect)(() => {
106
114
  if (setFromProp !== undefined && !initializedFromThreadMessage) {
@@ -1 +1 @@
1
- {"version":3,"file":"use-component-state.js","sourceRoot":"","sources":["../../src/hooks/use-component-state.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\nimport { useCallback, useContext, useEffect, useState } from \"react\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport { TamboThreadMessage, useTamboClient, useTamboThread } from \"..\";\nimport { useTamboInteractable } from \"../providers/tambo-interactable-provider\";\nimport { TamboMessageContext } from \"./use-current-message\";\n\ntype StateUpdateResult<T> = [currentState: T, setState: (newState: T) => void];\n\n/**\n * A React hook that acts like useState, but also automatically updates the thread message's componentState.\n * If used within an interactable component (wrapped with withTamboInteractable), it updates the\n * interactable provider's global state (sent to Tambo on every request) instead of the remote thread message state.\n * For generated components, it updates both the local and remote thread message's componentState.\n *\n * Benefits: Passes user changes to AI, and when threads are returned, state is preserved.\n * Works in both generative and interactable component contexts.\n * @param keyName - The unique key to identify this state value within the message's componentState object\n * @param initialValue - Optional initial value for the state, used if no componentState value exists in the Tambo message containing this hook usage.\n * @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.\n * @param debounceTime - Optional debounce time in milliseconds (default: 500ms) to limit API calls.\n * @returns A tuple containing:\n * - The current state value\n * - A setter function to update the state (updates UI immediately, debounces server sync)\n * @example\n * const [count, setCount] = useTamboComponentState(\"counter\", 0);\n *\n * Use `setFromProp` to seed state from streamed props. During streaming,\n * state updates as new prop values arrive. Once streaming completes,\n * user edits take precedence over the original prop value.\n *\n * Pair with `useTamboStreamStatus` to disable inputs while streaming.\n * @see {@link https://docs.tambo.co/concepts/streaming/streaming-best-practices}\n * @param keyName - Unique key within the message's componentState\n * @param initialValue - Default value if no componentState exists\n * @param setFromProp - Seeds state from props (updates during streaming, then user edits take over)\n * @param debounceTime - Server sync debounce in ms (default: 500)\n * @returns A tuple of [currentState, setState] similar to React's useState\n */\nexport function useTamboComponentState<S = undefined>(\n keyName: string,\n initialValue?: S,\n setFromProp?: S,\n debounceTime?: number,\n): StateUpdateResult<S | undefined>;\nexport function useTamboComponentState<S>(\n keyName: string,\n initialValue: S,\n setFromProp?: S,\n debounceTime?: number,\n): StateUpdateResult<S>;\nexport function useTamboComponentState<S>(\n keyName: string,\n initialValue?: S,\n setFromProp?: S,\n debounceTime = 500,\n): StateUpdateResult<S> {\n const message = useContext(TamboMessageContext);\n const { updateThreadMessage } = useTamboThread();\n const client = useTamboClient();\n const componentId = message?.interactableMetadata?.id ?? null;\n const { setInteractableState, getInteractableComponentState } =\n useTamboInteractable();\n const messageState = message?.componentState?.[keyName];\n const interactableState = componentId\n ? getInteractableComponentState(componentId)?.[keyName]\n : undefined;\n const initialState =\n (interactableState as S) ?? (messageState as S) ?? initialValue;\n const [localState, setLocalState] = useState<S | undefined>(initialState);\n const [initializedFromThreadMessage, setInitializedFromThreadMessage] =\n useState(messageState ? true : false);\n\n // Optimistically update the local thread message's componentState\n const updateLocalThreadMessage = useCallback(\n async (newState: S, existingMessage: TamboThreadMessage | null) => {\n if (!existingMessage) {\n return;\n }\n const updatedMessage = {\n threadId: existingMessage.threadId,\n componentState: {\n ...existingMessage.componentState,\n [keyName]: newState,\n },\n };\n await updateThreadMessage(existingMessage.id, updatedMessage, false);\n },\n [updateThreadMessage, keyName],\n );\n\n // Debounced callback to update the remote thread message's componentState\n const updateRemoteThreadMessage = useDebouncedCallback(\n async (newState: S, existingMessage: TamboThreadMessage | null) => {\n if (!existingMessage) {\n return;\n }\n await client.beta.threads.messages.updateComponentState(\n existingMessage.id,\n {\n id: existingMessage.threadId,\n state: { [keyName]: newState },\n },\n );\n },\n debounceTime,\n );\n\n const setValue = useCallback(\n (newState: S) => {\n setLocalState(newState);\n if (componentId) {\n // For interactable components, update the interactable provider's state\n setInteractableState(componentId, keyName, newState);\n } else if (message) {\n // For generated components, update both local and remote thread message state\n void updateLocalThreadMessage(newState, message);\n void updateRemoteThreadMessage(newState, message);\n }\n },\n [\n message,\n updateLocalThreadMessage,\n updateRemoteThreadMessage,\n setInteractableState,\n componentId,\n keyName,\n ],\n );\n\n const existingInteractableState = componentId\n ? getInteractableComponentState(componentId)?.[keyName]\n : undefined;\n const shouldUpdateInteractableInitial =\n !!componentId &&\n existingInteractableState === undefined &&\n initialValue !== undefined;\n\n // Set initial value in interactable state if we're in an interactable context and there's no existing state\n useEffect(() => {\n if (!shouldUpdateInteractableInitial) {\n return;\n }\n setInteractableState(componentId, keyName, initialValue!);\n }, [\n shouldUpdateInteractableInitial,\n componentId,\n keyName,\n initialValue,\n setInteractableState,\n ]);\n\n const shouldSyncFromMessage =\n !!message && messageState !== undefined && messageState !== null;\n\n // Mirror the thread message's componentState value to the local state and interactable state\n useEffect(() => {\n if (!shouldSyncFromMessage) {\n return;\n }\n setInitializedFromThreadMessage(true);\n const stateValue = messageState as S;\n setLocalState(stateValue);\n if (componentId) {\n setInteractableState(componentId, keyName, stateValue);\n }\n }, [\n shouldSyncFromMessage,\n messageState,\n keyName,\n setInteractableState,\n componentId,\n ]);\n\n // 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.\n useEffect(() => {\n if (setFromProp !== undefined && !initializedFromThreadMessage) {\n setLocalState(setFromProp as S);\n }\n }, [setFromProp, initializedFromThreadMessage]);\n\n // Ensure pending changes are flushed on unmount (only for generated components)\n useEffect(() => {\n // Only flush remote updates for generated components, not interactable components\n if (componentId) {\n return;\n }\n return () => {\n async function flushUpdates() {\n try {\n await updateRemoteThreadMessage.flush();\n } catch (error) {\n console.error(\n \"Failed to flush pending thread message updates:\",\n error,\n );\n }\n }\n // Fire-and-forget cleanup (errors handled inside)\n void flushUpdates();\n };\n }, [updateRemoteThreadMessage, componentId]);\n\n return [localState as S, setValue];\n}\n"]}
1
+ {"version":3,"file":"use-component-state.js","sourceRoot":"","sources":["../../src/hooks/use-component-state.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\nimport { deepEqual } from \"fast-equals\";\nimport { useCallback, useContext, useEffect, useState } from \"react\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport { TamboThreadMessage, useTamboClient, useTamboThread } from \"..\";\nimport { useTamboInteractable } from \"../providers/tambo-interactable-provider\";\nimport { TamboMessageContext } from \"./use-current-message\";\n\ntype StateUpdateResult<T> = [currentState: T, setState: (newState: T) => void];\n\n/**\n * A React hook that acts like useState, but also automatically updates the thread message's componentState.\n * If used within an interactable component (wrapped with withTamboInteractable), it updates the\n * interactable provider's global state (sent to Tambo on every request) instead of the remote thread message state.\n * For generated components, it updates both the local and remote thread message's componentState.\n *\n * Benefits: Passes user changes to AI, and when threads are returned, state is preserved.\n * Works in both generative and interactable component contexts.\n * @param keyName - The unique key to identify this state value within the message's componentState object\n * @param initialValue - Optional initial value for the state, used if no componentState value exists in the Tambo message containing this hook usage.\n * @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.\n * @param debounceTime - Optional debounce time in milliseconds (default: 500ms) to limit API calls.\n * @returns A tuple of [currentState, setState] similar to React's useState\n * @example\n * ```tsx\n * const [count, setCount] = useTamboComponentState(\"counter\", 0);\n * ```\n *\n * Use `setFromProp` to seed state from streamed props. During streaming,\n * state updates as new prop values arrive. Once streaming completes,\n * user edits take precedence over the original prop value.\n *\n * Pair with `useTamboStreamStatus` to disable inputs while streaming.\n * @see {@link https://docs.tambo.co/concepts/streaming/streaming-best-practices}\n */\nexport function useTamboComponentState<S = undefined>(\n keyName: string,\n initialValue?: S,\n setFromProp?: S,\n debounceTime?: number,\n): StateUpdateResult<S | undefined>;\nexport function useTamboComponentState<S>(\n keyName: string,\n initialValue: S,\n setFromProp?: S,\n debounceTime?: number,\n): StateUpdateResult<S>;\nexport function useTamboComponentState<S>(\n keyName: string,\n initialValue?: S,\n setFromProp?: S,\n debounceTime = 500,\n): StateUpdateResult<S> {\n const message = useContext(TamboMessageContext);\n const { updateThreadMessage } = useTamboThread();\n const client = useTamboClient();\n const componentId = message?.interactableMetadata?.id ?? null;\n const { setInteractableState, getInteractableComponentState } =\n useTamboInteractable();\n const messageState = message?.componentState?.[keyName];\n const interactableState = componentId\n ? getInteractableComponentState(componentId)?.[keyName]\n : undefined;\n const initialState =\n (interactableState as S) ?? (messageState as S) ?? initialValue;\n const [localState, setLocalState] = useState<S | undefined>(initialState);\n const [initializedFromThreadMessage, setInitializedFromThreadMessage] =\n useState(messageState ? true : false);\n\n // Optimistically update the local thread message's componentState\n const updateLocalThreadMessage = useCallback(\n async (newState: S, existingMessage: TamboThreadMessage | null) => {\n if (!existingMessage) {\n return;\n }\n const updatedMessage = {\n threadId: existingMessage.threadId,\n componentState: {\n ...existingMessage.componentState,\n [keyName]: newState,\n },\n };\n await updateThreadMessage(existingMessage.id, updatedMessage, false);\n },\n [updateThreadMessage, keyName],\n );\n\n // Debounced callback to update the remote thread message's componentState\n const updateRemoteThreadMessage = useDebouncedCallback(\n async (newState: S, existingMessage: TamboThreadMessage | null) => {\n if (!existingMessage) {\n return;\n }\n await client.beta.threads.messages.updateComponentState(\n existingMessage.id,\n {\n id: existingMessage.threadId,\n state: { [keyName]: newState },\n },\n );\n },\n debounceTime,\n );\n\n const setValue = useCallback(\n (newState: S) => {\n setLocalState(newState);\n if (componentId) {\n // For interactable components, update the interactable provider's state\n setInteractableState(componentId, keyName, newState);\n } else if (message) {\n // For generated components, update both local and remote thread message state\n void updateLocalThreadMessage(newState, message);\n void updateRemoteThreadMessage(newState, message);\n }\n },\n [\n message,\n updateLocalThreadMessage,\n updateRemoteThreadMessage,\n setInteractableState,\n componentId,\n keyName,\n ],\n );\n\n const existingInteractableState = componentId\n ? getInteractableComponentState(componentId)?.[keyName]\n : undefined;\n const shouldUpdateInteractableInitial =\n !!componentId &&\n existingInteractableState === undefined &&\n initialValue !== undefined;\n\n // Set initial value in interactable state if we're in an interactable context and there's no existing state\n useEffect(() => {\n if (!shouldUpdateInteractableInitial) {\n return;\n }\n setInteractableState(componentId, keyName, initialValue!);\n }, [\n shouldUpdateInteractableInitial,\n componentId,\n keyName,\n initialValue,\n setInteractableState,\n ]);\n\n const shouldSyncFromMessage =\n !!message && messageState !== undefined && messageState !== null;\n\n // Mirror the thread message's componentState value to the local state and interactable state\n useEffect(() => {\n if (!shouldSyncFromMessage) {\n return;\n }\n setInitializedFromThreadMessage(true);\n const stateValue = messageState as S;\n setLocalState(stateValue);\n if (componentId) {\n setInteractableState(componentId, keyName, stateValue);\n }\n }, [\n shouldSyncFromMessage,\n messageState,\n keyName,\n setInteractableState,\n componentId,\n ]);\n\n // Sync from interactable provider to local state when state changes externally (e.g., from Tambo tool call)\n useEffect(() => {\n if (!componentId) return;\n // only update if different\n setLocalState((prev) =>\n deepEqual(prev, interactableState) ? prev : (interactableState as S),\n );\n }, [componentId, interactableState]);\n\n // 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.\n useEffect(() => {\n if (setFromProp !== undefined && !initializedFromThreadMessage) {\n setLocalState(setFromProp as S);\n }\n }, [setFromProp, initializedFromThreadMessage]);\n\n // Ensure pending changes are flushed on unmount (only for generated components)\n useEffect(() => {\n // Only flush remote updates for generated components, not interactable components\n if (componentId) {\n return;\n }\n return () => {\n async function flushUpdates() {\n try {\n await updateRemoteThreadMessage.flush();\n } catch (error) {\n console.error(\n \"Failed to flush pending thread message updates:\",\n error,\n );\n }\n }\n // Fire-and-forget cleanup (errors handled inside)\n void flushUpdates();\n };\n }, [updateRemoteThreadMessage, componentId]);\n\n return [localState as S, setValue];\n}\n"]}
@@ -316,6 +316,43 @@ describe("useTamboComponentState", () => {
316
316
  expect(result.current[0]).toBe("initial");
317
317
  });
318
318
  });
319
+ describe("Interactable State Sync", () => {
320
+ it("should sync local state when interactable state changes externally", () => {
321
+ // Setup: Component is in interactable context with an ID
322
+ const message = createMockMessage({
323
+ componentState: {},
324
+ interactableMetadata: {
325
+ id: "test-interactable-id",
326
+ componentName: "TestComponent",
327
+ description: "Test",
328
+ },
329
+ });
330
+ jest.mocked(use_current_message_1.useTamboCurrentMessage).mockReturnValue(message);
331
+ // Start with initial state
332
+ mockGetInteractableComponentState.mockReturnValue({ testKey: "initial" });
333
+ const { result, rerender } = (0, react_1.renderHook)(() => (0, use_component_state_1.useTamboComponentState)("testKey", "initial"));
334
+ expect(result.current[0]).toBe("initial");
335
+ // Simulate external state update (e.g., from Tambo tool call)
336
+ mockGetInteractableComponentState.mockReturnValue({
337
+ testKey: "updated-by-tambo",
338
+ });
339
+ // Trigger rerender to pick up new interactable state
340
+ rerender();
341
+ // Local state should sync with the external update
342
+ expect(result.current[0]).toBe("updated-by-tambo");
343
+ });
344
+ it("should not sync when not in interactable context", () => {
345
+ // Setup: Component is NOT in interactable context (no interactableMetadata.id)
346
+ const message = createMockMessage({
347
+ componentState: { testKey: "initial" },
348
+ });
349
+ jest.mocked(use_current_message_1.useTamboCurrentMessage).mockReturnValue(message);
350
+ mockGetInteractableComponentState.mockReturnValue({ testKey: "ignored" });
351
+ const { result } = (0, react_1.renderHook)(() => (0, use_component_state_1.useTamboComponentState)("testKey", "initial"));
352
+ // Should use message state, not interactable state
353
+ expect(result.current[0]).toBe("initial");
354
+ });
355
+ });
319
356
  describe("Message State Sync", () => {
320
357
  it("should sync with message.componentState changes", () => {
321
358
  const { result, rerender } = (0, react_1.renderHook)(({ message }) => {