@assistant-ui/react 0.14.15 → 0.14.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 (84) hide show
  1. package/dist/client/ExternalThread.d.ts +4 -3
  2. package/dist/client/ExternalThread.d.ts.map +1 -1
  3. package/dist/client/ExternalThread.js +46 -21
  4. package/dist/client/ExternalThread.js.map +1 -1
  5. package/dist/client/InMemoryThreadList.d.ts +1 -1
  6. package/dist/client/InMemoryThreadList.d.ts.map +1 -1
  7. package/dist/client/InMemoryThreadList.js +7 -5
  8. package/dist/client/InMemoryThreadList.js.map +1 -1
  9. package/dist/client/SingleThreadList.d.ts +1 -6
  10. package/dist/client/SingleThreadList.d.ts.map +1 -1
  11. package/dist/client/SingleThreadList.js +6 -4
  12. package/dist/client/SingleThreadList.js.map +1 -1
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +3 -1
  15. package/dist/mcp-apps/McpAppRenderer.d.ts +2 -10
  16. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  17. package/dist/mcp-apps/McpAppRenderer.js +3 -2
  18. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  19. package/dist/mcp-apps/McpAppsRemoteHost.d.ts +1 -8
  20. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  21. package/dist/mcp-apps/McpAppsRemoteHost.js +3 -2
  22. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  23. package/dist/primitives/composer/ComposerInput.js +3 -3
  24. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  25. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +2 -10
  26. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
  27. package/dist/primitives/composer/trigger/TriggerPopoverResource.js +3 -2
  28. package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
  29. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts +2 -6
  30. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
  31. package/dist/primitives/composer/trigger/triggerDetectionResource.js +3 -2
  32. package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
  33. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts +2 -17
  34. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  35. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +3 -2
  36. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  37. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts +2 -10
  38. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  39. package/dist/primitives/composer/trigger/triggerNavigationResource.js +3 -2
  40. package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
  41. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts +2 -10
  42. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  43. package/dist/primitives/composer/trigger/triggerSelectionResource.js +3 -2
  44. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  45. package/dist/primitives/messagePart/MessagePartText.d.ts +5 -2
  46. package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
  47. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  48. package/dist/primitives/reasoning/useScrollLock.js +11 -2
  49. package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
  50. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  51. package/dist/primitives/thread/useThreadViewportAutoScroll.js +5 -0
  52. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  53. package/dist/unstable/useComposerInputHistory.d.ts +30 -0
  54. package/dist/unstable/useComposerInputHistory.d.ts.map +1 -0
  55. package/dist/unstable/useComposerInputHistory.js +117 -0
  56. package/dist/unstable/useComposerInputHistory.js.map +1 -0
  57. package/dist/utils/smooth/useSmooth.d.ts +40 -2
  58. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  59. package/dist/utils/smooth/useSmooth.js +48 -9
  60. package/dist/utils/smooth/useSmooth.js.map +1 -1
  61. package/package.json +31 -24
  62. package/src/client/ExternalThread.ts +70 -27
  63. package/src/client/InMemoryThreadList.ts +11 -7
  64. package/src/client/SingleThreadList.ts +29 -27
  65. package/src/index.ts +8 -0
  66. package/src/mcp-apps/McpAppRenderer.tsx +5 -3
  67. package/src/mcp-apps/McpAppsRemoteHost.ts +5 -3
  68. package/src/primitives/composer/ComposerInput.test.tsx +1 -1
  69. package/src/primitives/composer/ComposerInput.tsx +3 -3
  70. package/src/primitives/composer/trigger/TriggerPopoverResource.ts +5 -3
  71. package/src/primitives/composer/trigger/triggerDetectionResource.ts +21 -21
  72. package/src/primitives/composer/trigger/triggerKeyboardResource.test.ts +5 -4
  73. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +99 -101
  74. package/src/primitives/composer/trigger/triggerNavigationResource.ts +92 -98
  75. package/src/primitives/composer/trigger/triggerSelectionResource.ts +76 -76
  76. package/src/primitives/messagePart/MessagePartText.tsx +3 -2
  77. package/src/primitives/reasoning/useScrollLock.ts +25 -2
  78. package/src/primitives/thread/useThreadViewportAutoScroll.ts +8 -0
  79. package/src/tests/external-thread-branches.test.tsx +160 -0
  80. package/src/tests/shouldContinue.test.ts +33 -0
  81. package/src/unstable/useComposerInputHistory.test.tsx +201 -0
  82. package/src/unstable/useComposerInputHistory.ts +160 -0
  83. package/src/utils/smooth/useSmooth.test.tsx +95 -0
  84. package/src/utils/smooth/useSmooth.ts +82 -10
