@assistant-ui/react 0.15.17 → 0.15.18

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 (105) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +6 -3
  3. package/dist/index.js +4 -2
  4. package/dist/mcp-apps/McpAppRenderer.d.ts +30 -8
  5. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  6. package/dist/mcp-apps/McpAppRenderer.js +91 -47
  7. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  8. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  9. package/dist/mcp-apps/app-frame.js +41 -22
  10. package/dist/mcp-apps/app-frame.js.map +1 -1
  11. package/dist/mcp-apps/bridge.js +2 -3
  12. package/dist/mcp-apps/bridge.js.map +1 -1
  13. package/dist/mcp-apps/index.d.ts +2 -2
  14. package/dist/primitives/composer/ComposerInputPluginContext.d.ts +1 -2
  15. package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -1
  16. package/dist/primitives/composer/ComposerInputPluginContext.js +1 -6
  17. package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -1
  18. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +46 -15
  19. package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
  20. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +40 -10
  21. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
  22. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts +6 -0
  23. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts.map +1 -0
  24. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +9 -0
  25. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js.map +1 -0
  26. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  27. package/dist/primitives/composer/trigger/triggerNavigationResource.js +3 -5
  28. package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
  29. package/dist/primitives/messagePart/MessagePartText.d.ts +1 -1
  30. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  31. package/dist/primitives/thread/useThreadViewportAutoScroll.js +3 -3
  32. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  33. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  34. package/dist/sandbox-host/SandboxHost.js +14 -5
  35. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  36. package/dist/unstable/useMentionAdapter.d.ts +9 -3
  37. package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
  38. package/dist/unstable/useMentionAdapter.js +50 -43
  39. package/dist/unstable/useMentionAdapter.js.map +1 -1
  40. package/dist/unstable/useModelContextSnapshot.d.ts +34 -0
  41. package/dist/unstable/useModelContextSnapshot.d.ts.map +1 -0
  42. package/dist/unstable/useModelContextSnapshot.js +61 -0
  43. package/dist/unstable/useModelContextSnapshot.js.map +1 -0
  44. package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
  45. package/dist/unstable/useSlashCommandAdapter.js +2 -8
  46. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  47. package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts +14 -0
  48. package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts.map +1 -0
  49. package/dist/unstable/webmcp/WebMcpRegistrationResource.js +85 -0
  50. package/dist/unstable/webmcp/WebMcpRegistrationResource.js.map +1 -0
  51. package/dist/unstable/webmcp/convertTools.d.ts +21 -0
  52. package/dist/unstable/webmcp/convertTools.d.ts.map +1 -0
  53. package/dist/unstable/webmcp/convertTools.js +110 -0
  54. package/dist/unstable/webmcp/convertTools.js.map +1 -0
  55. package/dist/unstable/webmcp/useWebMcpProvider.d.ts +21 -0
  56. package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -0
  57. package/dist/unstable/webmcp/useWebMcpProvider.js +105 -0
  58. package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -0
  59. package/dist/unstable/webmcp/webmcp-host.d.ts +39 -0
  60. package/dist/unstable/webmcp/webmcp-host.d.ts.map +1 -0
  61. package/dist/unstable/webmcp/webmcp-host.js +29 -0
  62. package/dist/unstable/webmcp/webmcp-host.js.map +1 -0
  63. package/dist/utils/Primitive.d.ts +4 -4
  64. package/package.json +12 -12
  65. package/src/index.ts +13 -0
  66. package/src/mcp-apps/McpAppRenderer.test.tsx +253 -5
  67. package/src/mcp-apps/McpAppRenderer.tsx +147 -52
  68. package/src/mcp-apps/app-frame.test.tsx +248 -1
  69. package/src/mcp-apps/app-frame.tsx +51 -5
  70. package/src/mcp-apps/bridge.ts +7 -2
  71. package/src/mcp-apps/index.ts +5 -1
  72. package/src/primitives/composer/ComposerInputPluginContext.tsx +0 -10
  73. package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +4 -2
  74. package/src/primitives/composer/trigger/TriggerPopoverBehavior.test.tsx +109 -0
  75. package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +4 -2
  76. package/src/primitives/composer/trigger/matchesTriggerItemQuery.test.ts +52 -0
  77. package/src/primitives/composer/trigger/matchesTriggerItemQuery.ts +13 -0
  78. package/src/primitives/composer/trigger/triggerNavigationResource.ts +3 -10
  79. package/src/primitives/thread/useThreadViewportAutoScroll.ts +7 -6
  80. package/src/sandbox-host/SandboxHost.test.tsx +120 -2
  81. package/src/sandbox-host/SandboxHost.tsx +26 -6
  82. package/src/unstable/useComposerInputHistory.test.tsx +2 -1
  83. package/src/unstable/useMentionAdapter.test.tsx +337 -0
  84. package/src/unstable/useMentionAdapter.ts +94 -57
  85. package/src/unstable/useModelContextSnapshot.test.tsx +132 -0
  86. package/src/unstable/useModelContextSnapshot.ts +67 -0
  87. package/src/unstable/useSlashCommandAdapter.ts +2 -9
  88. package/src/unstable/webmcp/WebMcpRegistrationResource.ts +92 -0
  89. package/src/unstable/webmcp/__tests__/webmcp.fake.tsx +168 -0
  90. package/src/unstable/webmcp/convertTools.test.ts +393 -0
  91. package/src/unstable/webmcp/convertTools.ts +180 -0
  92. package/src/unstable/webmcp/useWebMcpProvider.test.tsx +466 -0
  93. package/src/unstable/webmcp/useWebMcpProvider.ts +150 -0
  94. package/src/unstable/webmcp/webmcp-host.test.ts +142 -0
  95. package/src/unstable/webmcp/webmcp-host.ts +83 -0
  96. package/dist/utils/invokeCallbackSafely.d.ts +0 -5
  97. package/dist/utils/invokeCallbackSafely.d.ts.map +0 -1
  98. package/dist/utils/invokeCallbackSafely.js +0 -15
  99. package/dist/utils/invokeCallbackSafely.js.map +0 -1
  100. package/dist/utils/useToolArgsFieldStatus.d.ts +0 -19
  101. package/dist/utils/useToolArgsFieldStatus.d.ts.map +0 -1
  102. package/dist/utils/useToolArgsFieldStatus.js +0 -23
  103. package/dist/utils/useToolArgsFieldStatus.js.map +0 -1
  104. package/src/utils/invokeCallbackSafely.ts +0 -11
  105. package/src/utils/useToolArgsFieldStatus.ts +0 -18
@@ -1 +1 @@
1
- {"version":3,"file":"triggerNavigationResource.js","names":["useEffect","useEffectEvent","useMemo","useState","resource","Unstable_TriggerAdapter","Unstable_TriggerCategory","Unstable_TriggerItem","matchesQuery","item","lower","id","toLowerCase","includes","label","description","TriggerNavigationResourceOutput","categories","items","isSearchMode","activeCategoryId","navigableList","selectCategory","categoryId","goBack","useTriggerNavigationResource","t0","$","_c","adapter","query","open","setActiveCategoryId","t1","t2","t3","bb0","t4","Symbol","for","effectiveActiveCategoryId","bb1","t5","categoryItems","allItems","bb2","length","search","t6","all","cat","push","searchResults","bb3","t7","lower_0","filter","cat_0","filteredCategories","bb4","t8","lower_1","item_0","filteredItems","bb5","t9","t10","t11","TriggerNavigationResource"],"sources":["../../../../src/primitives/composer/trigger/triggerNavigationResource.ts"],"sourcesContent":["import { useEffect, useEffectEvent, useMemo, useState } from \"react\";\nimport { resource } from \"@assistant-ui/tap\";\nimport type {\n Unstable_TriggerAdapter,\n Unstable_TriggerCategory,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\n\nfunction matchesQuery(item: Unstable_TriggerItem, lower: string): boolean {\n return (\n item.id.toLowerCase().includes(lower) ||\n item.label.toLowerCase().includes(lower) ||\n (item.description?.toLowerCase().includes(lower) ?? false)\n );\n}\n\nexport type TriggerNavigationResourceOutput = {\n /** Filtered categories visible in the list (empty in search mode). */\n readonly categories: readonly Unstable_TriggerCategory[];\n /** Filtered items visible in the list. */\n readonly items: readonly Unstable_TriggerItem[];\n /** `true` when the current list is search results rather than categories. */\n readonly isSearchMode: boolean;\n /** Currently drilled-into category id (or `null` for the top level). */\n readonly activeCategoryId: string | null;\n /** Flat list used for keyboard navigation (categories or items). */\n readonly navigableList: readonly (\n | Unstable_TriggerCategory\n | Unstable_TriggerItem\n )[];\n /** Drill into a category. */\n selectCategory(categoryId: string): void;\n /** Return to the top-level category list. */\n goBack(): void;\n};\n\n/**\n * Computes categories, items, search results, and navigation state from the\n * adapter + current query. Pure derivation — no side effects on the composer.\n */\nconst useTriggerNavigationResource = ({\n adapter,\n query,\n open,\n}: {\n adapter: Unstable_TriggerAdapter | undefined;\n query: string;\n open: boolean;\n}): TriggerNavigationResourceOutput => {\n const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);\n\n useEffect(() => {\n if (!open) setActiveCategoryId(null);\n }, [open]);\n\n const categories = useMemo<readonly Unstable_TriggerCategory[]>(() => {\n if (!open || !adapter) return [];\n return adapter.categories();\n }, [open, adapter]);\n\n const effectiveActiveCategoryId = open ? activeCategoryId : null;\n\n const allItems = useMemo<readonly Unstable_TriggerItem[]>(() => {\n if (!effectiveActiveCategoryId || !adapter) return [];\n return adapter.categoryItems(effectiveActiveCategoryId);\n }, [effectiveActiveCategoryId, adapter]);\n\n const searchResults = useMemo<readonly Unstable_TriggerItem[] | null>(() => {\n if (!open || !adapter || effectiveActiveCategoryId) return null;\n // If categories exist and query is empty, show categories first (not search)\n if (!query && categories.length > 0) return null;\n if (adapter.search) return adapter.search(query);\n\n // fallback: no adapter.search\n const all: Unstable_TriggerItem[] = [];\n const lower = query.toLowerCase();\n for (const cat of categories) {\n for (const item of adapter.categoryItems(cat.id)) {\n if (matchesQuery(item, lower)) {\n all.push(item);\n }\n }\n }\n return all;\n }, [open, adapter, query, effectiveActiveCategoryId, categories]);\n\n const isSearchMode = searchResults !== null;\n\n const filteredCategories = useMemo(() => {\n if (isSearchMode) return [];\n if (!query) return categories;\n const lower = query.toLowerCase();\n return categories.filter((cat) => cat.label.toLowerCase().includes(lower));\n }, [categories, query, isSearchMode]);\n\n const filteredItems = useMemo(() => {\n if (isSearchMode) return searchResults ?? [];\n if (!query) return allItems;\n const lower = query.toLowerCase();\n return allItems.filter((item) => matchesQuery(item, lower));\n }, [allItems, query, isSearchMode, searchResults]);\n\n const navigableList = useMemo(() => {\n if (isSearchMode) return searchResults ?? [];\n if (effectiveActiveCategoryId) return filteredItems;\n return filteredCategories;\n }, [\n isSearchMode,\n searchResults,\n effectiveActiveCategoryId,\n filteredItems,\n filteredCategories,\n ]);\n\n const selectCategory = useEffectEvent((categoryId: string) => {\n setActiveCategoryId(categoryId);\n });\n\n const goBack = useEffectEvent(() => {\n setActiveCategoryId(null);\n });\n\n return {\n categories: filteredCategories,\n items: filteredItems,\n isSearchMode,\n activeCategoryId: effectiveActiveCategoryId,\n navigableList,\n selectCategory,\n goBack,\n };\n};\n\nexport const TriggerNavigationResource = resource(useTriggerNavigationResource);\n"],"mappings":"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"}
1
+ {"version":3,"file":"triggerNavigationResource.js","names":["useEffect","useEffectEvent","useMemo","useState","resource","Unstable_TriggerAdapter","Unstable_TriggerCategory","Unstable_TriggerItem","matchesTriggerItemQuery","TriggerNavigationResourceOutput","categories","items","isSearchMode","activeCategoryId","navigableList","selectCategory","categoryId","goBack","useTriggerNavigationResource","t0","$","_c","adapter","query","open","setActiveCategoryId","t1","t2","t3","bb0","t4","Symbol","for","effectiveActiveCategoryId","bb1","t5","categoryItems","allItems","bb2","length","search","t6","all","lower","toLowerCase","cat","item","id","push","searchResults","bb3","t7","lower_0","filter","cat_0","label","includes","filteredCategories","bb4","t8","lower_1","item_0","filteredItems","bb5","t9","t10","t11","TriggerNavigationResource"],"sources":["../../../../src/primitives/composer/trigger/triggerNavigationResource.ts"],"sourcesContent":["import { useEffect, useEffectEvent, useMemo, useState } from \"react\";\nimport { resource } from \"@assistant-ui/tap\";\nimport type {\n Unstable_TriggerAdapter,\n Unstable_TriggerCategory,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\nimport { matchesTriggerItemQuery } from \"./matchesTriggerItemQuery\";\n\nexport type TriggerNavigationResourceOutput = {\n /** Filtered categories visible in the list (empty in search mode). */\n readonly categories: readonly Unstable_TriggerCategory[];\n /** Filtered items visible in the list. */\n readonly items: readonly Unstable_TriggerItem[];\n /** `true` when the current list is search results rather than categories. */\n readonly isSearchMode: boolean;\n /** Currently drilled-into category id (or `null` for the top level). */\n readonly activeCategoryId: string | null;\n /** Flat list used for keyboard navigation (categories or items). */\n readonly navigableList: readonly (\n | Unstable_TriggerCategory\n | Unstable_TriggerItem\n )[];\n /** Drill into a category. */\n selectCategory(categoryId: string): void;\n /** Return to the top-level category list. */\n goBack(): void;\n};\n\n/**\n * Computes categories, items, search results, and navigation state from the\n * adapter + current query. Pure derivation — no side effects on the composer.\n */\nconst useTriggerNavigationResource = ({\n adapter,\n query,\n open,\n}: {\n adapter: Unstable_TriggerAdapter | undefined;\n query: string;\n open: boolean;\n}): TriggerNavigationResourceOutput => {\n const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);\n\n useEffect(() => {\n if (!open) setActiveCategoryId(null);\n }, [open]);\n\n const categories = useMemo<readonly Unstable_TriggerCategory[]>(() => {\n if (!open || !adapter) return [];\n return adapter.categories();\n }, [open, adapter]);\n\n const effectiveActiveCategoryId = open ? activeCategoryId : null;\n\n const allItems = useMemo<readonly Unstable_TriggerItem[]>(() => {\n if (!effectiveActiveCategoryId || !adapter) return [];\n return adapter.categoryItems(effectiveActiveCategoryId);\n }, [effectiveActiveCategoryId, adapter]);\n\n const searchResults = useMemo<readonly Unstable_TriggerItem[] | null>(() => {\n if (!open || !adapter || effectiveActiveCategoryId) return null;\n // If categories exist and query is empty, show categories first (not search)\n if (!query && categories.length > 0) return null;\n if (adapter.search) return adapter.search(query);\n\n // fallback: no adapter.search\n const all: Unstable_TriggerItem[] = [];\n const lower = query.toLowerCase();\n for (const cat of categories) {\n for (const item of adapter.categoryItems(cat.id)) {\n if (matchesTriggerItemQuery(item, lower)) {\n all.push(item);\n }\n }\n }\n return all;\n }, [open, adapter, query, effectiveActiveCategoryId, categories]);\n\n const isSearchMode = searchResults !== null;\n\n const filteredCategories = useMemo(() => {\n if (isSearchMode) return [];\n if (!query) return categories;\n const lower = query.toLowerCase();\n return categories.filter((cat) => cat.label.toLowerCase().includes(lower));\n }, [categories, query, isSearchMode]);\n\n const filteredItems = useMemo(() => {\n if (isSearchMode) return searchResults ?? [];\n if (!query) return allItems;\n const lower = query.toLowerCase();\n return allItems.filter((item) => matchesTriggerItemQuery(item, lower));\n }, [allItems, query, isSearchMode, searchResults]);\n\n const navigableList = useMemo(() => {\n if (isSearchMode) return searchResults ?? [];\n if (effectiveActiveCategoryId) return filteredItems;\n return filteredCategories;\n }, [\n isSearchMode,\n searchResults,\n effectiveActiveCategoryId,\n filteredItems,\n filteredCategories,\n ]);\n\n const selectCategory = useEffectEvent((categoryId: string) => {\n setActiveCategoryId(categoryId);\n });\n\n const goBack = useEffectEvent(() => {\n setActiveCategoryId(null);\n });\n\n return {\n categories: filteredCategories,\n items: filteredItems,\n isSearchMode,\n activeCategoryId: effectiveActiveCategoryId,\n navigableList,\n selectCategory,\n goBack,\n };\n};\n\nexport const TriggerNavigationResource = resource(useTriggerNavigationResource);\n"],"mappings":"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"}
@@ -40,7 +40,7 @@ declare const MessagePartPrimitiveText: import("react").ForwardRefExoticComponen
40
40
  asChild?: boolean;
41
41
  }, "ref"> & {
42
42
  render?: import("react").ReactElement | undefined;
43
- } & import("react").RefAttributes<HTMLSpanElement>, "ref">, "asChild" | "children"> & {
43
+ } & import("react").RefAttributes<HTMLSpanElement>, "ref">, "children" | "asChild"> & {
44
44
  /**
45
45
  * Whether to enable smooth text streaming animation.
46
46
  * When enabled, text appears with a typing effect as it streams in.
@@ -1 +1 @@
1
- {"version":3,"file":"useThreadViewportAutoScroll.d.ts","names":[],"sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"mappings":";;kBAYiB;OACH;;;;;;;IAOV;;;;;;IAOA;;;;;;IAOA;;;;;;IAOA;;;cAIS,8BAA+B,iBAAiB,eAAa,YAAA,0BAAA,4BAAA,gCAKvE,4BAA4B,YAAU,YAAY"}
1
+ {"version":3,"file":"useThreadViewportAutoScroll.d.ts","names":[],"sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"mappings":";;kBAgBiB;OACH;;;;;;;IAOV;;;;;;IAOA;;;;;;IAOA;;;;;;IAOA;;;cAIS,8BAA+B,iBAAiB,eAAa,YAAA,0BAAA,4BAAA,gCAKvE,4BAA4B,YAAU,YAAY"}
@@ -7,7 +7,7 @@ import { useOnResizeContent } from "../../utils/hooks/useOnResizeContent.js";
7
7
  import { useAuiEvent, useAuiState } from "@assistant-ui/store";
8
8
  import { useCallback, useLayoutEffect, useRef } from "@assistant-ui/tap/react-shim";
9
9
  import { useComposedRefs } from "radix-ui/internal";
10
- import { isUserScrollUp } from "@assistant-ui/store/client";
10
+ import { isUserScrollUp, isViewportAtBottom, viewportOverflows } from "@assistant-ui/store/client";
11
11
  //#region src/primitives/thread/useThreadViewportAutoScroll.ts
12
12
  const useThreadViewportAutoScroll = ({ autoScroll, scrollToBottomOnRunStart = true, scrollToBottomOnInitialize = true, scrollToBottomOnThreadSwitch = true }) => {
13
13
  const divRef = useRef(null);
@@ -55,14 +55,14 @@ const useThreadViewportAutoScroll = ({ autoScroll, scrollToBottomOnRunStart = tr
55
55
  const div_0 = divRef.current;
56
56
  if (!div_0) return;
57
57
  const isAtBottom = threadViewportStore.getState().isAtBottom;
58
- const newIsAtBottom = Math.abs(div_0.scrollHeight - div_0.scrollTop - div_0.clientHeight) <= 1 || div_0.scrollHeight <= div_0.clientHeight;
58
+ const newIsAtBottom = isViewportAtBottom(div_0);
59
59
  if (!newIsAtBottom && lastScrollTop.current < div_0.scrollTop) {} else {
60
60
  const userScrolledUp = isUserScrollUp({
61
61
  scrollTop: lastScrollTop.current,
62
62
  scrollHeight: lastScrollHeight.current
63
63
  }, div_0);
64
64
  if (newIsAtBottom) {
65
- if (div_0.scrollHeight > div_0.clientHeight + 1) scrollingToBottomBehaviorRef.current = null;
65
+ if (viewportOverflows(div_0)) scrollingToBottomBehaviorRef.current = null;
66
66
  if (autoScroll) followBottomRef.current = true;
67
67
  } else if (userScrolledUp) {
68
68
  cancelScheduledFrame();
@@ -1 +1 @@
1
- {"version":3,"file":"useThreadViewportAutoScroll.js","names":["useComposedRefs","useCallback","useLayoutEffect","useRef","RefCallback","useAuiEvent","useAuiState","isUserScrollUp","useOnResizeContent","useOnScrollToBottom","useManagedRef","writableStore","useThreadViewportStore","useThreadViewportAutoScroll","Options","autoScroll","scrollToBottomOnRunStart","scrollToBottomOnInitialize","scrollToBottomOnThreadSwitch","HTMLElement","TElement","divRef","hasMessages","s","thread","messages","length","isRunning","initializeScrollRequestedRef","scheduledFrameRef","threadViewportStore","undefined","getState","turnAnchor","lastScrollTop","lastScrollHeight","lastObservedScrollHeight","lastObservedClientHeight","scrollingToBottomBehaviorRef","ScrollBehavior","followBottomRef","scrollToBottom","behavior","div","current","scrollTo","top","scrollHeight","cancelScheduledFrame","cancelAnimationFrame","scheduleScrollToBottom","requestAnimationFrame","hasActiveTopAnchor","state","element","viewport","anchor","handleScroll","isAtBottom","newIsAtBottom","Math","abs","scrollTop","clientHeight","isInFlightDownwardScroll","userScrolledUp","viewportOverflows","shouldUpdate","setState","resizeRef","scrollBehavior","scrollRef","el","cancelPendingScrollToBottom","addEventListener","removeEventListener","autoScrollRef"],"sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"sourcesContent":["\"use client\";\n\nimport { useComposedRefs } from \"radix-ui/internal\";\nimport { useCallback, useLayoutEffect, useRef, type RefCallback } from \"react\";\nimport { useAuiEvent, useAuiState } from \"@assistant-ui/store\";\nimport { isUserScrollUp } from \"@assistant-ui/store/client\";\nimport { useOnResizeContent } from \"../../utils/hooks/useOnResizeContent\";\nimport { useOnScrollToBottom } from \"../../utils/hooks/useOnScrollToBottom\";\nimport { useManagedRef } from \"../../utils/hooks/useManagedRef\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\nimport { useThreadViewportStore } from \"../../context/react/ThreadViewportContext\";\n\nexport namespace useThreadViewportAutoScroll {\n export type Options = {\n /**\n * Whether to automatically scroll to the bottom when new messages are added.\n * When enabled, the viewport will automatically scroll to show the latest content.\n *\n * Default false if `turnAnchor` is \"top\", otherwise defaults to true.\n */\n autoScroll?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when a new run starts.\n *\n * Defaults to true.\n */\n scrollToBottomOnRunStart?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when messages first appear in the thread.\n *\n * Defaults to true.\n */\n scrollToBottomOnInitialize?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when switching to a different thread.\n *\n * Defaults to true.\n */\n scrollToBottomOnThreadSwitch?: boolean | undefined;\n };\n}\n\nexport const useThreadViewportAutoScroll = <TElement extends HTMLElement>({\n autoScroll,\n scrollToBottomOnRunStart = true,\n scrollToBottomOnInitialize = true,\n scrollToBottomOnThreadSwitch = true,\n}: useThreadViewportAutoScroll.Options): RefCallback<TElement> => {\n const divRef = useRef<TElement>(null);\n const hasMessages = useAuiState((s) => s.thread.messages.length > 0);\n const isRunning = useAuiState((s) => s.thread.isRunning);\n const initializeScrollRequestedRef = useRef(false);\n const scheduledFrameRef = useRef<number | null>(null);\n\n const threadViewportStore = useThreadViewportStore();\n if (autoScroll === undefined) {\n autoScroll = threadViewportStore.getState().turnAnchor !== \"top\";\n }\n\n const lastScrollTop = useRef<number>(0);\n const lastScrollHeight = useRef<number>(0);\n const lastObservedScrollHeight = useRef<number>(0);\n const lastObservedClientHeight = useRef<number>(0);\n\n // Pending bottom-scroll intent. Planted by initialize/run-start/switch/button\n // triggers, cleared when handleScroll confirms we reached bottom, or when the\n // user actively scrolls up while content size is stable.\n const scrollingToBottomBehaviorRef = useRef<ScrollBehavior | null>(null);\n const followBottomRef = useRef(autoScroll);\n\n const scrollToBottom = useCallback((behavior: ScrollBehavior) => {\n const div = divRef.current;\n if (!div) return;\n\n followBottomRef.current = true;\n scrollingToBottomBehaviorRef.current = behavior;\n div.scrollTo({ top: div.scrollHeight, behavior });\n }, []);\n\n const cancelScheduledFrame = useCallback(() => {\n if (scheduledFrameRef.current === null) return;\n cancelAnimationFrame(scheduledFrameRef.current);\n scheduledFrameRef.current = null;\n }, []);\n\n const scheduleScrollToBottom = useCallback(\n (behavior: ScrollBehavior) => {\n scrollingToBottomBehaviorRef.current = behavior;\n cancelScheduledFrame();\n scheduledFrameRef.current = requestAnimationFrame(() => {\n scheduledFrameRef.current = null;\n scrollToBottom(behavior);\n });\n },\n [cancelScheduledFrame, scrollToBottom],\n );\n\n useLayoutEffect(() => () => cancelScheduledFrame(), [cancelScheduledFrame]);\n\n const hasActiveTopAnchor = useCallback(() => {\n const state = threadViewportStore.getState();\n return (\n state.turnAnchor === \"top\" &&\n state.element.viewport === divRef.current &&\n state.element.anchor !== null\n );\n }, [threadViewportStore]);\n\n const handleScroll = () => {\n const div = divRef.current;\n if (!div) return;\n\n const isAtBottom = threadViewportStore.getState().isAtBottom;\n const newIsAtBottom =\n Math.abs(div.scrollHeight - div.scrollTop - div.clientHeight) <= 1 ||\n div.scrollHeight <= div.clientHeight;\n\n const isInFlightDownwardScroll =\n !newIsAtBottom && lastScrollTop.current < div.scrollTop;\n if (isInFlightDownwardScroll) {\n // no-op: a smooth scroll-to-bottom fires many midpoint scroll events\n // before landing, don't flicker isAtBottom or clear intent mid-animation\n } else {\n const userScrolledUp = isUserScrollUp(\n {\n scrollTop: lastScrollTop.current,\n scrollHeight: lastScrollHeight.current,\n },\n div,\n );\n\n if (newIsAtBottom) {\n // newIsAtBottom is ambiguous when the viewport doesn't overflow —\n // keep intent alive until content can actually scroll\n const viewportOverflows = div.scrollHeight > div.clientHeight + 1;\n if (viewportOverflows) {\n scrollingToBottomBehaviorRef.current = null;\n }\n if (autoScroll) followBottomRef.current = true;\n } else if (userScrolledUp) {\n cancelScheduledFrame();\n scrollingToBottomBehaviorRef.current = null;\n followBottomRef.current = false;\n }\n\n const shouldUpdate =\n newIsAtBottom || scrollingToBottomBehaviorRef.current === null;\n\n if (shouldUpdate && newIsAtBottom !== isAtBottom) {\n writableStore(threadViewportStore).setState({\n isAtBottom: newIsAtBottom,\n });\n }\n }\n\n lastScrollTop.current = div.scrollTop;\n lastScrollHeight.current = div.scrollHeight;\n };\n\n const resizeRef = useOnResizeContent(() => {\n const div = divRef.current;\n if (!div) return;\n\n const { scrollHeight, clientHeight } = div;\n if (\n scrollHeight === lastObservedScrollHeight.current &&\n clientHeight === lastObservedClientHeight.current\n ) {\n return;\n }\n lastObservedScrollHeight.current = scrollHeight;\n lastObservedClientHeight.current = clientHeight;\n\n const scrollBehavior = scrollingToBottomBehaviorRef.current;\n if (scrollBehavior && hasActiveTopAnchor()) {\n // Let the top-anchor reserve own scrolling while a run starts to avoid a bottom-scroll race.\n scrollingToBottomBehaviorRef.current = null;\n } else if (scrollBehavior) {\n scrollToBottom(scrollBehavior);\n } else if (\n autoScroll &&\n !(isRunning && hasActiveTopAnchor()) &&\n followBottomRef.current\n ) {\n scrollToBottom(\"instant\");\n }\n\n handleScroll();\n });\n\n const scrollRef = useManagedRef<HTMLElement>((el) => {\n // A pointer gesture invalidates pending bottom-scroll intent; otherwise an\n // intent kept alive by a non-overflowing thread (see handleScroll) hijacks\n // the next content growth, e.g. expanding a collapsible tool call.\n const cancelPendingScrollToBottom = () => {\n scrollingToBottomBehaviorRef.current = null;\n };\n el.addEventListener(\"scroll\", handleScroll);\n el.addEventListener(\"pointerdown\", cancelPendingScrollToBottom);\n return () => {\n el.removeEventListener(\"scroll\", handleScroll);\n el.removeEventListener(\"pointerdown\", cancelPendingScrollToBottom);\n };\n });\n\n useLayoutEffect(() => {\n if (!scrollToBottomOnInitialize) return;\n if (!hasMessages) {\n initializeScrollRequestedRef.current = false;\n return;\n }\n if (initializeScrollRequestedRef.current) return;\n\n initializeScrollRequestedRef.current = true;\n // defer to an in-flight run (e.g. first message on a new thread) that\n // already planted intent — otherwise we'd downgrade its \"auto\" to \"instant\"\n if (scrollingToBottomBehaviorRef.current !== null) return;\n scheduleScrollToBottom(\"instant\");\n }, [hasMessages, scheduleScrollToBottom, scrollToBottomOnInitialize]);\n\n useOnScrollToBottom(({ behavior }) => {\n scrollToBottom(behavior);\n });\n\n useAuiEvent(\"thread.runStart\", () => {\n if (!scrollToBottomOnRunStart) return;\n if (threadViewportStore.getState().turnAnchor === \"top\") return;\n scheduleScrollToBottom(\"auto\");\n });\n\n useAuiEvent(\"threads.selectionChanged\", () => {\n if (!scrollToBottomOnThreadSwitch) return;\n scheduleScrollToBottom(\"instant\");\n });\n\n const autoScrollRef = useComposedRefs<TElement>(resizeRef, scrollRef, divRef);\n return autoScrollRef as RefCallback<TElement>;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useThreadViewportAutoScroll.js","names":["useComposedRefs","useCallback","useLayoutEffect","useRef","RefCallback","useAuiEvent","useAuiState","isUserScrollUp","isViewportAtBottom","viewportOverflows","useOnResizeContent","useOnScrollToBottom","useManagedRef","writableStore","useThreadViewportStore","useThreadViewportAutoScroll","Options","autoScroll","scrollToBottomOnRunStart","scrollToBottomOnInitialize","scrollToBottomOnThreadSwitch","HTMLElement","TElement","divRef","hasMessages","s","thread","messages","length","isRunning","initializeScrollRequestedRef","scheduledFrameRef","threadViewportStore","undefined","getState","turnAnchor","lastScrollTop","lastScrollHeight","lastObservedScrollHeight","lastObservedClientHeight","scrollingToBottomBehaviorRef","ScrollBehavior","followBottomRef","scrollToBottom","behavior","div","current","scrollTo","top","scrollHeight","cancelScheduledFrame","cancelAnimationFrame","scheduleScrollToBottom","requestAnimationFrame","hasActiveTopAnchor","state","element","viewport","anchor","handleScroll","isAtBottom","newIsAtBottom","isInFlightDownwardScroll","scrollTop","userScrolledUp","shouldUpdate","setState","resizeRef","clientHeight","scrollBehavior","scrollRef","el","cancelPendingScrollToBottom","addEventListener","removeEventListener","autoScrollRef"],"sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"sourcesContent":["\"use client\";\n\nimport { useComposedRefs } from \"radix-ui/internal\";\nimport { useCallback, useLayoutEffect, useRef, type RefCallback } from \"react\";\nimport { useAuiEvent, useAuiState } from \"@assistant-ui/store\";\nimport {\n isUserScrollUp,\n isViewportAtBottom,\n viewportOverflows,\n} from \"@assistant-ui/store/client\";\nimport { useOnResizeContent } from \"../../utils/hooks/useOnResizeContent\";\nimport { useOnScrollToBottom } from \"../../utils/hooks/useOnScrollToBottom\";\nimport { useManagedRef } from \"../../utils/hooks/useManagedRef\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\nimport { useThreadViewportStore } from \"../../context/react/ThreadViewportContext\";\n\nexport namespace useThreadViewportAutoScroll {\n export type Options = {\n /**\n * Whether to automatically scroll to the bottom when new messages are added.\n * When enabled, the viewport will automatically scroll to show the latest content.\n *\n * Default false if `turnAnchor` is \"top\", otherwise defaults to true.\n */\n autoScroll?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when a new run starts.\n *\n * Defaults to true.\n */\n scrollToBottomOnRunStart?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when messages first appear in the thread.\n *\n * Defaults to true.\n */\n scrollToBottomOnInitialize?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when switching to a different thread.\n *\n * Defaults to true.\n */\n scrollToBottomOnThreadSwitch?: boolean | undefined;\n };\n}\n\nexport const useThreadViewportAutoScroll = <TElement extends HTMLElement>({\n autoScroll,\n scrollToBottomOnRunStart = true,\n scrollToBottomOnInitialize = true,\n scrollToBottomOnThreadSwitch = true,\n}: useThreadViewportAutoScroll.Options): RefCallback<TElement> => {\n const divRef = useRef<TElement>(null);\n const hasMessages = useAuiState((s) => s.thread.messages.length > 0);\n const isRunning = useAuiState((s) => s.thread.isRunning);\n const initializeScrollRequestedRef = useRef(false);\n const scheduledFrameRef = useRef<number | null>(null);\n\n const threadViewportStore = useThreadViewportStore();\n if (autoScroll === undefined) {\n autoScroll = threadViewportStore.getState().turnAnchor !== \"top\";\n }\n\n const lastScrollTop = useRef<number>(0);\n const lastScrollHeight = useRef<number>(0);\n const lastObservedScrollHeight = useRef<number>(0);\n const lastObservedClientHeight = useRef<number>(0);\n\n // Pending bottom-scroll intent. Planted by initialize/run-start/switch/button\n // triggers, cleared when handleScroll confirms we reached bottom, or when the\n // user actively scrolls up while content size is stable.\n const scrollingToBottomBehaviorRef = useRef<ScrollBehavior | null>(null);\n const followBottomRef = useRef(autoScroll);\n\n const scrollToBottom = useCallback((behavior: ScrollBehavior) => {\n const div = divRef.current;\n if (!div) return;\n\n followBottomRef.current = true;\n scrollingToBottomBehaviorRef.current = behavior;\n div.scrollTo({ top: div.scrollHeight, behavior });\n }, []);\n\n const cancelScheduledFrame = useCallback(() => {\n if (scheduledFrameRef.current === null) return;\n cancelAnimationFrame(scheduledFrameRef.current);\n scheduledFrameRef.current = null;\n }, []);\n\n const scheduleScrollToBottom = useCallback(\n (behavior: ScrollBehavior) => {\n scrollingToBottomBehaviorRef.current = behavior;\n cancelScheduledFrame();\n scheduledFrameRef.current = requestAnimationFrame(() => {\n scheduledFrameRef.current = null;\n scrollToBottom(behavior);\n });\n },\n [cancelScheduledFrame, scrollToBottom],\n );\n\n useLayoutEffect(() => () => cancelScheduledFrame(), [cancelScheduledFrame]);\n\n const hasActiveTopAnchor = useCallback(() => {\n const state = threadViewportStore.getState();\n return (\n state.turnAnchor === \"top\" &&\n state.element.viewport === divRef.current &&\n state.element.anchor !== null\n );\n }, [threadViewportStore]);\n\n const handleScroll = () => {\n const div = divRef.current;\n if (!div) return;\n\n const isAtBottom = threadViewportStore.getState().isAtBottom;\n const newIsAtBottom = isViewportAtBottom(div);\n\n const isInFlightDownwardScroll =\n !newIsAtBottom && lastScrollTop.current < div.scrollTop;\n if (isInFlightDownwardScroll) {\n // no-op: a smooth scroll-to-bottom fires many midpoint scroll events\n // before landing, don't flicker isAtBottom or clear intent mid-animation\n } else {\n const userScrolledUp = isUserScrollUp(\n {\n scrollTop: lastScrollTop.current,\n scrollHeight: lastScrollHeight.current,\n },\n div,\n );\n\n if (newIsAtBottom) {\n // newIsAtBottom is ambiguous when the viewport doesn't overflow —\n // keep intent alive until content can actually scroll\n if (viewportOverflows(div)) {\n scrollingToBottomBehaviorRef.current = null;\n }\n if (autoScroll) followBottomRef.current = true;\n } else if (userScrolledUp) {\n cancelScheduledFrame();\n scrollingToBottomBehaviorRef.current = null;\n followBottomRef.current = false;\n }\n\n const shouldUpdate =\n newIsAtBottom || scrollingToBottomBehaviorRef.current === null;\n\n if (shouldUpdate && newIsAtBottom !== isAtBottom) {\n writableStore(threadViewportStore).setState({\n isAtBottom: newIsAtBottom,\n });\n }\n }\n\n lastScrollTop.current = div.scrollTop;\n lastScrollHeight.current = div.scrollHeight;\n };\n\n const resizeRef = useOnResizeContent(() => {\n const div = divRef.current;\n if (!div) return;\n\n const { scrollHeight, clientHeight } = div;\n if (\n scrollHeight === lastObservedScrollHeight.current &&\n clientHeight === lastObservedClientHeight.current\n ) {\n return;\n }\n lastObservedScrollHeight.current = scrollHeight;\n lastObservedClientHeight.current = clientHeight;\n\n const scrollBehavior = scrollingToBottomBehaviorRef.current;\n if (scrollBehavior && hasActiveTopAnchor()) {\n // Let the top-anchor reserve own scrolling while a run starts to avoid a bottom-scroll race.\n scrollingToBottomBehaviorRef.current = null;\n } else if (scrollBehavior) {\n scrollToBottom(scrollBehavior);\n } else if (\n autoScroll &&\n !(isRunning && hasActiveTopAnchor()) &&\n followBottomRef.current\n ) {\n scrollToBottom(\"instant\");\n }\n\n handleScroll();\n });\n\n const scrollRef = useManagedRef<HTMLElement>((el) => {\n // A pointer gesture invalidates pending bottom-scroll intent; otherwise an\n // intent kept alive by a non-overflowing thread (see handleScroll) hijacks\n // the next content growth, e.g. expanding a collapsible tool call.\n const cancelPendingScrollToBottom = () => {\n scrollingToBottomBehaviorRef.current = null;\n };\n el.addEventListener(\"scroll\", handleScroll);\n el.addEventListener(\"pointerdown\", cancelPendingScrollToBottom);\n return () => {\n el.removeEventListener(\"scroll\", handleScroll);\n el.removeEventListener(\"pointerdown\", cancelPendingScrollToBottom);\n };\n });\n\n useLayoutEffect(() => {\n if (!scrollToBottomOnInitialize) return;\n if (!hasMessages) {\n initializeScrollRequestedRef.current = false;\n return;\n }\n if (initializeScrollRequestedRef.current) return;\n\n initializeScrollRequestedRef.current = true;\n // defer to an in-flight run (e.g. first message on a new thread) that\n // already planted intent — otherwise we'd downgrade its \"auto\" to \"instant\"\n if (scrollingToBottomBehaviorRef.current !== null) return;\n scheduleScrollToBottom(\"instant\");\n }, [hasMessages, scheduleScrollToBottom, scrollToBottomOnInitialize]);\n\n useOnScrollToBottom(({ behavior }) => {\n scrollToBottom(behavior);\n });\n\n useAuiEvent(\"thread.runStart\", () => {\n if (!scrollToBottomOnRunStart) return;\n if (threadViewportStore.getState().turnAnchor === \"top\") return;\n scheduleScrollToBottom(\"auto\");\n });\n\n useAuiEvent(\"threads.selectionChanged\", () => {\n if (!scrollToBottomOnThreadSwitch) return;\n scheduleScrollToBottom(\"instant\");\n });\n\n const autoScrollRef = useComposedRefs<TElement>(resizeRef, scrollRef, divRef);\n return autoScrollRef as RefCallback<TElement>;\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SandboxHost.d.ts","names":[],"sources":["../../src/sandbox-host/SandboxHost.tsx"],"mappings":";;;KAmBY;EACV,UAAU;EACV;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;;KAGU,mBAAmB,KAC7B;KAIU;EACV,YAAY;;KAGF;EACV,YAAY,OAAO;EACnB;;KAGU;EAAmB;;KAEnB;EACV,SAAS;EACT;EACA,UAAU;EACV;EACA,eACE,OAAO,kBACP,MAAM,mBACH;EACL,YAAY,OAAO;EACnB,iBAAiB;;iBAGH,sBACd,OAAO,cACP;EAAS,QAAQ;EAAmB;;iBActB,cACd,SACA,YACA,SACA,WACA,cACA,SACA,kBACC,mCAAgB,IAAA"}
1
+ {"version":3,"file":"SandboxHost.d.ts","names":[],"sources":["../../src/sandbox-host/SandboxHost.tsx"],"mappings":";;;KAqBY;EACV,UAAU;EACV;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;;KAGU,mBAAmB,KAC7B;KAIU;EACV,YAAY;;KAGF;EACV,YAAY,OAAO;EACnB;;KAGU;EAAmB;;KAEnB;EACV,SAAS;EACT;EACA,UAAU;EACV;EACA,eACE,OAAO,kBACP,MAAM,mBACH;EACL,YAAY,OAAO;EACnB,iBAAiB;;iBAGH,sBACd,OAAO,cACP;EAAS,QAAQ;EAAmB;;iBActB,cACd,SACA,YACA,SACA,WACA,cACA,SACA,kBACC,mCAAgB,IAAA"}
@@ -1,12 +1,13 @@
1
1
  "use client";
2
- import { invokeCallbackSafely } from "../utils/invokeCallbackSafely.js";
3
2
  import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
4
3
  import { useEffect, useLayoutEffect, useRef, useState } from "@assistant-ui/tap/react-shim";
5
4
  import { jsx } from "react/jsx-runtime";
6
- import { SafeContentFrame } from "safe-content-frame";
5
+ import { invokeUserCallback } from "@assistant-ui/core/internal";
6
+ import { SafeContentFrame, isShimLoadError } from "safe-content-frame";
7
7
  //#region src/sandbox-host/SandboxHost.tsx
8
8
  const DEFAULT_PRODUCT = "assistant-ui-sandbox";
9
9
  const DEFAULT_MAX_HEIGHT = 800;
10
+ const LOAD_TIMEOUT_MS = 1e4;
10
11
  function isSandboxFrameMessage(event, frame) {
11
12
  return event.source === frame.iframe.contentWindow && event.origin === frame.origin;
12
13
  }
@@ -69,6 +70,10 @@ function SandboxHost(t0) {
69
70
  let bridge = null;
70
71
  let onMessage = null;
71
72
  const { content: liveContent, sandbox: sb } = liveRef.current;
73
+ const reportError = (err) => {
74
+ const error = err instanceof Error ? err : new Error(String(err));
75
+ invokeUserCallback("assistant-ui", "SandboxHost onError", liveRef.current.onError?.bind(liveRef.current), error);
76
+ };
72
77
  const scf = new SafeContentFrame(sb?.product ?? DEFAULT_PRODUCT, {
73
78
  ...sb?.sandbox !== void 0 && { sandbox: sb.sandbox },
74
79
  ...sb?.useShadowDom !== void 0 && { useShadowDom: sb.useShadowDom },
@@ -94,12 +99,16 @@ function SandboxHost(t0) {
94
99
  bridge?.onMessage(event);
95
100
  };
96
101
  window.addEventListener("message", onMessage);
97
- }).catch((err) => {
102
+ rendered.fullyLoadedPromiseWithTimeout(LOAD_TIMEOUT_MS).catch((err_0) => {
103
+ if (cancelled) return;
104
+ if (isShimLoadError(err_0) && err_0.code === "render-timeout") return;
105
+ reportError(err_0);
106
+ });
107
+ }).catch((err_1) => {
98
108
  if (cancelled) return;
99
109
  frame?.dispose();
100
110
  frame = null;
101
- const error = err instanceof Error ? err : new Error(String(err));
102
- invokeCallbackSafely(() => liveRef.current.onError?.(error), "SandboxHost onError");
111
+ reportError(err_1);
103
112
  });
104
113
  return () => {
105
114
  cancelled = true;
@@ -1 +1 @@
1
- {"version":3,"file":"SandboxHost.js","names":["c","_c","CSSProperties","useEffect","useLayoutEffect","useRef","useState","RenderedFrame","SafeContentFrame","SandboxOption","invokeCallbackSafely","DEFAULT_PRODUCT","DEFAULT_MAX_HEIGHT","SandboxHostConfig","sandbox","useShadowDom","enableBrowserCaching","salt","product","className","style","unsafeDocumentWrite","SandboxHostFrame","Pick","SandboxHostApi","setHeight","height","SandboxBridge","onMessage","event","MessageEvent","dispose","SandboxContent","html","SandboxHostProps","content","contentKey","maxHeight","createBridge","frame","host","onError","error","Error","containerProps","Record","isSandboxFrameMessage","iframe","HTMLIFrameElement","origin","source","contentWindow","LiveSnapshot","SandboxHost","t0","$","t1","undefined","containerRef","contentHeight","setContentHeight","t2","liveRef","t3","t4","current","t5","Symbol","for","container","cancelled","bridge","liveContent","sb","scf","renderOpts","renderHtml","then","rendered","hostApi","Number","isFinite","sendMessage","window","addEventListener","catch","err","String","removeEventListener","t6","resolvedHeight","Math","min","t7","mergedStyle","t8","t9"],"sources":["../../src/sandbox-host/SandboxHost.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type CSSProperties,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport {\n type RenderedFrame,\n SafeContentFrame,\n type SandboxOption,\n} from \"safe-content-frame\";\nimport { invokeCallbackSafely } from \"../utils/invokeCallbackSafely\";\n\nconst DEFAULT_PRODUCT = \"assistant-ui-sandbox\";\nconst DEFAULT_MAX_HEIGHT = 800;\n\nexport type SandboxHostConfig = {\n sandbox?: SandboxOption[];\n useShadowDom?: boolean;\n enableBrowserCaching?: boolean;\n salt?: string;\n product?: string;\n className?: string;\n style?: CSSProperties;\n unsafeDocumentWrite?: boolean;\n};\n\nexport type SandboxHostFrame = Pick<\n RenderedFrame,\n \"iframe\" | \"origin\" | \"sendMessage\"\n>;\n\nexport type SandboxHostApi = {\n setHeight: (height: number) => void;\n};\n\nexport type SandboxBridge = {\n onMessage: (event: MessageEvent) => void;\n dispose: () => void;\n};\n\nexport type SandboxContent = { html: string };\n\nexport type SandboxHostProps = {\n content: SandboxContent;\n contentKey: string;\n sandbox?: SandboxHostConfig | undefined;\n maxHeight?: number | undefined;\n createBridge: (\n frame: SandboxHostFrame,\n host: SandboxHostApi,\n ) => SandboxBridge;\n onError?: ((error: Error) => void) | undefined;\n containerProps?: Record<string, string | undefined> | undefined;\n};\n\nexport function isSandboxFrameMessage(\n event: MessageEvent,\n frame: { iframe: HTMLIFrameElement; origin: string },\n): boolean {\n return (\n event.source === frame.iframe.contentWindow && event.origin === frame.origin\n );\n}\n\ntype LiveSnapshot = {\n content: SandboxContent;\n sandbox: SandboxHostConfig | undefined;\n createBridge: SandboxHostProps[\"createBridge\"];\n onError: SandboxHostProps[\"onError\"];\n};\n\nexport function SandboxHost({\n content,\n contentKey,\n sandbox,\n maxHeight = DEFAULT_MAX_HEIGHT,\n createBridge,\n onError,\n containerProps,\n}: SandboxHostProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [contentHeight, setContentHeight] = useState<number | undefined>(\n undefined,\n );\n\n const liveRef = useRef<LiveSnapshot>({\n content,\n sandbox,\n createBridge,\n onError,\n });\n useLayoutEffect(() => {\n liveRef.current = { content, sandbox, createBridge, onError };\n }, [content, sandbox, createBridge, onError]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n let cancelled = false;\n let frame: RenderedFrame | null = null;\n let bridge: SandboxBridge | null = null;\n let onMessage: ((event: MessageEvent) => void) | null = null;\n\n const { content: liveContent, sandbox: sb } = liveRef.current;\n\n const scf = new SafeContentFrame(sb?.product ?? DEFAULT_PRODUCT, {\n ...(sb?.sandbox !== undefined && { sandbox: sb.sandbox }),\n ...(sb?.useShadowDom !== undefined && { useShadowDom: sb.useShadowDom }),\n ...(sb?.enableBrowserCaching !== undefined && {\n enableBrowserCaching: sb.enableBrowserCaching,\n }),\n ...(sb?.salt !== undefined && { salt: sb.salt }),\n });\n\n const renderOpts =\n sb?.unsafeDocumentWrite !== undefined\n ? { unsafeDocumentWrite: sb.unsafeDocumentWrite }\n : undefined;\n\n scf\n .renderHtml(liveContent.html, container, renderOpts)\n .then((rendered) => {\n if (cancelled) {\n rendered.dispose();\n return;\n }\n frame = rendered;\n\n const hostApi: SandboxHostApi = {\n setHeight: (height) => {\n if (\n typeof height === \"number\" &&\n Number.isFinite(height) &&\n height > 0\n ) {\n setContentHeight(height);\n }\n },\n };\n\n bridge = liveRef.current.createBridge(\n {\n iframe: rendered.iframe,\n origin: rendered.origin,\n sendMessage: rendered.sendMessage,\n },\n hostApi,\n );\n\n // Single owner of the window listener; the cross-origin guard runs\n // here so the bridge only sees frame-validated messages.\n onMessage = (event) => {\n if (!isSandboxFrameMessage(event, rendered)) return;\n bridge?.onMessage(event);\n };\n window.addEventListener(\"message\", onMessage);\n })\n .catch((err) => {\n if (cancelled) return;\n frame?.dispose();\n frame = null;\n const error = err instanceof Error ? err : new Error(String(err));\n invokeCallbackSafely(\n () => liveRef.current.onError?.(error),\n \"SandboxHost onError\",\n );\n });\n\n return () => {\n cancelled = true;\n if (onMessage) {\n window.removeEventListener(\"message\", onMessage);\n onMessage = null;\n }\n bridge?.dispose();\n bridge = null;\n frame?.dispose();\n frame = null;\n setContentHeight(undefined);\n };\n // oxlint-disable-next-line react/exhaustive-deps -- re-init only on contentKey change; live values flow through liveRef\n }, [contentKey]);\n\n const resolvedHeight =\n contentHeight != null ? Math.min(contentHeight, maxHeight) : undefined;\n const mergedStyle =\n resolvedHeight != null\n ? { ...sandbox?.style, height: resolvedHeight }\n : sandbox?.style;\n\n return (\n <div\n {...containerProps}\n ref={containerRef}\n className={sandbox?.className}\n style={mergedStyle}\n />\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SandboxHost.js","names":["c","_c","CSSProperties","useEffect","useLayoutEffect","useRef","useState","isShimLoadError","RenderedFrame","SafeContentFrame","SandboxOption","invokeUserCallback","DEFAULT_PRODUCT","DEFAULT_MAX_HEIGHT","LOAD_TIMEOUT_MS","SandboxHostConfig","sandbox","useShadowDom","enableBrowserCaching","salt","product","className","style","unsafeDocumentWrite","SandboxHostFrame","Pick","SandboxHostApi","setHeight","height","SandboxBridge","onMessage","event","MessageEvent","dispose","SandboxContent","html","SandboxHostProps","content","contentKey","maxHeight","createBridge","frame","host","onError","error","Error","containerProps","Record","isSandboxFrameMessage","iframe","HTMLIFrameElement","origin","source","contentWindow","LiveSnapshot","SandboxHost","t0","$","t1","undefined","containerRef","contentHeight","setContentHeight","t2","liveRef","t3","t4","current","t5","Symbol","for","container","cancelled","bridge","liveContent","sb","reportError","err","String","bind","scf","renderOpts","renderHtml","then","rendered","hostApi","Number","isFinite","sendMessage","window","addEventListener","fullyLoadedPromiseWithTimeout","catch","err_0","code","err_1","removeEventListener","t6","resolvedHeight","Math","min","t7","mergedStyle","t8","t9"],"sources":["../../src/sandbox-host/SandboxHost.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type CSSProperties,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport {\n isShimLoadError,\n type RenderedFrame,\n SafeContentFrame,\n type SandboxOption,\n} from \"safe-content-frame\";\nimport { invokeUserCallback } from \"@assistant-ui/core/internal\";\n\nconst DEFAULT_PRODUCT = \"assistant-ui-sandbox\";\nconst DEFAULT_MAX_HEIGHT = 800;\nconst LOAD_TIMEOUT_MS = 10_000;\n\nexport type SandboxHostConfig = {\n sandbox?: SandboxOption[];\n useShadowDom?: boolean;\n enableBrowserCaching?: boolean;\n salt?: string;\n product?: string;\n className?: string;\n style?: CSSProperties;\n unsafeDocumentWrite?: boolean;\n};\n\nexport type SandboxHostFrame = Pick<\n RenderedFrame,\n \"iframe\" | \"origin\" | \"sendMessage\"\n>;\n\nexport type SandboxHostApi = {\n setHeight: (height: number) => void;\n};\n\nexport type SandboxBridge = {\n onMessage: (event: MessageEvent) => void;\n dispose: () => void;\n};\n\nexport type SandboxContent = { html: string };\n\nexport type SandboxHostProps = {\n content: SandboxContent;\n contentKey: string;\n sandbox?: SandboxHostConfig | undefined;\n maxHeight?: number | undefined;\n createBridge: (\n frame: SandboxHostFrame,\n host: SandboxHostApi,\n ) => SandboxBridge;\n onError?: ((error: Error) => void) | undefined;\n containerProps?: Record<string, string | undefined> | undefined;\n};\n\nexport function isSandboxFrameMessage(\n event: MessageEvent,\n frame: { iframe: HTMLIFrameElement; origin: string },\n): boolean {\n return (\n event.source === frame.iframe.contentWindow && event.origin === frame.origin\n );\n}\n\ntype LiveSnapshot = {\n content: SandboxContent;\n sandbox: SandboxHostConfig | undefined;\n createBridge: SandboxHostProps[\"createBridge\"];\n onError: SandboxHostProps[\"onError\"];\n};\n\nexport function SandboxHost({\n content,\n contentKey,\n sandbox,\n maxHeight = DEFAULT_MAX_HEIGHT,\n createBridge,\n onError,\n containerProps,\n}: SandboxHostProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [contentHeight, setContentHeight] = useState<number | undefined>(\n undefined,\n );\n\n const liveRef = useRef<LiveSnapshot>({\n content,\n sandbox,\n createBridge,\n onError,\n });\n useLayoutEffect(() => {\n liveRef.current = { content, sandbox, createBridge, onError };\n }, [content, sandbox, createBridge, onError]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n let cancelled = false;\n let frame: RenderedFrame | null = null;\n let bridge: SandboxBridge | null = null;\n let onMessage: ((event: MessageEvent) => void) | null = null;\n\n const { content: liveContent, sandbox: sb } = liveRef.current;\n\n const reportError = (err: unknown) => {\n const error = err instanceof Error ? err : new Error(String(err));\n invokeUserCallback(\n \"assistant-ui\",\n \"SandboxHost onError\",\n liveRef.current.onError?.bind(liveRef.current),\n error,\n );\n };\n\n const scf = new SafeContentFrame(sb?.product ?? DEFAULT_PRODUCT, {\n ...(sb?.sandbox !== undefined && { sandbox: sb.sandbox }),\n ...(sb?.useShadowDom !== undefined && { useShadowDom: sb.useShadowDom }),\n ...(sb?.enableBrowserCaching !== undefined && {\n enableBrowserCaching: sb.enableBrowserCaching,\n }),\n ...(sb?.salt !== undefined && { salt: sb.salt }),\n });\n\n const renderOpts =\n sb?.unsafeDocumentWrite !== undefined\n ? { unsafeDocumentWrite: sb.unsafeDocumentWrite }\n : undefined;\n\n scf\n .renderHtml(liveContent.html, container, renderOpts)\n .then((rendered) => {\n if (cancelled) {\n rendered.dispose();\n return;\n }\n frame = rendered;\n\n const hostApi: SandboxHostApi = {\n setHeight: (height) => {\n if (\n typeof height === \"number\" &&\n Number.isFinite(height) &&\n height > 0\n ) {\n setContentHeight(height);\n }\n },\n };\n\n bridge = liveRef.current.createBridge(\n {\n iframe: rendered.iframe,\n origin: rendered.origin,\n sendMessage: rendered.sendMessage,\n },\n hostApi,\n );\n\n // Single owner of the window listener; the cross-origin guard runs\n // here so the bridge only sees frame-validated messages.\n onMessage = (event) => {\n if (!isSandboxFrameMessage(event, rendered)) return;\n bridge?.onMessage(event);\n };\n window.addEventListener(\"message\", onMessage);\n\n // renderHtml resolves at iframe load, which a shim that was never\n // served also reaches, so a completed render is only known once the\n // frame says so. render-timeout is excluded because the shim is\n // running and the guest may still paint, and reporting it would leave\n // a host that renders an error state showing one over a frame that\n // recovers.\n rendered.fullyLoadedPromiseWithTimeout(LOAD_TIMEOUT_MS).catch((err) => {\n if (cancelled) return;\n if (isShimLoadError(err) && err.code === \"render-timeout\") return;\n reportError(err);\n });\n })\n .catch((err) => {\n if (cancelled) return;\n frame?.dispose();\n frame = null;\n reportError(err);\n });\n\n return () => {\n cancelled = true;\n if (onMessage) {\n window.removeEventListener(\"message\", onMessage);\n onMessage = null;\n }\n bridge?.dispose();\n bridge = null;\n frame?.dispose();\n frame = null;\n setContentHeight(undefined);\n };\n // oxlint-disable-next-line react/exhaustive-deps -- re-init only on contentKey change; live values flow through liveRef\n }, [contentKey]);\n\n const resolvedHeight =\n contentHeight != null ? Math.min(contentHeight, maxHeight) : undefined;\n const mergedStyle =\n resolvedHeight != null\n ? { ...sandbox?.style, height: resolvedHeight }\n : sandbox?.style;\n\n return (\n <div\n {...containerProps}\n ref={containerRef}\n className={sandbox?.className}\n style={mergedStyle}\n />\n );\n}\n"],"mappings":"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"}
@@ -21,7 +21,10 @@ type Unstable_MentionCategory = {
21
21
  readonly items: readonly Unstable_Mention[];
22
22
  };
23
23
  type Unstable_ModelContextToolsOptions = {
24
- /** Wrap tools in a dedicated category (drill-down mode). */
24
+ /**
25
+ * Wrap tools in a dedicated category. Selects drill-down mode on its own
26
+ * when `categories` is unset and `items` is omitted.
27
+ */
25
28
  readonly category?: {
26
29
  readonly id: string;
27
30
  readonly label: string;
@@ -32,7 +35,10 @@ type Unstable_ModelContextToolsOptions = {
32
35
  readonly icon?: string;
33
36
  };
34
37
  type Unstable_UseMentionAdapterOptions = {
35
- /** Flat mention list. Ignored when `categories` is set. */
38
+ /**
39
+ * Flat mention list. Ignored when `categories` is set, and keeps the
40
+ * adapter flat when a tool `category` is configured.
41
+ */
36
42
  readonly items?: readonly Unstable_Mention[];
37
43
  /** Categorized mentions for drill-down navigation. */
38
44
  readonly categories?: readonly Unstable_MentionCategory[];
@@ -41,7 +47,7 @@ type Unstable_UseMentionAdapterOptions = {
41
47
  * - `false`: exclude.
42
48
  * - `true`: include (default when no `items`/`categories`; as a category
43
49
  * if `categories` is set, flat otherwise).
44
- * - object: explicit config.
50
+ * - object: explicit config; `category` also selects drill-down mode.
45
51
  *
46
52
  * Omitted → defaults to `true` iff neither `items` nor `categories`.
47
53
  */
@@ -1 +1 @@
1
- {"version":3,"file":"useMentionAdapter.d.ts","names":[],"sources":["../../src/unstable/useMentionAdapter.ts"],"mappings":";;;;;KAcY,yBAAyB;EAAK;;KAE9B;WACD;WACA;WACA;WACA;;WAEA;WACA,WAAW;;KAGV;WACD;WACA;WACA,gBAAgB;;KAGf;;WAED;aAAsB;aAAqB;;;WAE3C,eAAe;;WAEf;;KAGC;;WAED,iBAAiB;;WAEjB,sBAAsB;;;;;;;;;;WAUtB,qCAEL;;WAEK,YAAY;;WAEZ,cAAc,MAAM;;WAEpB,UAAU,eAAe;;WAEzB,eAAe;;KAGd;WACD,WAAW;WACX,eAAe,MAAM;;;;;;;;;;;;;;;iBAgBhB,2BACd,UAAU;EAEV,SAAS;EACT,WAAW;EACX,UAAU,eAAe;EACzB,eAAe"}
1
+ {"version":3,"file":"useMentionAdapter.d.ts","names":[],"sources":["../../src/unstable/useMentionAdapter.ts"],"mappings":";;;;;KAoBY,yBAAyB;EAAK;;KAE9B;WACD;WACA;WACA;WACA;;WAEA;WACA,WAAW;;KAGV;WACD;WACA;WACA,gBAAgB;;KAGf;;;;;WAKD;aAAsB;aAAqB;;;WAE3C,eAAe;;WAEf;;KAGC;;;;;WAKD,iBAAiB;;WAEjB,sBAAsB;;;;;;;;;;WAUtB,qCAEL;;WAEK,YAAY;;WAEZ,cAAc,MAAM;;WAEpB,UAAU,eAAe;;WAEzB,eAAe;;KAGd;WACD,WAAW;WACX,eAAe,MAAM;;;;;;;;;;;;;;;iBAoDhB,2BACd,UAAU;EAEV,SAAS;EACT,WAAW;EACX,UAAU,eAAe;EACzB,eAAe"}
@@ -1,8 +1,33 @@
1
1
  "use client";
2
+ import { matchesTriggerItemQuery } from "../primitives/composer/trigger/matchesTriggerItemQuery.js";
3
+ import { shallowEqualRecords, useModelContextSnapshot } from "./useModelContextSnapshot.js";
2
4
  import { useAui } from "@assistant-ui/store";
3
5
  import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
4
6
  import { useMemo } from "@assistant-ui/tap/react-shim";
5
7
  //#region src/unstable/useMentionAdapter.ts
8
+ const EMPTY_TOOL_MENTIONS = Object.freeze({});
9
+ const toolMentionSource = {
10
+ empty: EMPTY_TOOL_MENTIONS,
11
+ read: (aui) => {
12
+ const tools = aui.thread.getModelContext().tools;
13
+ if (!tools) return EMPTY_TOOL_MENTIONS;
14
+ const mentions = {};
15
+ for (const [name, tool] of Object.entries(tools)) mentions[name] = tool.description;
16
+ return mentions;
17
+ },
18
+ subscribe: (aui, onChange) => {
19
+ const unsubscribeContext = aui.on("thread.modelContextUpdate", onChange);
20
+ const unsubscribeSelection = aui.on({
21
+ scope: "*",
22
+ event: "threads.selectionChanged"
23
+ }, onChange);
24
+ return () => {
25
+ unsubscribeContext();
26
+ unsubscribeSelection();
27
+ };
28
+ },
29
+ isEqual: shallowEqualRecords
30
+ };
6
31
  /**
7
32
  * @deprecated Under active development and might change without notice.
8
33
  *
@@ -25,38 +50,30 @@ function unstable_useMentionAdapter(options) {
25
50
  const wantsTools = includeTools !== false;
26
51
  const formatter = options?.formatter;
27
52
  const onInserted = options?.onInserted;
53
+ const isCategorized = categories !== void 0 && categories.length > 0 || toolsConfig?.category !== void 0 && items === void 0;
54
+ const toolMentions = useModelContextSnapshot(aui, wantsTools, toolMentionSource);
28
55
  return {
29
56
  adapter: useMemo(() => {
30
- const getModelContextTools = () => {
31
- if (!wantsTools) return [];
32
- const tools = aui.thread.getModelContext().tools;
33
- if (!tools) return [];
34
- const formatLabel = toolsConfig?.formatLabel;
35
- const defaultIcon = toolsConfig?.icon;
36
- return Object.entries(tools).map(([name, tool]) => toTriggerItem({
37
- id: name,
38
- type: "tool",
39
- label: formatLabel ? formatLabel(name) : name,
40
- description: tool.description ?? void 0,
41
- icon: defaultIcon
42
- }));
43
- };
44
- if (categories && categories.length > 0) {
45
- const groups = categories.map((cat) => ({
57
+ const formatLabel = toolsConfig?.formatLabel;
58
+ const defaultIcon = toolsConfig?.icon;
59
+ const toolItems = wantsTools ? Object.entries(toolMentions).map(([name, description]) => toTriggerItem({
60
+ id: name,
61
+ type: "tool",
62
+ label: formatLabel ? formatLabel(name) : name,
63
+ description,
64
+ icon: defaultIcon
65
+ })) : [];
66
+ if (isCategorized) {
67
+ const groups = (categories ?? []).map((cat) => ({
46
68
  id: cat.id,
47
69
  label: cat.label,
48
70
  items: cat.items.map(toTriggerItem)
49
71
  }));
50
- let toolCategory = null;
51
- if (wantsTools) {
52
- const toolItems = getModelContextTools();
53
- if (toolItems.length > 0) toolCategory = {
54
- id: toolsConfig?.category?.id ?? "tools",
55
- label: toolsConfig?.category?.label ?? "Tools",
56
- items: toolItems
57
- };
58
- }
59
- const allGroups = toolCategory ? [...groups, toolCategory] : groups;
72
+ const allGroups = toolItems.length > 0 ? [...groups, {
73
+ id: toolsConfig?.category?.id ?? "tools",
74
+ label: toolsConfig?.category?.label ?? "Tools",
75
+ items: toolItems
76
+ }] : groups;
60
77
  return {
61
78
  categories: () => allGroups.map(({ id, label }) => ({
62
79
  id,
@@ -65,31 +82,28 @@ function unstable_useMentionAdapter(options) {
65
82
  categoryItems: (id) => allGroups.find((g) => g.id === id)?.items ?? [],
66
83
  search: (query) => {
67
84
  const lower = query.toLowerCase();
68
- return allGroups.flatMap((g) => g.items).filter((item) => matchesQuery(item, lower));
85
+ return allGroups.flatMap((g) => g.items).filter((item) => matchesTriggerItemQuery(item, lower));
69
86
  }
70
87
  };
71
88
  }
72
89
  const flatItems = (items ?? []).map(toTriggerItem);
73
- const getFlatPool = () => {
74
- if (!wantsTools) return flatItems;
75
- const toolItems = getModelContextTools();
76
- const seen = new Set(flatItems.map((i) => i.id));
77
- return [...flatItems, ...toolItems.filter((t) => !seen.has(t.id))];
78
- };
90
+ const seen = new Set(flatItems.map((i) => i.id));
91
+ const flatPool = [...flatItems, ...toolItems.filter((t) => !seen.has(t.id))];
79
92
  return {
80
93
  categories: () => [],
81
94
  categoryItems: () => [],
82
95
  search: (query) => {
83
96
  const lower = query.toLowerCase();
84
- return getFlatPool().filter((item) => matchesQuery(item, lower));
97
+ return flatPool.filter((item) => matchesTriggerItemQuery(item, lower));
85
98
  }
86
99
  };
87
100
  }, [
88
- aui,
89
101
  items,
90
102
  categories,
91
103
  wantsTools,
92
- toolsConfig
104
+ toolsConfig,
105
+ isCategorized,
106
+ toolMentions
93
107
  ]),
94
108
  directive: useMemo(() => ({
95
109
  formatter: formatter ?? unstable_defaultDirectiveFormatter,
@@ -112,13 +126,6 @@ function toTriggerItem(m) {
112
126
  ...metadata !== void 0 ? { metadata } : {}
113
127
  };
114
128
  }
115
- function matchesQuery(item, lower) {
116
- if (!lower) return true;
117
- if (item.id.toLowerCase().includes(lower)) return true;
118
- if (item.label.toLowerCase().includes(lower)) return true;
119
- if (item.description?.toLowerCase().includes(lower)) return true;
120
- return false;
121
- }
122
129
  //#endregion
123
130
  export { unstable_useMentionAdapter };
124
131
 
@@ -1 +1 @@
1
- {"version":3,"file":"useMentionAdapter.js","names":["useMemo","FC","useAui","Unstable_DirectiveFormatter","Unstable_TriggerAdapter","Unstable_TriggerCategory","Unstable_TriggerItem","unstable_defaultDirectiveFormatter","ReadonlyJSONObject","Unstable_IconComponent","className","Unstable_Mention","id","type","label","description","icon","metadata","Unstable_MentionCategory","items","Unstable_ModelContextToolsOptions","category","formatLabel","toolName","Unstable_UseMentionAdapterOptions","categories","includeModelContextTools","formatter","onInserted","item","iconMap","Record","fallbackIcon","Unstable_MentionDirective","unstable_useMentionAdapter","options","adapter","directive","aui","includeTools","toolsConfig","undefined","wantsTools","getModelContextTools","ctx","thread","getModelContext","tools","defaultIcon","Object","entries","map","name","tool","toTriggerItem","length","groups","cat","toolCategory","toolItems","allGroups","categoryItems","find","g","search","query","lower","toLowerCase","flatMap","filter","matchesQuery","flatItems","getFlatPool","seen","Set","i","t","has","m","includes"],"sources":["../../src/unstable/useMentionAdapter.ts"],"sourcesContent":["\"use client\";\n\nimport { useMemo, type FC } from \"react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport type {\n Unstable_DirectiveFormatter,\n Unstable_TriggerAdapter,\n Unstable_TriggerCategory,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\nimport { unstable_defaultDirectiveFormatter } from \"@assistant-ui/core\";\nimport type { ReadonlyJSONObject } from \"assistant-stream/utils\";\n\n/** Icon component shape consumed by `ComposerTriggerPopover`'s `iconMap`. */\nexport type Unstable_IconComponent = FC<{ className?: string }>;\n\nexport type Unstable_Mention = {\n readonly id: string;\n readonly type: string;\n readonly label: string;\n readonly description?: string | undefined;\n /** Shortcut for `metadata.icon`; merged with `metadata` if both are given. */\n readonly icon?: string | undefined;\n readonly metadata?: ReadonlyJSONObject | undefined;\n};\n\nexport type Unstable_MentionCategory = {\n readonly id: string;\n readonly label: string;\n readonly items: readonly Unstable_Mention[];\n};\n\nexport type Unstable_ModelContextToolsOptions = {\n /** Wrap tools in a dedicated category (drill-down mode). */\n readonly category?: { readonly id: string; readonly label: string };\n /** Format tool name for display. */\n readonly formatLabel?: (toolName: string) => string;\n /** Default icon key for each tool. */\n readonly icon?: string;\n};\n\nexport type Unstable_UseMentionAdapterOptions = {\n /** Flat mention list. Ignored when `categories` is set. */\n readonly items?: readonly Unstable_Mention[];\n /** Categorized mentions for drill-down navigation. */\n readonly categories?: readonly Unstable_MentionCategory[];\n /**\n * How tools registered in model context integrate.\n * - `false`: exclude.\n * - `true`: include (default when no `items`/`categories`; as a category\n * if `categories` is set, flat otherwise).\n * - object: explicit config.\n *\n * Omitted → defaults to `true` iff neither `items` nor `categories`.\n */\n readonly includeModelContextTools?:\n | boolean\n | Unstable_ModelContextToolsOptions;\n /** Directive formatter. @default unstable_defaultDirectiveFormatter */\n readonly formatter?: Unstable_DirectiveFormatter;\n /** Fires after an item is inserted into the composer. */\n readonly onInserted?: (item: Unstable_TriggerItem) => void;\n /** Maps `metadata.icon` / `category.id` string keys to React components. */\n readonly iconMap?: Record<string, Unstable_IconComponent>;\n /** Fallback icon when no entry in `iconMap` matches. */\n readonly fallbackIcon?: Unstable_IconComponent;\n};\n\nexport type Unstable_MentionDirective = {\n readonly formatter: Unstable_DirectiveFormatter;\n readonly onInserted?: ((item: Unstable_TriggerItem) => void) | undefined;\n};\n\n/**\n * @deprecated Under active development and might change without notice.\n *\n * Creates a spreadable `{ adapter, directive }` bundle for `@` mentions.\n * Supports tools registered in model context, explicit items, or both —\n * flat or categorized.\n *\n * @example\n * ```tsx\n * const mention = unstable_useMentionAdapter();\n * <ComposerTriggerPopover char=\"@\" {...mention} />\n * ```\n */\nexport function unstable_useMentionAdapter(\n options?: Unstable_UseMentionAdapterOptions,\n): {\n adapter: Unstable_TriggerAdapter;\n directive: Unstable_MentionDirective;\n iconMap?: Record<string, Unstable_IconComponent>;\n fallbackIcon?: Unstable_IconComponent;\n} {\n const aui = useAui();\n\n const items = options?.items;\n const categories = options?.categories;\n const includeTools =\n options?.includeModelContextTools ?? (!items && !categories);\n const toolsConfig =\n typeof includeTools === \"object\" ? includeTools : undefined;\n const wantsTools = includeTools !== false;\n const formatter = options?.formatter;\n const onInserted = options?.onInserted;\n\n const adapter = useMemo<Unstable_TriggerAdapter>(() => {\n const getModelContextTools = (): Unstable_TriggerItem[] => {\n if (!wantsTools) return [];\n const ctx = aui.thread.getModelContext();\n const tools = ctx.tools;\n if (!tools) return [];\n const formatLabel = toolsConfig?.formatLabel;\n const defaultIcon = toolsConfig?.icon;\n return Object.entries(tools).map(([name, tool]) =>\n toTriggerItem({\n id: name,\n type: \"tool\",\n label: formatLabel ? formatLabel(name) : name,\n description: tool.description ?? undefined,\n icon: defaultIcon,\n }),\n );\n };\n\n // Categorized: drill-down mode\n if (categories && categories.length > 0) {\n const groups = categories.map((cat) => ({\n id: cat.id,\n label: cat.label,\n items: cat.items.map(toTriggerItem),\n }));\n\n let toolCategory: {\n id: string;\n label: string;\n items: Unstable_TriggerItem[];\n } | null = null;\n if (wantsTools) {\n const toolItems = getModelContextTools();\n if (toolItems.length > 0) {\n toolCategory = {\n id: toolsConfig?.category?.id ?? \"tools\",\n label: toolsConfig?.category?.label ?? \"Tools\",\n items: toolItems,\n };\n }\n }\n const allGroups = toolCategory ? [...groups, toolCategory] : groups;\n\n return {\n categories: () => allGroups.map(({ id, label }) => ({ id, label })),\n categoryItems: (id) => allGroups.find((g) => g.id === id)?.items ?? [],\n search: (query) => {\n const lower = query.toLowerCase();\n return allGroups\n .flatMap((g) => g.items)\n .filter((item) => matchesQuery(item, lower));\n },\n };\n }\n\n // Flat: items + (optionally) tools, all in one search pool\n const flatItems = (items ?? []).map(toTriggerItem);\n const getFlatPool = (): Unstable_TriggerItem[] => {\n if (!wantsTools) return flatItems;\n const toolItems = getModelContextTools();\n // Dedupe by id — explicit items win.\n const seen = new Set(flatItems.map((i) => i.id));\n return [...flatItems, ...toolItems.filter((t) => !seen.has(t.id))];\n };\n\n return {\n categories: (): readonly Unstable_TriggerCategory[] => [],\n categoryItems: () => [],\n search: (query) => {\n const lower = query.toLowerCase();\n return getFlatPool().filter((item) => matchesQuery(item, lower));\n },\n };\n }, [aui, items, categories, wantsTools, toolsConfig]);\n\n const directive = useMemo<Unstable_MentionDirective>(\n () => ({\n formatter: formatter ?? unstable_defaultDirectiveFormatter,\n ...(onInserted ? { onInserted } : {}),\n }),\n [formatter, onInserted],\n );\n\n return {\n adapter,\n directive,\n ...(options?.iconMap ? { iconMap: options.iconMap } : {}),\n ...(options?.fallbackIcon ? { fallbackIcon: options.fallbackIcon } : {}),\n };\n}\n\nfunction toTriggerItem(m: Unstable_Mention): Unstable_TriggerItem {\n const metadata =\n m.icon !== undefined ? { ...(m.metadata ?? {}), icon: m.icon } : m.metadata;\n return {\n id: m.id,\n type: m.type,\n label: m.label,\n ...(m.description !== undefined ? { description: m.description } : {}),\n ...(metadata !== undefined ? { metadata } : {}),\n };\n}\n\nfunction matchesQuery(item: Unstable_TriggerItem, lower: string): boolean {\n if (!lower) return true;\n if (item.id.toLowerCase().includes(lower)) return true;\n if (item.label.toLowerCase().includes(lower)) return true;\n if (item.description?.toLowerCase().includes(lower)) return true;\n return false;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useMentionAdapter.js","names":["useMemo","FC","useAui","Unstable_DirectiveFormatter","Unstable_TriggerAdapter","Unstable_TriggerCategory","Unstable_TriggerItem","unstable_defaultDirectiveFormatter","ReadonlyJSONObject","matchesTriggerItemQuery","shallowEqualRecords","useModelContextSnapshot","ModelContextSnapshotSource","Unstable_IconComponent","className","Unstable_Mention","id","type","label","description","icon","metadata","Unstable_MentionCategory","items","Unstable_ModelContextToolsOptions","category","formatLabel","toolName","Unstable_UseMentionAdapterOptions","categories","includeModelContextTools","formatter","onInserted","item","iconMap","Record","fallbackIcon","Unstable_MentionDirective","EMPTY_TOOL_MENTIONS","Readonly","Object","freeze","toolMentionSource","empty","read","aui","tools","thread","getModelContext","mentions","name","tool","entries","subscribe","onChange","unsubscribeContext","on","unsubscribeSelection","scope","event","isEqual","unstable_useMentionAdapter","options","adapter","directive","includeTools","toolsConfig","undefined","wantsTools","isCategorized","length","toolMentions","defaultIcon","toolItems","map","toTriggerItem","groups","cat","allGroups","categoryItems","find","g","search","query","lower","toLowerCase","flatMap","filter","flatItems","seen","Set","i","flatPool","t","has","m"],"sources":["../../src/unstable/useMentionAdapter.ts"],"sourcesContent":["\"use client\";\n\nimport { useMemo, type FC } from \"react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport type {\n Unstable_DirectiveFormatter,\n Unstable_TriggerAdapter,\n Unstable_TriggerCategory,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\nimport { unstable_defaultDirectiveFormatter } from \"@assistant-ui/core\";\nimport type { ReadonlyJSONObject } from \"assistant-stream/utils\";\nimport { matchesTriggerItemQuery } from \"../primitives/composer/trigger/matchesTriggerItemQuery\";\nimport {\n shallowEqualRecords,\n useModelContextSnapshot,\n type ModelContextSnapshotSource,\n} from \"./useModelContextSnapshot\";\n\n/** Icon component shape consumed by `ComposerTriggerPopover`'s `iconMap`. */\nexport type Unstable_IconComponent = FC<{ className?: string }>;\n\nexport type Unstable_Mention = {\n readonly id: string;\n readonly type: string;\n readonly label: string;\n readonly description?: string | undefined;\n /** Shortcut for `metadata.icon`; merged with `metadata` if both are given. */\n readonly icon?: string | undefined;\n readonly metadata?: ReadonlyJSONObject | undefined;\n};\n\nexport type Unstable_MentionCategory = {\n readonly id: string;\n readonly label: string;\n readonly items: readonly Unstable_Mention[];\n};\n\nexport type Unstable_ModelContextToolsOptions = {\n /**\n * Wrap tools in a dedicated category. Selects drill-down mode on its own\n * when `categories` is unset and `items` is omitted.\n */\n readonly category?: { readonly id: string; readonly label: string };\n /** Format tool name for display. */\n readonly formatLabel?: (toolName: string) => string;\n /** Default icon key for each tool. */\n readonly icon?: string;\n};\n\nexport type Unstable_UseMentionAdapterOptions = {\n /**\n * Flat mention list. Ignored when `categories` is set, and keeps the\n * adapter flat when a tool `category` is configured.\n */\n readonly items?: readonly Unstable_Mention[];\n /** Categorized mentions for drill-down navigation. */\n readonly categories?: readonly Unstable_MentionCategory[];\n /**\n * How tools registered in model context integrate.\n * - `false`: exclude.\n * - `true`: include (default when no `items`/`categories`; as a category\n * if `categories` is set, flat otherwise).\n * - object: explicit config; `category` also selects drill-down mode.\n *\n * Omitted → defaults to `true` iff neither `items` nor `categories`.\n */\n readonly includeModelContextTools?:\n | boolean\n | Unstable_ModelContextToolsOptions;\n /** Directive formatter. @default unstable_defaultDirectiveFormatter */\n readonly formatter?: Unstable_DirectiveFormatter;\n /** Fires after an item is inserted into the composer. */\n readonly onInserted?: (item: Unstable_TriggerItem) => void;\n /** Maps `metadata.icon` / `category.id` string keys to React components. */\n readonly iconMap?: Record<string, Unstable_IconComponent>;\n /** Fallback icon when no entry in `iconMap` matches. */\n readonly fallbackIcon?: Unstable_IconComponent;\n};\n\nexport type Unstable_MentionDirective = {\n readonly formatter: Unstable_DirectiveFormatter;\n readonly onInserted?: ((item: Unstable_TriggerItem) => void) | undefined;\n};\n\nconst EMPTY_TOOL_MENTIONS: Readonly<Record<string, string | undefined>> =\n Object.freeze({});\n\n// The description is copied out rather than reached through the tool, so a\n// provider that returns a stable tool object and edits it in place is observed.\nconst toolMentionSource: ModelContextSnapshotSource<\n Readonly<Record<string, string | undefined>>\n> = {\n empty: EMPTY_TOOL_MENTIONS,\n read: (aui) => {\n const tools = aui.thread.getModelContext().tools;\n if (!tools) return EMPTY_TOOL_MENTIONS;\n const mentions: Record<string, string | undefined> = {};\n for (const [name, tool] of Object.entries(tools)) {\n mentions[name] = tool.description;\n }\n return mentions;\n },\n subscribe: (aui, onChange) => {\n const unsubscribeContext = aui.on(\"thread.modelContextUpdate\", onChange);\n // Rebinding the thread event subject to a new thread does not replay it,\n // so a switch between threads carrying different providers is its own\n // refresh trigger. Subscribed globally because a composer can render\n // without a thread list scope.\n const unsubscribeSelection = aui.on(\n { scope: \"*\", event: \"threads.selectionChanged\" },\n onChange,\n );\n return () => {\n unsubscribeContext();\n unsubscribeSelection();\n };\n },\n isEqual: shallowEqualRecords,\n};\n\n/**\n * @deprecated Under active development and might change without notice.\n *\n * Creates a spreadable `{ adapter, directive }` bundle for `@` mentions.\n * Supports tools registered in model context, explicit items, or both —\n * flat or categorized.\n *\n * @example\n * ```tsx\n * const mention = unstable_useMentionAdapter();\n * <ComposerTriggerPopover char=\"@\" {...mention} />\n * ```\n */\nexport function unstable_useMentionAdapter(\n options?: Unstable_UseMentionAdapterOptions,\n): {\n adapter: Unstable_TriggerAdapter;\n directive: Unstable_MentionDirective;\n iconMap?: Record<string, Unstable_IconComponent>;\n fallbackIcon?: Unstable_IconComponent;\n} {\n const aui = useAui();\n\n const items = options?.items;\n const categories = options?.categories;\n const includeTools =\n options?.includeModelContextTools ?? (!items && !categories);\n const toolsConfig =\n typeof includeTools === \"object\" ? includeTools : undefined;\n const wantsTools = includeTools !== false;\n const formatter = options?.formatter;\n const onInserted = options?.onInserted;\n const isCategorized =\n (categories !== undefined && categories.length > 0) ||\n (toolsConfig?.category !== undefined && items === undefined);\n const toolMentions = useModelContextSnapshot(\n aui,\n wantsTools,\n toolMentionSource,\n );\n\n const adapter = useMemo<Unstable_TriggerAdapter>(() => {\n const formatLabel = toolsConfig?.formatLabel;\n const defaultIcon = toolsConfig?.icon;\n const toolItems = wantsTools\n ? Object.entries(toolMentions).map(([name, description]) =>\n toTriggerItem({\n id: name,\n type: \"tool\",\n label: formatLabel ? formatLabel(name) : name,\n description,\n icon: defaultIcon,\n }),\n )\n : [];\n\n // Categorized: drill-down mode\n if (isCategorized) {\n const groups = (categories ?? []).map((cat) => ({\n id: cat.id,\n label: cat.label,\n items: cat.items.map(toTriggerItem),\n }));\n const allGroups =\n toolItems.length > 0\n ? [\n ...groups,\n {\n id: toolsConfig?.category?.id ?? \"tools\",\n label: toolsConfig?.category?.label ?? \"Tools\",\n items: toolItems,\n },\n ]\n : groups;\n\n return {\n categories: () => allGroups.map(({ id, label }) => ({ id, label })),\n categoryItems: (id) => allGroups.find((g) => g.id === id)?.items ?? [],\n search: (query) => {\n const lower = query.toLowerCase();\n return allGroups\n .flatMap((g) => g.items)\n .filter((item) => matchesTriggerItemQuery(item, lower));\n },\n };\n }\n\n // Flat: items + (optionally) tools, all in one search pool\n const flatItems = (items ?? []).map(toTriggerItem);\n // Dedupe by id — explicit items win.\n const seen = new Set(flatItems.map((i) => i.id));\n const flatPool = [\n ...flatItems,\n ...toolItems.filter((t) => !seen.has(t.id)),\n ];\n\n return {\n categories: (): readonly Unstable_TriggerCategory[] => [],\n categoryItems: () => [],\n search: (query) => {\n const lower = query.toLowerCase();\n return flatPool.filter((item) => matchesTriggerItemQuery(item, lower));\n },\n };\n }, [items, categories, wantsTools, toolsConfig, isCategorized, toolMentions]);\n\n const directive = useMemo<Unstable_MentionDirective>(\n () => ({\n formatter: formatter ?? unstable_defaultDirectiveFormatter,\n ...(onInserted ? { onInserted } : {}),\n }),\n [formatter, onInserted],\n );\n\n return {\n adapter,\n directive,\n ...(options?.iconMap ? { iconMap: options.iconMap } : {}),\n ...(options?.fallbackIcon ? { fallbackIcon: options.fallbackIcon } : {}),\n };\n}\n\nfunction toTriggerItem(m: Unstable_Mention): Unstable_TriggerItem {\n const metadata =\n m.icon !== undefined ? { ...(m.metadata ?? {}), icon: m.icon } : m.metadata;\n return {\n id: m.id,\n type: m.type,\n label: m.label,\n ...(m.description !== undefined ? { description: m.description } : {}),\n ...(metadata !== undefined ? { metadata } : {}),\n };\n}\n"],"mappings":";;;;;;;AAqFA,MAAMsC,sBACJE,OAAOC,OAAO,CAAC,CAAC;AAIlB,MAAMC,oBAEF;CACFC,OAAOL;CACPM,OAAOC,QAAQ;EACb,MAAMC,QAAQD,IAAIE,OAAOC,gBAAgB,CAAC,CAACF;EAC3C,IAAI,CAACA,OAAO,OAAOR;EACnB,MAAMW,WAA+C,CAAC;EACtD,KAAK,MAAM,CAACC,MAAMC,SAASX,OAAOY,QAAQN,KAAK,GAC7CG,SAASC,QAAQC,KAAKhC;EAExB,OAAO8B;CACT;CACAI,YAAYR,KAAKS,aAAa;EAC5B,MAAMC,qBAAqBV,IAAIW,GAAG,6BAA6BF,QAAQ;EAKvE,MAAMG,uBAAuBZ,IAAIW,GAC/B;GAAEE,OAAO;GAAKC,OAAO;EAA2B,GAChDL,QACF;EACA,aAAa;GACXC,mBAAmB;GACnBE,qBAAqB;EACvB;CACF;CACAG,SAASlD;AACX;;;;;;;;;;;;;;AAeA,SAAgBmD,2BACdC,SAMA;CACA,MAAMjB,MAAM3C,OAAO;CAEnB,MAAMqB,QAAQuC,SAASvC;CACvB,MAAMM,aAAaiC,SAASjC;CAC5B,MAAMoC,eACJH,SAAShC,6BAA6B,CAACP,SAAS,CAACM;CACnD,MAAMqC,cACJ,OAAOD,iBAAiB,WAAWA,eAAeE,KAAAA;CACpD,MAAMC,aAAaH,iBAAiB;CACpC,MAAMlC,YAAY+B,SAAS/B;CAC3B,MAAMC,aAAa8B,SAAS9B;CAC5B,MAAMqC,gBACHxC,eAAesC,KAAAA,KAAatC,WAAWyC,SAAS,KAChDJ,aAAazC,aAAa0C,KAAAA,KAAa5C,UAAU4C,KAAAA;CACpD,MAAMI,eAAe5D,wBACnBkC,KACAuB,YACA1B,iBACF;CA2EA,OAAO;EACLqB,SA1Ec/D,cAAuC;GACrD,MAAM0B,cAAcwC,aAAaxC;GACjC,MAAM8C,cAAcN,aAAa9C;GACjC,MAAMqD,YAAYL,aACd5B,OAAOY,QAAQmB,YAAY,CAAC,CAACG,KAAK,CAACxB,MAAM/B,iBACvCwD,cAAc;IACZ3D,IAAIkC;IACJjC,MAAM;IACNC,OAAOQ,cAAcA,YAAYwB,IAAI,IAAIA;IACzC/B;IACAC,MAAMoD;GACR,CAAC,CACH,IACA,CAAA;GAGJ,IAAIH,eAAe;IACjB,MAAMO,UAAU/C,cAAc,CAAA,EAAA,CAAI6C,KAAKG,SAAS;KAC9C7D,IAAI6D,IAAI7D;KACRE,OAAO2D,IAAI3D;KACXK,OAAOsD,IAAItD,MAAMmD,IAAIC,aAAa;IACpC,EAAE;IACF,MAAMG,YACJL,UAAUH,SAAS,IACf,CACE,GAAGM,QACH;KACE5D,IAAIkD,aAAazC,UAAUT,MAAM;KACjCE,OAAOgD,aAAazC,UAAUP,SAAS;KACvCK,OAAOkD;IACT,CAAC,IAEHG;IAEN,OAAO;KACL/C,kBAAkBiD,UAAUJ,KAAK,EAAE1D,IAAIE,aAAa;MAAEF;MAAIE;KAAM,EAAE;KAClE6D,gBAAgB/D,OAAO8D,UAAUE,MAAMC,MAAMA,EAAEjE,OAAOA,EAAE,CAAC,EAAEO,SAAS,CAAA;KACpE2D,SAASC,UAAU;MACjB,MAAMC,QAAQD,MAAME,YAAY;MAChC,OAAOP,UACJQ,SAASL,MAAMA,EAAE1D,KAAK,CAAC,CACvBgE,QAAQtD,SAASxB,wBAAwBwB,MAAMmD,KAAK,CAAC;KAC1D;IACF;GACF;GAGA,MAAMI,aAAajE,SAAS,CAAA,EAAA,CAAImD,IAAIC,aAAa;GAEjD,MAAMc,OAAO,IAAIC,IAAIF,UAAUd,KAAKiB,MAAMA,EAAE3E,EAAE,CAAC;GAC/C,MAAM4E,WAAW,CACf,GAAGJ,WACH,GAAGf,UAAUc,QAAQM,MAAM,CAACJ,KAAKK,IAAID,EAAE7E,EAAE,CAAC,CAAC;GAG7C,OAAO;IACLa,kBAAuD,CAAA;IACvDkD,qBAAqB,CAAA;IACrBG,SAASC,UAAU;KACjB,MAAMC,QAAQD,MAAME,YAAY;KAChC,OAAOO,SAASL,QAAQtD,SAASxB,wBAAwBwB,MAAMmD,KAAK,CAAC;IACvE;GACF;EACF,GAAG;GAAC7D;GAAOM;GAAYuC;GAAYF;GAAaG;GAAeE;EAAY,CAWzER;EACAC,WAVgBhE,eACT;GACL+B,WAAWA,aAAaxB;GACxB,GAAIyB,aAAa,EAAEA,WAAW,IAAI,CAAC;EACrC,IACA,CAACD,WAAWC,UAAU,CAKtBgC;EACA,GAAIF,SAAS5B,UAAU,EAAEA,SAAS4B,QAAQ5B,QAAQ,IAAI,CAAC;EACvD,GAAI4B,SAAS1B,eAAe,EAAEA,cAAc0B,QAAQ1B,aAAa,IAAI,CAAC;CACxE;AACF;AAEA,SAASuC,cAAcoB,GAA2C;CAChE,MAAM1E,WACJ0E,EAAE3E,SAAS+C,KAAAA,IAAY;EAAE,GAAI4B,EAAE1E,YAAY,CAAC;EAAID,MAAM2E,EAAE3E;CAAK,IAAI2E,EAAE1E;CACrE,OAAO;EACLL,IAAI+E,EAAE/E;EACNC,MAAM8E,EAAE9E;EACRC,OAAO6E,EAAE7E;EACT,GAAI6E,EAAE5E,gBAAgBgD,KAAAA,IAAY,EAAEhD,aAAa4E,EAAE5E,YAAY,IAAI,CAAC;EACpE,GAAIE,aAAa8C,KAAAA,IAAY,EAAE9C,SAAS,IAAI,CAAC;CAC/C;AACF"}
@@ -0,0 +1,34 @@
1
+ import { AssistantClient } from "@assistant-ui/store";
2
+ import { Unsubscribe } from "@assistant-ui/core";
3
+ //#region src/unstable/useModelContextSnapshot.d.ts
4
+ /**
5
+ * Where a snapshot comes from and what counts as a change to it. Declared once
6
+ * per consumer at module scope: the subscription effect keys on this object, so
7
+ * an inline source without an `isEqual` re-reads itself into a render loop.
8
+ */
9
+ type ModelContextSnapshotSource<T> = {
10
+ /** Seeds the snapshot when it starts disabled. */
11
+ readonly empty: T;
12
+ /** Projects the fields the consumer observes out of the live model context. */
13
+ read(aui: AssistantClient): T;
14
+ subscribe(aui: AssistantClient, onChange: () => void): Unsubscribe;
15
+ /**
16
+ * An equal pair keeps the previous reference. Omitted by a consumer that
17
+ * re-derives on every notification, where a caller republishing an unchanged
18
+ * tool set is how it asks to be re-run.
19
+ */
20
+ isEqual?(previous: T, next: T): boolean;
21
+ };
22
+ /**
23
+ * `getModelContext()` rebuilds its result on every call, so a projection of the
24
+ * fields a consumer observes is held in state and refreshed on notification
25
+ * rather than read during render. Keeping the previous reference for an equal
26
+ * projection is what stops a notification carrying no observable change from
27
+ * invalidating everything derived from it.
28
+ */
29
+ declare const useModelContextSnapshot: <T>(aui: AssistantClient, enabled: boolean, source: ModelContextSnapshotSource<T>) => T;
30
+ /** Shallow own-key comparison, for a projection whose values are records. */
31
+ declare const shallowEqualRecords: (previous: Readonly<Record<string, unknown>>, next: Readonly<Record<string, unknown>>) => boolean;
32
+ //#endregion
33
+ export { ModelContextSnapshotSource, shallowEqualRecords, useModelContextSnapshot };
34
+ //# sourceMappingURL=useModelContextSnapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useModelContextSnapshot.d.ts","names":[],"sources":["../../src/unstable/useModelContextSnapshot.ts"],"mappings":";;;;;;;;KAWY,2BAA2B;;WAE5B,OAAO;;EAEhB,KAAK,KAAK,kBAAkB;EAC5B,UAAU,KAAK,iBAAiB,uBAAuB;;;;;;EAMvD,SAAS,UAAU,GAAG,MAAM;;;;;;;;;cAUjB,0BAA2B,GACtC,KAAK,iBACL,kBACA,QAAQ,2BAA2B,OAClC;;cAqBU,sBACX,UAAU,SAAS,0BACnB,MAAM,SAAS"}