@@ -8,7 +8,7 @@ function matchesQuery(item, lower) {
8
8
  * Computes categories, items, search results, and navigation state from the
9
9
  * adapter + current query. Pure derivation — no side effects on the composer.
10
10
  */
11
- const TriggerNavigationResource = resource(function TriggerNavigationResource({ adapter, query, open }) {
11
+ const useTriggerNavigationResource = ({ adapter, query, open }) => {
12
12
  const [activeCategoryId, setActiveCategoryId] = useState(null);
13
13
  useEffect(() => {
14
14
  if (!open) setActiveCategoryId(null);
@@ -82,7 +82,8 @@ const TriggerNavigationResource = resource(function TriggerNavigationResource({
82
82
  setActiveCategoryId(null);
83
83
  })
84
84
  };
85
- });
85
+ };
86
+ const TriggerNavigationResource = resource(useTriggerNavigationResource);
86
87
  //#endregion
87
88
  export { TriggerNavigationResource };
88
89
 
@@ -1 +1 @@
1
- {"version":3,"file":"triggerNavigationResource.js","names":[],"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 */\nexport const TriggerNavigationResource = resource(\n function TriggerNavigationResource({\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>(\n null,\n );\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<\n readonly Unstable_TriggerItem[] | null\n >(() => {\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) =>\n cat.label.toLowerCase().includes(lower),\n );\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);\n"],"mappings":";;;AAQA,SAAS,aAAa,MAA4B,OAAwB;CACxE,OACE,KAAK,GAAG,YAAY,CAAC,CAAC,SAAS,KAAK,KACpC,KAAK,MAAM,YAAY,CAAC,CAAC,SAAS,KAAK,MACtC,KAAK,aAAa,YAAY,CAAC,CAAC,SAAS,KAAK,KAAK;AAExD;;;;;AA0BA,MAAa,4BAA4B,SACvC,SAAS,0BAA0B,EACjC,SACA,OACA,QAKkC;CAClC,MAAM,CAAC,kBAAkB,uBAAuB,SAC9C,IACF;CAEA,gBAAgB;EACd,IAAI,CAAC,MAAM,oBAAoB,IAAI;CACrC,GAAG,CAAC,IAAI,CAAC;CAET,MAAM,aAAa,cAAmD;EACpE,IAAI,CAAC,QAAQ,CAAC,SAAS,OAAO,CAAC;EAC/B,OAAO,QAAQ,WAAW;CAC5B,GAAG,CAAC,MAAM,OAAO,CAAC;CAElB,MAAM,4BAA4B,OAAO,mBAAmB;CAE5D,MAAM,WAAW,cAA+C;EAC9D,IAAI,CAAC,6BAA6B,CAAC,SAAS,OAAO,CAAC;EACpD,OAAO,QAAQ,cAAc,yBAAyB;CACxD,GAAG,CAAC,2BAA2B,OAAO,CAAC;CAEvC,MAAM,gBAAgB,cAEd;EACN,IAAI,CAAC,QAAQ,CAAC,WAAW,2BAA2B,OAAO;EAE3D,IAAI,CAAC,SAAS,WAAW,SAAS,GAAG,OAAO;EAC5C,IAAI,QAAQ,QAAQ,OAAO,QAAQ,OAAO,KAAK;EAG/C,MAAM,MAA8B,CAAC;EACrC,MAAM,QAAQ,MAAM,YAAY;EAChC,KAAK,MAAM,OAAO,YAChB,KAAK,MAAM,QAAQ,QAAQ,cAAc,IAAI,EAAE,GAC7C,IAAI,aAAa,MAAM,KAAK,GAC1B,IAAI,KAAK,IAAI;EAInB,OAAO;CACT,GAAG;EAAC;EAAM;EAAS;EAAO;EAA2B;CAAU,CAAC;CAEhE,MAAM,eAAe,kBAAkB;CAEvC,MAAM,qBAAqB,cAAc;EACvC,IAAI,cAAc,OAAO,CAAC;EAC1B,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,QAAQ,MAAM,YAAY;EAChC,OAAO,WAAW,QAAQ,QACxB,IAAI,MAAM,YAAY,CAAC,CAAC,SAAS,KAAK,CACxC;CACF,GAAG;EAAC;EAAY;EAAO;CAAY,CAAC;CAEpC,MAAM,gBAAgB,cAAc;EAClC,IAAI,cAAc,OAAO,iBAAiB,CAAC;EAC3C,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,QAAQ,MAAM,YAAY;EAChC,OAAO,SAAS,QAAQ,SAAS,aAAa,MAAM,KAAK,CAAC;CAC5D,GAAG;EAAC;EAAU;EAAO;EAAc;CAAa,CAAC;CAsBjD,OAAO;EACL,YAAY;EACZ,OAAO;EACP;EACA,kBAAkB;EAClB,eAzBoB,cAAc;GAClC,IAAI,cAAc,OAAO,iBAAiB,CAAC;GAC3C,IAAI,2BAA2B,OAAO;GACtC,OAAO;EACT,GAAG;GACD;GACA;GACA;GACA;GACA;EACF,CAec;EACZ,gBAdqB,gBAAgB,eAAuB;GAC5D,oBAAoB,UAAU;EAChC,CAYe;EACb,QAXa,qBAAqB;GAClC,oBAAoB,IAAI;EAC1B,CASO;CACP;AACF,CACF"}
1
+ {"version":3,"file":"triggerNavigationResource.js","names":[],"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":";;;AAQA,SAAS,aAAa,MAA4B,OAAwB;CACxE,OACE,KAAK,GAAG,YAAY,CAAC,CAAC,SAAS,KAAK,KACpC,KAAK,MAAM,YAAY,CAAC,CAAC,SAAS,KAAK,MACtC,KAAK,aAAa,YAAY,CAAC,CAAC,SAAS,KAAK,KAAK;AAExD;;;;;AA0BA,MAAM,gCAAgC,EACpC,SACA,OACA,WAKqC;CACrC,MAAM,CAAC,kBAAkB,uBAAuB,SAAwB,IAAI;CAE5E,gBAAgB;EACd,IAAI,CAAC,MAAM,oBAAoB,IAAI;CACrC,GAAG,CAAC,IAAI,CAAC;CAET,MAAM,aAAa,cAAmD;EACpE,IAAI,CAAC,QAAQ,CAAC,SAAS,OAAO,CAAC;EAC/B,OAAO,QAAQ,WAAW;CAC5B,GAAG,CAAC,MAAM,OAAO,CAAC;CAElB,MAAM,4BAA4B,OAAO,mBAAmB;CAE5D,MAAM,WAAW,cAA+C;EAC9D,IAAI,CAAC,6BAA6B,CAAC,SAAS,OAAO,CAAC;EACpD,OAAO,QAAQ,cAAc,yBAAyB;CACxD,GAAG,CAAC,2BAA2B,OAAO,CAAC;CAEvC,MAAM,gBAAgB,cAAsD;EAC1E,IAAI,CAAC,QAAQ,CAAC,WAAW,2BAA2B,OAAO;EAE3D,IAAI,CAAC,SAAS,WAAW,SAAS,GAAG,OAAO;EAC5C,IAAI,QAAQ,QAAQ,OAAO,QAAQ,OAAO,KAAK;EAG/C,MAAM,MAA8B,CAAC;EACrC,MAAM,QAAQ,MAAM,YAAY;EAChC,KAAK,MAAM,OAAO,YAChB,KAAK,MAAM,QAAQ,QAAQ,cAAc,IAAI,EAAE,GAC7C,IAAI,aAAa,MAAM,KAAK,GAC1B,IAAI,KAAK,IAAI;EAInB,OAAO;CACT,GAAG;EAAC;EAAM;EAAS;EAAO;EAA2B;CAAU,CAAC;CAEhE,MAAM,eAAe,kBAAkB;CAEvC,MAAM,qBAAqB,cAAc;EACvC,IAAI,cAAc,OAAO,CAAC;EAC1B,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,QAAQ,MAAM,YAAY;EAChC,OAAO,WAAW,QAAQ,QAAQ,IAAI,MAAM,YAAY,CAAC,CAAC,SAAS,KAAK,CAAC;CAC3E,GAAG;EAAC;EAAY;EAAO;CAAY,CAAC;CAEpC,MAAM,gBAAgB,cAAc;EAClC,IAAI,cAAc,OAAO,iBAAiB,CAAC;EAC3C,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,QAAQ,MAAM,YAAY;EAChC,OAAO,SAAS,QAAQ,SAAS,aAAa,MAAM,KAAK,CAAC;CAC5D,GAAG;EAAC;EAAU;EAAO;EAAc;CAAa,CAAC;CAsBjD,OAAO;EACL,YAAY;EACZ,OAAO;EACP;EACA,kBAAkB;EAClB,eAzBoB,cAAc;GAClC,IAAI,cAAc,OAAO,iBAAiB,CAAC;GAC3C,IAAI,2BAA2B,OAAO;GACtC,OAAO;EACT,GAAG;GACD;GACA;GACA;GACA;GACA;EACF,CAec;EACZ,gBAdqB,gBAAgB,eAAuB;GAC5D,oBAAoB,UAAU;EAChC,CAYe;EACb,QAXa,qBAAqB;GAClC,oBAAoB,IAAI;EAC1B,CASO;CACP;AACF;AAEA,MAAa,4BAA4B,SAAS,4BAA4B"}
@@ -20,22 +20,14 @@ type TriggerSelectionResourceOutput = {
20
20
  close(): void; /** Register a Lexical-style selection override. Returns unregister fn. */
21
21
  registerSelectItemOverride(fn: SelectItemOverride): () => void;
22
22
  };
23
- /** Owns composer text mutation + behavior dispatch on item selection. */
24
- declare const TriggerSelectionResource: (props: {
23
+ declare const TriggerSelectionResource: import("@assistant-ui/tap").Resource<TriggerSelectionResourceOutput, [{
25
24
  behavior: TriggerBehavior | undefined;
26
25
  trigger: DetectedTrigger | null;
27
26
  aui: AssistantClient;
28
27
  triggerChar: string;
29
28
  setCursorPosition: (pos: number) => void; /** Called after a successful selection so the parent can reset nav state. */
30
29
  onSelected: () => void;
31
- }) => import("@assistant-ui/tap").ResourceElement<TriggerSelectionResourceOutput, {
32
- behavior: TriggerBehavior | undefined;
33
- trigger: DetectedTrigger | null;
34
- aui: AssistantClient;
35
- triggerChar: string;
36
- setCursorPosition: (pos: number) => void; /** Called after a successful selection so the parent can reset nav state. */
37
- onSelected: () => void;
38
- }>;
30
+ }]>;
39
31
  //#endregion
40
32
  export { SelectItemOverride, TriggerBehavior, TriggerSelectionResource, TriggerSelectionResourceOutput };
41
33
  //# sourceMappingURL=triggerSelectionResource.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"triggerSelectionResource.d.ts","names":[],"sources":["../../../../src/primitives/composer/trigger/triggerSelectionResource.ts"],"mappings":";;;;;;KAUY,kBAAA,IAAsB,IAA0B,EAApB,oBAAoB;AAAA,KAEhD,eAAA;EAAA,SAEG,IAAA;EAAA,SACA,SAAA,EAAW,2BAAA;EAAA,SACX,UAAA,IAAc,IAAA,EAAM,oBAAA;AAAA;EAAA,SAGpB,IAAA;EAAA,SACA,SAAA,EAAW,2BAAA;EAAA,SACX,SAAA,GAAY,IAAA,EAAM,oBAAA;EAAA,SAClB,eAAA;AAAA;AAAA,KAGH,8BAAA;EAJqB,qEAM/B,UAAA,CAAW,IAAA,EAAM,oBAAA,SANkC;EAQnD,KAAA,UAda;EAgBb,0BAAA,CAA2B,EAAA,EAAI,kBAAkB;AAAA;;cAItC,wBAAA,GAAwB,KAAA;YASvB,eAAA;WACD,eAAA;OACJ,eAAA;;sBAEe,GAAA,mBA3BG;;;YAuBb,eAAA;WACD,eAAA;OACJ,eAAA;;sBAEe,GAAA,mBArBtB"}
1
+ {"version":3,"file":"triggerSelectionResource.d.ts","names":[],"sources":["../../../../src/primitives/composer/trigger/triggerSelectionResource.ts"],"mappings":";;;;;;KAUY,kBAAA,IAAsB,IAA0B,EAApB,oBAAoB;AAAA,KAEhD,eAAA;EAAA,SAEG,IAAA;EAAA,SACA,SAAA,EAAW,2BAAA;EAAA,SACX,UAAA,IAAc,IAAA,EAAM,oBAAA;AAAA;EAAA,SAGpB,IAAA;EAAA,SACA,SAAA,EAAW,2BAAA;EAAA,SACX,SAAA,GAAY,IAAA,EAAM,oBAAA;EAAA,SAClB,eAAA;AAAA;AAAA,KAGH,8BAAA;EAJqB,qEAM/B,UAAA,CAAW,IAAA,EAAM,oBAAA,SANkC;EAQnD,KAAA,UAda;EAgBb,0BAAA,CAA2B,EAAA,EAAI,kBAAkB;AAAA;AAAA,cA2FtC,wBAAA,8BAAwB,QAAA,CAAA,8BAAA;YA/EzB,eAAA;WACD,eAAA;OACJ,eAAA;;sBAEe,GAAA,mBA1BW"}
@@ -2,7 +2,7 @@ import { useEffectEvent, useRef } from "@assistant-ui/tap/react-shim";
2
2
  import { resource } from "@assistant-ui/tap";
3
3
  //#region src/primitives/composer/trigger/triggerSelectionResource.ts
4
4
  /** Owns composer text mutation + behavior dispatch on item selection. */
5
- const TriggerSelectionResource = resource(function TriggerSelectionResource({ behavior, trigger, aui, triggerChar, setCursorPosition, onSelected }) {
5
+ const useTriggerSelectionResource = ({ behavior, trigger, aui, triggerChar, setCursorPosition, onSelected }) => {
6
6
  const selectItemOverrideRef = useRef(null);
7
7
  const registerSelectItemOverride = useEffectEvent((fn) => {
8
8
  selectItemOverrideRef.current = fn;
@@ -40,7 +40,8 @@ const TriggerSelectionResource = resource(function TriggerSelectionResource({ be
40
40
  }),
41
41
  registerSelectItemOverride
42
42
  };
43
- });
43
+ };
44
+ const TriggerSelectionResource = resource(useTriggerSelectionResource);
44
45
  //#endregion
45
46
  export { TriggerSelectionResource };
46
47
 
@@ -1 +1 @@
1
- {"version":3,"file":"triggerSelectionResource.js","names":[],"sources":["../../../../src/primitives/composer/trigger/triggerSelectionResource.ts"],"sourcesContent":["import { useEffectEvent, useRef } from \"react\";\nimport { resource } from \"@assistant-ui/tap\";\nimport type {\n Unstable_DirectiveFormatter,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\nimport type { AssistantClient } from \"@assistant-ui/store\";\nimport type { DetectedTrigger } from \"./triggerDetectionResource\";\n\n/** External override for selection (used by Lexical's DirectivePlugin). */\nexport type SelectItemOverride = (item: Unstable_TriggerItem) => boolean;\n\nexport type TriggerBehavior =\n | {\n readonly kind: \"directive\";\n readonly formatter: Unstable_DirectiveFormatter;\n readonly onInserted?: (item: Unstable_TriggerItem) => void;\n }\n | {\n readonly kind: \"action\";\n readonly formatter: Unstable_DirectiveFormatter;\n readonly onExecute: (item: Unstable_TriggerItem) => void;\n readonly removeOnExecute?: boolean;\n };\n\nexport type TriggerSelectionResourceOutput = {\n /** Select an item — runs override (if any) then applies behavior. */\n selectItem(item: Unstable_TriggerItem): void;\n /** Close the popover (moves cursor before trigger to deactivate detection). */\n close(): void;\n /** Register a Lexical-style selection override. Returns unregister fn. */\n registerSelectItemOverride(fn: SelectItemOverride): () => void;\n};\n\n/** Owns composer text mutation + behavior dispatch on item selection. */\nexport const TriggerSelectionResource = resource(\n function TriggerSelectionResource({\n behavior,\n trigger,\n aui,\n triggerChar,\n setCursorPosition,\n onSelected,\n }: {\n behavior: TriggerBehavior | undefined;\n trigger: DetectedTrigger | null;\n aui: AssistantClient;\n triggerChar: string;\n setCursorPosition: (pos: number) => void;\n /** Called after a successful selection so the parent can reset nav state. */\n onSelected: () => void;\n }): TriggerSelectionResourceOutput {\n // Select-item override: lets Lexical's DirectivePlugin intercept selection\n // and drive its own node insertion.\n const selectItemOverrideRef = useRef<SelectItemOverride | null>(null);\n\n const registerSelectItemOverride = useEffectEvent(\n (fn: SelectItemOverride) => {\n selectItemOverrideRef.current = fn;\n return () => {\n if (selectItemOverrideRef.current === fn) {\n selectItemOverrideRef.current = null;\n }\n };\n },\n );\n\n const selectItem = useEffectEvent((item: Unstable_TriggerItem) => {\n if (!trigger || !behavior) return;\n\n if (selectItemOverrideRef.current?.(item)) {\n onSelected();\n return;\n }\n\n const currentText = aui.composer().getState().text;\n const before = currentText.slice(0, trigger.offset);\n const after = currentText.slice(\n trigger.offset + triggerChar.length + trigger.query.length,\n );\n\n const insertDirective = () => {\n const directive = behavior.formatter.serialize(item);\n aui\n .composer()\n .setText(\n before + directive + (after.startsWith(\" \") ? after : ` ${after}`),\n );\n };\n\n if (behavior.kind === \"directive\") {\n insertDirective();\n behavior.onInserted?.(item);\n } else {\n if (behavior.removeOnExecute) {\n aui\n .composer()\n .setText(before + (after.startsWith(\" \") ? after.slice(1) : after));\n } else {\n // Leave directive chip in the composer as an audit trail\n insertDirective();\n }\n behavior.onExecute(item);\n }\n\n onSelected();\n });\n\n const close = useEffectEvent(() => {\n onSelected();\n // Move cursor before the trigger so trigger detection deactivates\n if (trigger) {\n setCursorPosition(trigger.offset);\n }\n });\n\n return {\n selectItem,\n close,\n registerSelectItemOverride,\n };\n },\n);\n"],"mappings":";;;;AAmCA,MAAa,2BAA2B,SACtC,SAAS,yBAAyB,EAChC,UACA,SACA,KACA,aACA,mBACA,cASiC;CAGjC,MAAM,wBAAwB,OAAkC,IAAI;CAEpE,MAAM,6BAA6B,gBAChC,OAA2B;EAC1B,sBAAsB,UAAU;EAChC,aAAa;GACX,IAAI,sBAAsB,YAAY,IACpC,sBAAsB,UAAU;EAEpC;CACF,CACF;CAmDA,OAAO;EACL,YAlDiB,gBAAgB,SAA+B;GAChE,IAAI,CAAC,WAAW,CAAC,UAAU;GAE3B,IAAI,sBAAsB,UAAU,IAAI,GAAG;IACzC,WAAW;IACX;GACF;GAEA,MAAM,cAAc,IAAI,SAAS,CAAC,CAAC,SAAS,CAAC,CAAC;GAC9C,MAAM,SAAS,YAAY,MAAM,GAAG,QAAQ,MAAM;GAClD,MAAM,QAAQ,YAAY,MACxB,QAAQ,SAAS,YAAY,SAAS,QAAQ,MAAM,MACtD;GAEA,MAAM,wBAAwB;IAC5B,MAAM,YAAY,SAAS,UAAU,UAAU,IAAI;IACnD,IACG,SAAS,CAAC,CACV,QACC,SAAS,aAAa,MAAM,WAAW,GAAG,IAAI,QAAQ,IAAI,QAC5D;GACJ;GAEA,IAAI,SAAS,SAAS,aAAa;IACjC,gBAAgB;IAChB,SAAS,aAAa,IAAI;GAC5B,OAAO;IACL,IAAI,SAAS,iBACX,IACG,SAAS,CAAC,CACV,QAAQ,UAAU,MAAM,WAAW,GAAG,IAAI,MAAM,MAAM,CAAC,IAAI,MAAM;SAGpE,gBAAgB;IAElB,SAAS,UAAU,IAAI;GACzB;GAEA,WAAW;EACb,CAWW;EACT,OAVY,qBAAqB;GACjC,WAAW;GAEX,IAAI,SACF,kBAAkB,QAAQ,MAAM;EAEpC,CAIM;EACJ;CACF;AACF,CACF"}
1
+ {"version":3,"file":"triggerSelectionResource.js","names":[],"sources":["../../../../src/primitives/composer/trigger/triggerSelectionResource.ts"],"sourcesContent":["import { useEffectEvent, useRef } from \"react\";\nimport { resource } from \"@assistant-ui/tap\";\nimport type {\n Unstable_DirectiveFormatter,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\nimport type { AssistantClient } from \"@assistant-ui/store\";\nimport type { DetectedTrigger } from \"./triggerDetectionResource\";\n\n/** External override for selection (used by Lexical's DirectivePlugin). */\nexport type SelectItemOverride = (item: Unstable_TriggerItem) => boolean;\n\nexport type TriggerBehavior =\n | {\n readonly kind: \"directive\";\n readonly formatter: Unstable_DirectiveFormatter;\n readonly onInserted?: (item: Unstable_TriggerItem) => void;\n }\n | {\n readonly kind: \"action\";\n readonly formatter: Unstable_DirectiveFormatter;\n readonly onExecute: (item: Unstable_TriggerItem) => void;\n readonly removeOnExecute?: boolean;\n };\n\nexport type TriggerSelectionResourceOutput = {\n /** Select an item — runs override (if any) then applies behavior. */\n selectItem(item: Unstable_TriggerItem): void;\n /** Close the popover (moves cursor before trigger to deactivate detection). */\n close(): void;\n /** Register a Lexical-style selection override. Returns unregister fn. */\n registerSelectItemOverride(fn: SelectItemOverride): () => void;\n};\n\n/** Owns composer text mutation + behavior dispatch on item selection. */\nconst useTriggerSelectionResource = ({\n behavior,\n trigger,\n aui,\n triggerChar,\n setCursorPosition,\n onSelected,\n}: {\n behavior: TriggerBehavior | undefined;\n trigger: DetectedTrigger | null;\n aui: AssistantClient;\n triggerChar: string;\n setCursorPosition: (pos: number) => void;\n /** Called after a successful selection so the parent can reset nav state. */\n onSelected: () => void;\n}): TriggerSelectionResourceOutput => {\n // Select-item override: lets Lexical's DirectivePlugin intercept selection\n // and drive its own node insertion.\n const selectItemOverrideRef = useRef<SelectItemOverride | null>(null);\n\n const registerSelectItemOverride = useEffectEvent(\n (fn: SelectItemOverride) => {\n selectItemOverrideRef.current = fn;\n return () => {\n if (selectItemOverrideRef.current === fn) {\n selectItemOverrideRef.current = null;\n }\n };\n },\n );\n\n const selectItem = useEffectEvent((item: Unstable_TriggerItem) => {\n if (!trigger || !behavior) return;\n\n if (selectItemOverrideRef.current?.(item)) {\n onSelected();\n return;\n }\n\n const currentText = aui.composer().getState().text;\n const before = currentText.slice(0, trigger.offset);\n const after = currentText.slice(\n trigger.offset + triggerChar.length + trigger.query.length,\n );\n\n const insertDirective = () => {\n const directive = behavior.formatter.serialize(item);\n aui\n .composer()\n .setText(\n before + directive + (after.startsWith(\" \") ? after : ` ${after}`),\n );\n };\n\n if (behavior.kind === \"directive\") {\n insertDirective();\n behavior.onInserted?.(item);\n } else {\n if (behavior.removeOnExecute) {\n aui\n .composer()\n .setText(before + (after.startsWith(\" \") ? after.slice(1) : after));\n } else {\n // Leave directive chip in the composer as an audit trail\n insertDirective();\n }\n behavior.onExecute(item);\n }\n\n onSelected();\n });\n\n const close = useEffectEvent(() => {\n onSelected();\n // Move cursor before the trigger so trigger detection deactivates\n if (trigger) {\n setCursorPosition(trigger.offset);\n }\n });\n\n return {\n selectItem,\n close,\n registerSelectItemOverride,\n };\n};\n\nexport const TriggerSelectionResource = resource(useTriggerSelectionResource);\n"],"mappings":";;;;AAmCA,MAAM,+BAA+B,EACnC,UACA,SACA,KACA,aACA,mBACA,iBASoC;CAGpC,MAAM,wBAAwB,OAAkC,IAAI;CAEpE,MAAM,6BAA6B,gBAChC,OAA2B;EAC1B,sBAAsB,UAAU;EAChC,aAAa;GACX,IAAI,sBAAsB,YAAY,IACpC,sBAAsB,UAAU;EAEpC;CACF,CACF;CAmDA,OAAO;EACL,YAlDiB,gBAAgB,SAA+B;GAChE,IAAI,CAAC,WAAW,CAAC,UAAU;GAE3B,IAAI,sBAAsB,UAAU,IAAI,GAAG;IACzC,WAAW;IACX;GACF;GAEA,MAAM,cAAc,IAAI,SAAS,CAAC,CAAC,SAAS,CAAC,CAAC;GAC9C,MAAM,SAAS,YAAY,MAAM,GAAG,QAAQ,MAAM;GAClD,MAAM,QAAQ,YAAY,MACxB,QAAQ,SAAS,YAAY,SAAS,QAAQ,MAAM,MACtD;GAEA,MAAM,wBAAwB;IAC5B,MAAM,YAAY,SAAS,UAAU,UAAU,IAAI;IACnD,IACG,SAAS,CAAC,CACV,QACC,SAAS,aAAa,MAAM,WAAW,GAAG,IAAI,QAAQ,IAAI,QAC5D;GACJ;GAEA,IAAI,SAAS,SAAS,aAAa;IACjC,gBAAgB;IAChB,SAAS,aAAa,IAAI;GAC5B,OAAO;IACL,IAAI,SAAS,iBACX,IACG,SAAS,CAAC,CACV,QAAQ,UAAU,MAAM,WAAW,GAAG,IAAI,MAAM,MAAM,CAAC,IAAI,MAAM;SAGpE,gBAAgB;IAElB,SAAS,UAAU,IAAI;GACzB;GAEA,WAAW;EACb,CAWW;EACT,OAVY,qBAAqB;GACjC,WAAW;GAEX,IAAI,SACF,kBAAkB,QAAQ,MAAM;EAEpC,CAIM;EACJ;CACF;AACF;AAEA,MAAa,2BAA2B,SAAS,2BAA2B"}
@@ -1,4 +1,5 @@
1
1
  import { Primitive } from "../../utils/Primitive.js";
2
+ import { SmoothOptions } from "../../utils/smooth/useSmooth.js";
2
3
  import { ComponentPropsWithoutRef, ComponentRef, ElementType } from "react";
3
4
 
4
5
  //#region src/primitives/messagePart/MessagePartText.d.ts
@@ -8,9 +9,10 @@ declare namespace MessagePartPrimitiveText {
8
9
  /**
9
10
  * Whether to enable smooth text streaming animation.
10
11
  * When enabled, text appears with a typing effect as it streams in.
12
+ * Pass a `SmoothOptions` object to tune the reveal rate.
11
13
  * @default true
12
14
  */
13
- smooth?: boolean;
15
+ smooth?: boolean | SmoothOptions;
14
16
  /**
15
17
  * The HTML element or React component to render as.
16
18
  * @default "span"
@@ -42,9 +44,10 @@ declare const MessagePartPrimitiveText: import("react").ForwardRefExoticComponen
42
44
  /**
43
45
  * Whether to enable smooth text streaming animation.
44
46
  * When enabled, text appears with a typing effect as it streams in.
47
+ * Pass a `SmoothOptions` object to tune the reveal rate.
45
48
  * @default true
46
49
  */
47
- smooth?: boolean;
50
+ smooth?: boolean | SmoothOptions;
48
51
  /**
49
52
  * The HTML element or React component to render as.
50
53
  * @default "span"
@@ -1 +1 @@
1
- {"version":3,"file":"MessagePartText.d.ts","names":[],"sources":["../../../src/primitives/messagePart/MessagePartText.tsx"],"mappings":";;;;kBAYiB,wBAAA;EAAA,KACH,OAAA,GAAU,YAAA,QAAoB,SAAA,CAAU,IAAA;EAAA,KACxC,KAAA,GAAQ,IAAA,CAClB,wBAAA,QAAgC,SAAA,CAAU,IAAA;IAHL;;;;;IAWrC,MAAA;IATkB;;;;IAclB,SAAA,GAAY,WAAA;EAAA;AAAA;;;;;;;;;;;AAAW;AAoB3B;;;;;cAAa,wBAAA,kBAAwB,yBAAA,CAAA,IAAA,CAAA,IAAA,CAAA,IAAA,iBAAA,eAAA,CAAA,eAAA,oBAAA,cAAA,CAAA,eAAA;;;;;;;;;;;EAAA;;;;cApBrB,WAAA;AAAA"}
1
+ {"version":3,"file":"MessagePartText.d.ts","names":[],"sources":["../../../src/primitives/messagePart/MessagePartText.tsx"],"mappings":";;;;;kBAYiB,wBAAA;EAAA,KACH,OAAA,GAAU,YAAA,QAAoB,SAAA,CAAU,IAAA;EAAA,KACxC,KAAA,GAAQ,IAAA,CAClB,wBAAA,QAAgC,SAAA,CAAU,IAAA;IAHL;;;;;;IAYrC,MAAA,aAAmB,aAAA;IAAA;;;;IAKnB,SAAA,GAAY,WAAA;EAAA;AAAA;;;;;;;;;;;;AAAW;AAoB3B;;;;cAAa,wBAAA,kBAAwB,yBAAA,CAAA,IAAA,CAAA,IAAA,CAAA,IAAA,iBAAA,eAAA,CAAA,eAAA,oBAAA,cAAA,CAAA,eAAA;;;;;EAzBd;;;;;;qBAAA,aAAA;EAyBc;;;;cApBrB,WAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"MessagePartText.js","names":[],"sources":["../../../src/primitives/messagePart/MessagePartText.tsx"],"sourcesContent":["\"use client\";\n\nimport type { Primitive } from \"../../utils/Primitive\";\nimport {\n type ComponentRef,\n forwardRef,\n type ComponentPropsWithoutRef,\n type ElementType,\n} from \"react\";\nimport { useMessagePartText } from \"./useMessagePartText\";\nimport { useSmooth } from \"../../utils/smooth/useSmooth\";\n\nexport namespace MessagePartPrimitiveText {\n export type Element = ComponentRef<typeof Primitive.span>;\n export type Props = Omit<\n ComponentPropsWithoutRef<typeof Primitive.span>,\n \"children\" | \"asChild\"\n > & {\n /**\n * Whether to enable smooth text streaming animation.\n * When enabled, text appears with a typing effect as it streams in.\n * @default true\n */\n smooth?: boolean;\n /**\n * The HTML element or React component to render as.\n * @default \"span\"\n */\n component?: ElementType;\n };\n}\n\n/**\n * Renders the text content of a message part with optional smooth streaming.\n *\n * This component displays text content from the current message part context,\n * with support for smooth streaming animation that shows text appearing\n * character by character as it's generated.\n *\n * @example\n * ```tsx\n * <MessagePartPrimitive.Text\n * smooth={true}\n * component=\"p\"\n * className=\"message-text\"\n * />\n * ```\n */\nexport const MessagePartPrimitiveText = forwardRef<\n MessagePartPrimitiveText.Element,\n MessagePartPrimitiveText.Props\n>(({ smooth = true, component: Component = \"span\", ...rest }, forwardedRef) => {\n const { text, status } = useSmooth(useMessagePartText(), smooth);\n\n return (\n <Component data-status={status.type} {...rest} ref={forwardedRef}>\n {text}\n </Component>\n );\n});\n\nMessagePartPrimitiveText.displayName = \"MessagePartPrimitive.Text\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,2BAA2B,YAGrC,EAAE,SAAS,MAAM,WAAW,YAAY,QAAQ,GAAG,QAAQ,iBAAiB;CAC7E,MAAM,EAAE,MAAM,WAAW,UAAU,mBAAmB,GAAG,MAAM;CAE/D,OACE,oBAAC,WAAD;EAAW,eAAa,OAAO;EAAM,GAAI;EAAM,KAAK;YACjD;CACQ,CAAA;AAEf,CAAC;AAED,yBAAyB,cAAc"}
1
+ {"version":3,"file":"MessagePartText.js","names":[],"sources":["../../../src/primitives/messagePart/MessagePartText.tsx"],"sourcesContent":["\"use client\";\n\nimport type { Primitive } from \"../../utils/Primitive\";\nimport {\n type ComponentRef,\n forwardRef,\n type ComponentPropsWithoutRef,\n type ElementType,\n} from \"react\";\nimport { useMessagePartText } from \"./useMessagePartText\";\nimport { useSmooth, type SmoothOptions } from \"../../utils/smooth/useSmooth\";\n\nexport namespace MessagePartPrimitiveText {\n export type Element = ComponentRef<typeof Primitive.span>;\n export type Props = Omit<\n ComponentPropsWithoutRef<typeof Primitive.span>,\n \"children\" | \"asChild\"\n > & {\n /**\n * Whether to enable smooth text streaming animation.\n * When enabled, text appears with a typing effect as it streams in.\n * Pass a `SmoothOptions` object to tune the reveal rate.\n * @default true\n */\n smooth?: boolean | SmoothOptions;\n /**\n * The HTML element or React component to render as.\n * @default \"span\"\n */\n component?: ElementType;\n };\n}\n\n/**\n * Renders the text content of a message part with optional smooth streaming.\n *\n * This component displays text content from the current message part context,\n * with support for smooth streaming animation that shows text appearing\n * character by character as it's generated.\n *\n * @example\n * ```tsx\n * <MessagePartPrimitive.Text\n * smooth={true}\n * component=\"p\"\n * className=\"message-text\"\n * />\n * ```\n */\nexport const MessagePartPrimitiveText = forwardRef<\n MessagePartPrimitiveText.Element,\n MessagePartPrimitiveText.Props\n>(({ smooth = true, component: Component = \"span\", ...rest }, forwardedRef) => {\n const { text, status } = useSmooth(useMessagePartText(), smooth);\n\n return (\n <Component data-status={status.type} {...rest} ref={forwardedRef}>\n {text}\n </Component>\n );\n});\n\nMessagePartPrimitiveText.displayName = \"MessagePartPrimitive.Text\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,2BAA2B,YAGrC,EAAE,SAAS,MAAM,WAAW,YAAY,QAAQ,GAAG,QAAQ,iBAAiB;CAC7E,MAAM,EAAE,MAAM,WAAW,UAAU,mBAAmB,GAAG,MAAM;CAE/D,OACE,oBAAC,WAAD;EAAW,eAAa,OAAO;EAAM,GAAI;EAAM,KAAK;YACjD;CACQ,CAAA;AAEf,CAAC;AAED,yBAAyB,cAAc"}
@@ -53,18 +53,27 @@ const useScrollLock = (animatedElementRef, animationDuration) => {
53
53
  if (!scrollContainer) return;
54
54
  const scrollPosition = scrollContainer.scrollTop;
55
55
  const scrollbarWidth = scrollContainer.style.scrollbarWidth;
56
+ const computed = getComputedStyle(scrollContainer);
57
+ const paddingSide = computed.direction === "rtl" ? "paddingLeft" : "paddingRight";
58
+ const previousPadding = scrollContainer.style[paddingSide];
59
+ const scrollbarSize = scrollContainer.offsetWidth - scrollContainer.clientWidth - parseFloat(computed.borderLeftWidth) - parseFloat(computed.borderRightWidth);
56
60
  scrollContainer.style.scrollbarWidth = "none";
61
+ if (scrollbarSize > 0) scrollContainer.style[paddingSide] = `${parseFloat(computed[paddingSide]) + scrollbarSize}px`;
62
+ const restoreStyles = () => {
63
+ scrollContainer.style.scrollbarWidth = scrollbarWidth;
64
+ scrollContainer.style[paddingSide] = previousPadding;
65
+ };
57
66
  const resetPosition = () => scrollContainer.scrollTop = scrollPosition;
58
67
  scrollContainer.addEventListener("scroll", resetPosition);
59
68
  const timeoutId = setTimeout(() => {
60
69
  scrollContainer.removeEventListener("scroll", resetPosition);
61
- scrollContainer.style.scrollbarWidth = scrollbarWidth;
70
+ restoreStyles();
62
71
  cleanupRef.current = null;
63
72
  }, animationDuration);
64
73
  cleanupRef.current = () => {
65
74
  clearTimeout(timeoutId);
66
75
  scrollContainer.removeEventListener("scroll", resetPosition);
67
- scrollContainer.style.scrollbarWidth = scrollbarWidth;
76
+ restoreStyles();
68
77
  };
69
78
  }, [animationDuration, animatedElementRef]);
70
79
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useScrollLock.js","names":[],"sources":["../../../src/primitives/reasoning/useScrollLock.ts"],"sourcesContent":["\"use client\";\n\nimport { type RefObject, useCallback, useEffect, useRef } from \"react\";\n\n/**\n * Locks scroll position during collapsible/height animations and hides scrollbar.\n *\n * This utility prevents page jumps when content height changes during animations,\n * providing a smooth user experience. It finds the nearest scrollable ancestor and\n * temporarily locks its scroll position while the animation completes.\n *\n * - Prevents forced reflows: no layout reads, mutations scoped to scrollable parent only\n * - Reactive: only intercepts scroll events when browser actually adjusts\n * - Cleans up automatically after animation duration\n *\n * @param animatedElementRef - Ref to the animated element\n * @param animationDuration - Lock duration in milliseconds\n * @returns Function to activate the scroll lock\n *\n * @example\n * ```tsx\n * const collapsibleRef = useRef<HTMLDivElement>(null);\n * const lockScroll = useScrollLock(collapsibleRef, 200);\n *\n * const handleCollapse = () => {\n * lockScroll(); // Lock scroll before collapsing\n * setIsOpen(false);\n * };\n * ```\n */\nexport const useScrollLock = <T extends HTMLElement = HTMLElement>(\n animatedElementRef: RefObject<T | null>,\n animationDuration: number,\n) => {\n const scrollContainerRef = useRef<HTMLElement | null>(null);\n const cleanupRef = useRef<(() => void) | null>(null);\n\n useEffect(() => {\n return () => {\n cleanupRef.current?.();\n };\n }, []);\n\n const lockScroll = useCallback(() => {\n cleanupRef.current?.();\n\n (function findScrollableAncestor() {\n if (scrollContainerRef.current || !animatedElementRef.current) return;\n\n let el: HTMLElement | null = animatedElementRef.current;\n while (el) {\n const { overflowY } = getComputedStyle(el);\n if (overflowY === \"scroll\" || overflowY === \"auto\") {\n scrollContainerRef.current = el;\n break;\n }\n el = el.parentElement;\n }\n })();\n\n const scrollContainer = scrollContainerRef.current;\n if (!scrollContainer) return;\n\n const scrollPosition = scrollContainer.scrollTop;\n const scrollbarWidth = scrollContainer.style.scrollbarWidth;\n\n scrollContainer.style.scrollbarWidth = \"none\";\n\n const resetPosition = () => (scrollContainer.scrollTop = scrollPosition);\n scrollContainer.addEventListener(\"scroll\", resetPosition);\n\n const timeoutId = setTimeout(() => {\n scrollContainer.removeEventListener(\"scroll\", resetPosition);\n scrollContainer.style.scrollbarWidth = scrollbarWidth;\n cleanupRef.current = null;\n }, animationDuration);\n\n cleanupRef.current = () => {\n clearTimeout(timeoutId);\n scrollContainer.removeEventListener(\"scroll\", resetPosition);\n scrollContainer.style.scrollbarWidth = scrollbarWidth;\n };\n }, [animationDuration, animatedElementRef]);\n\n return lockScroll;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAa,iBACX,oBACA,sBACG;CACH,MAAM,qBAAqB,OAA2B,IAAI;CAC1D,MAAM,aAAa,OAA4B,IAAI;CAEnD,gBAAgB;EACd,aAAa;GACX,WAAW,UAAU;EACvB;CACF,GAAG,CAAC,CAAC;CA2CL,OAzCmB,kBAAkB;EACnC,WAAW,UAAU;EAErB,CAAC,SAAS,yBAAyB;GACjC,IAAI,mBAAmB,WAAW,CAAC,mBAAmB,SAAS;GAE/D,IAAI,KAAyB,mBAAmB;GAChD,OAAO,IAAI;IACT,MAAM,EAAE,cAAc,iBAAiB,EAAE;IACzC,IAAI,cAAc,YAAY,cAAc,QAAQ;KAClD,mBAAmB,UAAU;KAC7B;IACF;IACA,KAAK,GAAG;GACV;EACF,EAAA,CAAG;EAEH,MAAM,kBAAkB,mBAAmB;EAC3C,IAAI,CAAC,iBAAiB;EAEtB,MAAM,iBAAiB,gBAAgB;EACvC,MAAM,iBAAiB,gBAAgB,MAAM;EAE7C,gBAAgB,MAAM,iBAAiB;EAEvC,MAAM,sBAAuB,gBAAgB,YAAY;EACzD,gBAAgB,iBAAiB,UAAU,aAAa;EAExD,MAAM,YAAY,iBAAiB;GACjC,gBAAgB,oBAAoB,UAAU,aAAa;GAC3D,gBAAgB,MAAM,iBAAiB;GACvC,WAAW,UAAU;EACvB,GAAG,iBAAiB;EAEpB,WAAW,gBAAgB;GACzB,aAAa,SAAS;GACtB,gBAAgB,oBAAoB,UAAU,aAAa;GAC3D,gBAAgB,MAAM,iBAAiB;EACzC;CACF,GAAG,CAAC,mBAAmB,kBAAkB,CAEzB;AAClB"}
1
+ {"version":3,"file":"useScrollLock.js","names":[],"sources":["../../../src/primitives/reasoning/useScrollLock.ts"],"sourcesContent":["\"use client\";\n\nimport { type RefObject, useCallback, useEffect, useRef } from \"react\";\n\n/**\n * Locks scroll position during collapsible/height animations and hides scrollbar.\n *\n * This utility prevents page jumps when content height changes during animations,\n * providing a smooth user experience. It finds the nearest scrollable ancestor and\n * temporarily locks its scroll position while the animation completes.\n *\n * - Prevents forced reflows: no layout reads, mutations scoped to scrollable parent only\n * - Reactive: only intercepts scroll events when browser actually adjusts\n * - Cleans up automatically after animation duration\n *\n * @param animatedElementRef - Ref to the animated element\n * @param animationDuration - Lock duration in milliseconds\n * @returns Function to activate the scroll lock\n *\n * @example\n * ```tsx\n * const collapsibleRef = useRef<HTMLDivElement>(null);\n * const lockScroll = useScrollLock(collapsibleRef, 200);\n *\n * const handleCollapse = () => {\n * lockScroll(); // Lock scroll before collapsing\n * setIsOpen(false);\n * };\n * ```\n */\nexport const useScrollLock = <T extends HTMLElement = HTMLElement>(\n animatedElementRef: RefObject<T | null>,\n animationDuration: number,\n) => {\n const scrollContainerRef = useRef<HTMLElement | null>(null);\n const cleanupRef = useRef<(() => void) | null>(null);\n\n useEffect(() => {\n return () => {\n cleanupRef.current?.();\n };\n }, []);\n\n const lockScroll = useCallback(() => {\n cleanupRef.current?.();\n\n (function findScrollableAncestor() {\n if (scrollContainerRef.current || !animatedElementRef.current) return;\n\n let el: HTMLElement | null = animatedElementRef.current;\n while (el) {\n const { overflowY } = getComputedStyle(el);\n if (overflowY === \"scroll\" || overflowY === \"auto\") {\n scrollContainerRef.current = el;\n break;\n }\n el = el.parentElement;\n }\n })();\n\n const scrollContainer = scrollContainerRef.current;\n if (!scrollContainer) return;\n\n const scrollPosition = scrollContainer.scrollTop;\n const scrollbarWidth = scrollContainer.style.scrollbarWidth;\n\n // Hiding the scrollbar collapses its gutter on classic scrollbars, which\n // shifts centered content horizontally; compensate with padding on the\n // side the scrollbar occupies (the left side in RTL).\n const computed = getComputedStyle(scrollContainer);\n const paddingSide =\n computed.direction === \"rtl\" ? \"paddingLeft\" : \"paddingRight\";\n const previousPadding = scrollContainer.style[paddingSide];\n const scrollbarSize =\n scrollContainer.offsetWidth -\n scrollContainer.clientWidth -\n parseFloat(computed.borderLeftWidth) -\n parseFloat(computed.borderRightWidth);\n\n scrollContainer.style.scrollbarWidth = \"none\";\n if (scrollbarSize > 0) {\n scrollContainer.style[paddingSide] = `${\n parseFloat(computed[paddingSide]) + scrollbarSize\n }px`;\n }\n\n const restoreStyles = () => {\n scrollContainer.style.scrollbarWidth = scrollbarWidth;\n scrollContainer.style[paddingSide] = previousPadding;\n };\n\n const resetPosition = () => (scrollContainer.scrollTop = scrollPosition);\n scrollContainer.addEventListener(\"scroll\", resetPosition);\n\n const timeoutId = setTimeout(() => {\n scrollContainer.removeEventListener(\"scroll\", resetPosition);\n restoreStyles();\n cleanupRef.current = null;\n }, animationDuration);\n\n cleanupRef.current = () => {\n clearTimeout(timeoutId);\n scrollContainer.removeEventListener(\"scroll\", resetPosition);\n restoreStyles();\n };\n }, [animationDuration, animatedElementRef]);\n\n return lockScroll;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAa,iBACX,oBACA,sBACG;CACH,MAAM,qBAAqB,OAA2B,IAAI;CAC1D,MAAM,aAAa,OAA4B,IAAI;CAEnD,gBAAgB;EACd,aAAa;GACX,WAAW,UAAU;EACvB;CACF,GAAG,CAAC,CAAC;CAkEL,OAhEmB,kBAAkB;EACnC,WAAW,UAAU;EAErB,CAAC,SAAS,yBAAyB;GACjC,IAAI,mBAAmB,WAAW,CAAC,mBAAmB,SAAS;GAE/D,IAAI,KAAyB,mBAAmB;GAChD,OAAO,IAAI;IACT,MAAM,EAAE,cAAc,iBAAiB,EAAE;IACzC,IAAI,cAAc,YAAY,cAAc,QAAQ;KAClD,mBAAmB,UAAU;KAC7B;IACF;IACA,KAAK,GAAG;GACV;EACF,EAAA,CAAG;EAEH,MAAM,kBAAkB,mBAAmB;EAC3C,IAAI,CAAC,iBAAiB;EAEtB,MAAM,iBAAiB,gBAAgB;EACvC,MAAM,iBAAiB,gBAAgB,MAAM;EAK7C,MAAM,WAAW,iBAAiB,eAAe;EACjD,MAAM,cACJ,SAAS,cAAc,QAAQ,gBAAgB;EACjD,MAAM,kBAAkB,gBAAgB,MAAM;EAC9C,MAAM,gBACJ,gBAAgB,cAChB,gBAAgB,cAChB,WAAW,SAAS,eAAe,IACnC,WAAW,SAAS,gBAAgB;EAEtC,gBAAgB,MAAM,iBAAiB;EACvC,IAAI,gBAAgB,GAClB,gBAAgB,MAAM,eAAe,GACnC,WAAW,SAAS,YAAY,IAAI,cACrC;EAGH,MAAM,sBAAsB;GAC1B,gBAAgB,MAAM,iBAAiB;GACvC,gBAAgB,MAAM,eAAe;EACvC;EAEA,MAAM,sBAAuB,gBAAgB,YAAY;EACzD,gBAAgB,iBAAiB,UAAU,aAAa;EAExD,MAAM,YAAY,iBAAiB;GACjC,gBAAgB,oBAAoB,UAAU,aAAa;GAC3D,cAAc;GACd,WAAW,UAAU;EACvB,GAAG,iBAAiB;EAEpB,WAAW,gBAAgB;GACzB,aAAa,SAAS;GACtB,gBAAgB,oBAAoB,UAAU,aAAa;GAC3D,cAAc;EAChB;CACF,GAAG,CAAC,mBAAmB,kBAAkB,CAEzB;AAClB"}
@@ -1 +1 @@
1
- {"version":3,"file":"useThreadViewportAutoScroll.d.ts","names":[],"sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"mappings":";;;kBAWiB,2BAAA;EAAA,KACH,OAAA;IADG;;;;;;IAQb,UAAA;IAcA;;;AAO4B;AAIhC;IAlBI,wBAAA;IAkMH;;;;;IA3LG,0BAAA;IAgBD;;;;;IATC,4BAAA;EAAA;AAAA;AAAA,cAIS,2BAAA,oBAAgD,WAAA;EAAa,UAAA;EAAA,wBAAA;EAAA,0BAAA;EAAA;AAAA,GAKvE,2BAAA,CAA4B,OAAA,KAAU,WAAA,CAAY,QAAA"}
1
+ {"version":3,"file":"useThreadViewportAutoScroll.d.ts","names":[],"sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"mappings":";;;kBAWiB,2BAAA;EAAA,KACH,OAAA;IADG;;;;;;IAQb,UAAA;IAcA;;;AAO4B;AAIhC;IAlBI,wBAAA;IA0MH;;;;;IAnMG,0BAAA;IAgBD;;;;;IATC,4BAAA;EAAA;AAAA;AAAA,cAIS,2BAAA,oBAAgD,WAAA;EAAa,UAAA;EAAA,wBAAA;EAAA,0BAAA;EAAA;AAAA,GAKvE,2BAAA,CAA4B,OAAA,KAAU,WAAA,CAAY,QAAA"}
@@ -72,9 +72,14 @@ const useThreadViewportAutoScroll = ({ autoScroll, scrollToBottomOnRunStart = tr
72
72
  handleScroll();
73
73
  });
74
74
  const scrollRef = useManagedRef((el) => {
75
+ const cancelPendingScrollToBottom = () => {
76
+ scrollingToBottomBehaviorRef.current = null;
77
+ };
75
78
  el.addEventListener("scroll", handleScroll);
79
+ el.addEventListener("pointerdown", cancelPendingScrollToBottom);
76
80
  return () => {
77
81
  el.removeEventListener("scroll", handleScroll);
82
+ el.removeEventListener("pointerdown", cancelPendingScrollToBottom);
78
83
  };
79
84
  });
80
85
  useLayoutEffect(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"useThreadViewportAutoScroll.js","names":[],"sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"sourcesContent":["\"use client\";\n\nimport { useComposedRefs } from \"@radix-ui/react-compose-refs\";\nimport { useCallback, useLayoutEffect, useRef, type RefCallback } from \"react\";\nimport { useAuiEvent, useAuiState } from \"@assistant-ui/store\";\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 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\n const scrollToBottom = useCallback((behavior: ScrollBehavior) => {\n const div = divRef.current;\n if (!div) return;\n\n scrollingToBottomBehaviorRef.current = behavior;\n div.scrollTo({ top: div.scrollHeight, behavior });\n }, []);\n\n const scheduleScrollToBottom = useCallback(\n (behavior: ScrollBehavior) => {\n scrollingToBottomBehaviorRef.current = behavior;\n if (scheduledFrameRef.current !== null) {\n cancelAnimationFrame(scheduledFrameRef.current);\n }\n scheduledFrameRef.current = requestAnimationFrame(() => {\n scheduledFrameRef.current = null;\n scrollToBottom(behavior);\n });\n },\n [scrollToBottom],\n );\n\n useLayoutEffect(\n () => () => {\n if (scheduledFrameRef.current !== null) {\n cancelAnimationFrame(scheduledFrameRef.current);\n }\n },\n [],\n );\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 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 } else if (\n lastScrollTop.current > div.scrollTop &&\n lastScrollHeight.current === div.scrollHeight\n ) {\n // scrollHeight equality rules out content-driven shifts being misread as user scroll-up\n scrollingToBottomBehaviorRef.current = null;\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 (autoScroll && threadViewportStore.getState().isAtBottom) {\n scrollToBottom(\"instant\");\n }\n\n handleScroll();\n });\n\n const scrollRef = useManagedRef<HTMLElement>((el) => {\n el.addEventListener(\"scroll\", handleScroll);\n return () => {\n el.removeEventListener(\"scroll\", handleScroll);\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(\"threadListItem.switchedTo\", () => {\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":[],"sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"sourcesContent":["\"use client\";\n\nimport { useComposedRefs } from \"@radix-ui/react-compose-refs\";\nimport { useCallback, useLayoutEffect, useRef, type RefCallback } from \"react\";\nimport { useAuiEvent, useAuiState } from \"@assistant-ui/store\";\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 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\n const scrollToBottom = useCallback((behavior: ScrollBehavior) => {\n const div = divRef.current;\n if (!div) return;\n\n scrollingToBottomBehaviorRef.current = behavior;\n div.scrollTo({ top: div.scrollHeight, behavior });\n }, []);\n\n const scheduleScrollToBottom = useCallback(\n (behavior: ScrollBehavior) => {\n scrollingToBottomBehaviorRef.current = behavior;\n if (scheduledFrameRef.current !== null) {\n cancelAnimationFrame(scheduledFrameRef.current);\n }\n scheduledFrameRef.current = requestAnimationFrame(() => {\n scheduledFrameRef.current = null;\n scrollToBottom(behavior);\n });\n },\n [scrollToBottom],\n );\n\n useLayoutEffect(\n () => () => {\n if (scheduledFrameRef.current !== null) {\n cancelAnimationFrame(scheduledFrameRef.current);\n }\n },\n [],\n );\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 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 } else if (\n lastScrollTop.current > div.scrollTop &&\n lastScrollHeight.current === div.scrollHeight\n ) {\n // scrollHeight equality rules out content-driven shifts being misread as user scroll-up\n scrollingToBottomBehaviorRef.current = null;\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 (autoScroll && threadViewportStore.getState().isAtBottom) {\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(\"threadListItem.switchedTo\", () => {\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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"}
@@ -0,0 +1,30 @@
1
+ import { KeyboardEventHandler } from "react";
2
+
3
+ //#region src/unstable/useComposerInputHistory.d.ts
4
+ type Unstable_ComposerInputHistory = {
5
+ /** Keydown handler to spread onto `ComposerPrimitive.Input`. */onKeyDown: KeyboardEventHandler<HTMLTextAreaElement>;
6
+ };
7
+ /**
8
+ * @deprecated Under active development and might change without notice.
9
+ *
10
+ * Terminal-style input history for the thread composer: ArrowUp on an
11
+ * empty draft recalls previously sent user messages (newest first),
12
+ * ArrowDown steps back toward the newest and finally restores the draft
13
+ * that was being typed when browsing started.
14
+ *
15
+ * Recall only triggers when the caret is on the first/last line with no
16
+ * selection, so multi-line editing keeps native arrow behavior. The
17
+ * handler yields to an open mention/slash popover, to IME composition,
18
+ * to modifier keys, and to consumer handlers that already called
19
+ * `preventDefault`. It is inert on edit composers.
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * const history = unstable_useComposerInputHistory();
24
+ * <ComposerPrimitive.Input {...history} />
25
+ * ```
26
+ */
27
+ declare function unstable_useComposerInputHistory(): Unstable_ComposerInputHistory;
28
+ //#endregion
29
+ export { Unstable_ComposerInputHistory, unstable_useComposerInputHistory };
30
+ //# sourceMappingURL=useComposerInputHistory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useComposerInputHistory.d.ts","names":[],"sources":["../../src/unstable/useComposerInputHistory.ts"],"mappings":";;;KAgBY,6BAAA;kEAEV,SAAA,EAAW,oBAAoB,CAAC,mBAAA;AAAA;;;;;;;AAAmB;AAgDrD;;;;AAAiF;;;;;;;;;iBAAjE,gCAAA,IAAoC,6BAA6B"}
@@ -0,0 +1,117 @@
1
+ "use client";
2
+ import { useTriggerPopoverRootContextOptional } from "../primitives/composer/trigger/TriggerPopoverRootContext.js";
3
+ import { useAui } from "@assistant-ui/store";
4
+ import { useCallback, useEffect, useMemo, useRef } from "@assistant-ui/tap/react-shim";
5
+ import { flushTapSync } from "@assistant-ui/tap";
6
+ import { getThreadMessageText } from "@assistant-ui/core/internal";
7
+ //#region src/unstable/useComposerInputHistory.ts
8
+ const deriveHistory = (messages) => {
9
+ const entries = [];
10
+ for (let i = messages.length - 1; i >= 0; i--) {
11
+ const message = messages[i];
12
+ if (message.role !== "user") continue;
13
+ const text = getThreadMessageText(message).trim();
14
+ if (!text) continue;
15
+ if (entries[entries.length - 1] === text) continue;
16
+ entries.push(text);
17
+ }
18
+ return entries;
19
+ };
20
+ const isOnFirstLine = (value, caret) => !value.slice(0, caret).includes("\n");
21
+ const isOnLastLine = (value, caret) => !value.slice(caret).includes("\n");
22
+ /**
23
+ * @deprecated Under active development and might change without notice.
24
+ *
25
+ * Terminal-style input history for the thread composer: ArrowUp on an
26
+ * empty draft recalls previously sent user messages (newest first),
27
+ * ArrowDown steps back toward the newest and finally restores the draft
28
+ * that was being typed when browsing started.
29
+ *
30
+ * Recall only triggers when the caret is on the first/last line with no
31
+ * selection, so multi-line editing keeps native arrow behavior. The
32
+ * handler yields to an open mention/slash popover, to IME composition,
33
+ * to modifier keys, and to consumer handlers that already called
34
+ * `preventDefault`. It is inert on edit composers.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * const history = unstable_useComposerInputHistory();
39
+ * <ComposerPrimitive.Input {...history} />
40
+ * ```
41
+ */
42
+ function unstable_useComposerInputHistory() {
43
+ const aui = useAui();
44
+ const popoverCtx = useTriggerPopoverRootContextOptional();
45
+ const browseRef = useRef(null);
46
+ useEffect(() => {
47
+ if (aui.composer().getState().type !== "thread") return void 0;
48
+ return aui.on("threadListItem.switchedTo", () => {
49
+ browseRef.current = null;
50
+ });
51
+ }, [aui]);
52
+ const onKeyDown = useCallback((e) => {
53
+ if (e.defaultPrevented) return;
54
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
55
+ if (e.nativeEvent.isComposing) return;
56
+ if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
57
+ if (popoverCtx && popoverCtx.getActiveAria() !== null) return;
58
+ if (aui.composer().getState().type !== "thread") return;
59
+ const textarea = e.currentTarget;
60
+ const { selectionStart, selectionEnd, value } = textarea;
61
+ if (selectionStart !== selectionEnd) return;
62
+ if (browseRef.current && value !== browseRef.current.lastRecalledText) browseRef.current = null;
63
+ const browse = browseRef.current;
64
+ const commitText = (text) => {
65
+ flushTapSync(() => aui.composer().setText(text));
66
+ requestAnimationFrame(() => {
67
+ textarea.setSelectionRange(text.length, text.length);
68
+ });
69
+ e.preventDefault();
70
+ };
71
+ const recall = (history, cursor, draftSnapshot) => {
72
+ const entry = history[cursor];
73
+ if (entry === void 0) {
74
+ e.preventDefault();
75
+ return;
76
+ }
77
+ browseRef.current = {
78
+ cursor,
79
+ draftSnapshot,
80
+ lastRecalledText: entry
81
+ };
82
+ commitText(entry);
83
+ };
84
+ if (e.key === "ArrowUp") {
85
+ if (!isOnFirstLine(value, selectionStart)) return;
86
+ if (!browse) {
87
+ if (value.trim() !== "") return;
88
+ const history = deriveHistory(aui.thread().getState().messages);
89
+ if (history.length === 0) return;
90
+ recall(history, 0, value);
91
+ return;
92
+ }
93
+ const history = deriveHistory(aui.thread().getState().messages);
94
+ const next = browse.cursor + 1;
95
+ if (next >= history.length) {
96
+ e.preventDefault();
97
+ return;
98
+ }
99
+ recall(history, next, browse.draftSnapshot);
100
+ return;
101
+ }
102
+ if (!browse) return;
103
+ if (!isOnLastLine(value, selectionEnd)) return;
104
+ const next = browse.cursor - 1;
105
+ if (next < 0) {
106
+ browseRef.current = null;
107
+ commitText(browse.draftSnapshot);
108
+ return;
109
+ }
110
+ recall(deriveHistory(aui.thread().getState().messages), next, browse.draftSnapshot);
111
+ }, [aui, popoverCtx]);
112
+ return useMemo(() => ({ onKeyDown }), [onKeyDown]);
113
+ }
114
+ //#endregion
115
+ export { unstable_useComposerInputHistory };
116
+
117
+ //# sourceMappingURL=useComposerInputHistory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useComposerInputHistory.js","names":[],"sources":["../../src/unstable/useComposerInputHistory.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n type KeyboardEvent,\n type KeyboardEventHandler,\n} from \"react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { flushTapSync } from \"@assistant-ui/tap\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport { getThreadMessageText } from \"@assistant-ui/core/internal\";\nimport { useTriggerPopoverRootContextOptional } from \"../primitives/composer/trigger/TriggerPopoverRootContext\";\n\nexport type Unstable_ComposerInputHistory = {\n /** Keydown handler to spread onto `ComposerPrimitive.Input`. */\n onKeyDown: KeyboardEventHandler<HTMLTextAreaElement>;\n};\n\ntype BrowseState = {\n cursor: number;\n draftSnapshot: string;\n lastRecalledText: string;\n};\n\nconst deriveHistory = (messages: readonly ThreadMessage[]): string[] => {\n const entries: string[] = [];\n for (let i = messages.length - 1; i >= 0; i--) {\n const message = messages[i]!;\n if (message.role !== \"user\") continue;\n const text = getThreadMessageText(message).trim();\n if (!text) continue;\n if (entries[entries.length - 1] === text) continue;\n entries.push(text);\n }\n return entries;\n};\n\nconst isOnFirstLine = (value: string, caret: number): boolean =>\n !value.slice(0, caret).includes(\"\\n\");\n\nconst isOnLastLine = (value: string, caret: number): boolean =>\n !value.slice(caret).includes(\"\\n\");\n\n/**\n * @deprecated Under active development and might change without notice.\n *\n * Terminal-style input history for the thread composer: ArrowUp on an\n * empty draft recalls previously sent user messages (newest first),\n * ArrowDown steps back toward the newest and finally restores the draft\n * that was being typed when browsing started.\n *\n * Recall only triggers when the caret is on the first/last line with no\n * selection, so multi-line editing keeps native arrow behavior. The\n * handler yields to an open mention/slash popover, to IME composition,\n * to modifier keys, and to consumer handlers that already called\n * `preventDefault`. It is inert on edit composers.\n *\n * @example\n * ```tsx\n * const history = unstable_useComposerInputHistory();\n * <ComposerPrimitive.Input {...history} />\n * ```\n */\nexport function unstable_useComposerInputHistory(): Unstable_ComposerInputHistory {\n const aui = useAui();\n const popoverCtx = useTriggerPopoverRootContextOptional();\n const browseRef = useRef<BrowseState | null>(null);\n\n useEffect(() => {\n if (aui.composer().getState().type !== \"thread\") return undefined;\n\n return aui.on(\"threadListItem.switchedTo\", () => {\n browseRef.current = null;\n });\n }, [aui]);\n\n const onKeyDown = useCallback(\n (e: KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.defaultPrevented) return;\n if (e.key !== \"ArrowUp\" && e.key !== \"ArrowDown\") return;\n if (e.nativeEvent.isComposing) return;\n if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;\n if (popoverCtx && popoverCtx.getActiveAria() !== null) return;\n if (aui.composer().getState().type !== \"thread\") return;\n\n const textarea = e.currentTarget;\n const { selectionStart, selectionEnd, value } = textarea;\n if (selectionStart !== selectionEnd) return;\n\n if (browseRef.current && value !== browseRef.current.lastRecalledText) {\n browseRef.current = null;\n }\n const browse = browseRef.current;\n\n const commitText = (text: string): void => {\n flushTapSync(() => aui.composer().setText(text));\n // React's controlled-value commit restores the pre-recall caret;\n // reposition after the commit, before paint.\n requestAnimationFrame(() => {\n textarea.setSelectionRange(text.length, text.length);\n });\n e.preventDefault();\n };\n\n const recall = (\n history: readonly string[],\n cursor: number,\n draftSnapshot: string,\n ): void => {\n const entry = history[cursor];\n if (entry === undefined) {\n e.preventDefault();\n return;\n }\n browseRef.current = { cursor, draftSnapshot, lastRecalledText: entry };\n commitText(entry);\n };\n\n if (e.key === \"ArrowUp\") {\n if (!isOnFirstLine(value, selectionStart)) return;\n\n if (!browse) {\n if (value.trim() !== \"\") return;\n const history = deriveHistory(aui.thread().getState().messages);\n if (history.length === 0) return;\n recall(history, 0, value);\n return;\n }\n\n const history = deriveHistory(aui.thread().getState().messages);\n const next = browse.cursor + 1;\n if (next >= history.length) {\n e.preventDefault();\n return;\n }\n recall(history, next, browse.draftSnapshot);\n return;\n }\n\n if (!browse) return;\n if (!isOnLastLine(value, selectionEnd)) return;\n\n const next = browse.cursor - 1;\n if (next < 0) {\n browseRef.current = null;\n commitText(browse.draftSnapshot);\n return;\n }\n\n const history = deriveHistory(aui.thread().getState().messages);\n recall(history, next, browse.draftSnapshot);\n },\n [aui, popoverCtx],\n );\n\n return useMemo(() => ({ onKeyDown }), [onKeyDown]);\n}\n"],"mappings":"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"}
@@ -1,7 +1,45 @@
1
1
  import { MessagePartState, ReasoningMessagePart, TextMessagePart } from "@assistant-ui/core";
2
2
 
3
3
  //#region src/utils/smooth/useSmooth.d.ts
4
- declare const useSmooth: (state: MessagePartState & (TextMessagePart | ReasoningMessagePart), smooth?: boolean) => MessagePartState & (TextMessagePart | ReasoningMessagePart);
4
+ /**
5
+ * Tuning options for the smooth text streaming animation.
6
+ */
7
+ type SmoothOptions = {
8
+ /**
9
+ * Target time in milliseconds to drain the backlog of unrevealed
10
+ * characters. Larger values reveal long backlogs more gradually.
11
+ * @default 250
12
+ */
13
+ drainMs?: number | undefined;
14
+ /**
15
+ * Maximum time in milliseconds between revealed characters, i.e. the
16
+ * slowest reveal rate when the backlog is short.
17
+ * @default 5
18
+ */
19
+ maxCharIntervalMs?: number | undefined;
20
+ /**
21
+ * Maximum number of characters revealed per animation frame.
22
+ * @default Infinity
23
+ */
24
+ maxCharsPerFrame?: number | undefined;
25
+ };
26
+ /**
27
+ * Animates streamed message part text with a typewriter-style reveal.
28
+ *
29
+ * Takes the current part state and a `smooth` argument: `false` disables,
30
+ * `true` uses the default rate, and a {@link SmoothOptions} object tunes
31
+ * the reveal. Returns the part state with `text` replaced by the revealed
32
+ * prefix and `status` reporting `running` until the reveal catches up.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * const { text, status } = useSmooth(useMessagePartText(), {
37
+ * drainMs: 500,
38
+ * maxCharsPerFrame: 30,
39
+ * });
40
+ * ```
41
+ */
42
+ declare const useSmooth: (state: MessagePartState & (TextMessagePart | ReasoningMessagePart), smooth?: boolean | SmoothOptions) => MessagePartState & (TextMessagePart | ReasoningMessagePart);
5
43
  //#endregion
6
- export { useSmooth };
44
+ export { SmoothOptions, useSmooth };
7
45
  //# sourceMappingURL=useSmooth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSmooth.d.ts","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"mappings":";;;cAwEa,SAAA,GACX,KAAA,EAAO,gBAAA,IAAoB,eAAA,GAAkB,oBAAA,GAC7C,MAAA,eACC,gBAAA,IAAoB,eAAA,GAAkB,oBAAA"}
1
+ {"version":3,"file":"useSmooth.d.ts","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"mappings":";;;;;AAiBA;KAAY,aAAA;;;;;;EAMV,OAAA;EAWgB;AA+FlB;;;;EApGE,iBAAA;EAqG6C;;;;EAhG7C,gBAAA;AAAA;;;;;;;;;;;;AAkG2D;;;;;cAHhD,SAAA,GACX,KAAA,EAAO,gBAAA,IAAoB,eAAA,GAAkB,oBAAA,GAC7C,MAAA,aAAkB,aAAA,KACjB,gBAAA,IAAoB,eAAA,GAAkB,oBAAA"}