@assistant-ui/react 0.12.0 → 0.12.3

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 (154) hide show
  1. package/dist/client/Suggestions.d.ts +11 -0
  2. package/dist/client/Suggestions.d.ts.map +1 -0
  3. package/dist/client/Suggestions.js +43 -0
  4. package/dist/client/Suggestions.js.map +1 -0
  5. package/dist/client/Tools.d.ts.map +1 -1
  6. package/dist/client/Tools.js +5 -1
  7. package/dist/client/Tools.js.map +1 -1
  8. package/dist/context/providers/SuggestionByIndexProvider.d.ts +6 -0
  9. package/dist/context/providers/SuggestionByIndexProvider.d.ts.map +1 -0
  10. package/dist/context/providers/SuggestionByIndexProvider.js +14 -0
  11. package/dist/context/providers/SuggestionByIndexProvider.js.map +1 -0
  12. package/dist/context/providers/ThreadListItemProvider.d.ts +3 -2
  13. package/dist/context/providers/ThreadListItemProvider.d.ts.map +1 -1
  14. package/dist/context/providers/ThreadListItemProvider.js +3 -6
  15. package/dist/context/providers/ThreadListItemProvider.js.map +1 -1
  16. package/dist/context/providers/index.d.ts +2 -1
  17. package/dist/context/providers/index.d.ts.map +1 -1
  18. package/dist/context/providers/index.js +2 -1
  19. package/dist/context/providers/index.js.map +1 -1
  20. package/dist/legacy-runtime/AssistantRuntimeProvider.d.ts +5 -0
  21. package/dist/legacy-runtime/AssistantRuntimeProvider.d.ts.map +1 -1
  22. package/dist/legacy-runtime/AssistantRuntimeProvider.js +2 -4
  23. package/dist/legacy-runtime/AssistantRuntimeProvider.js.map +1 -1
  24. package/dist/legacy-runtime/RuntimeAdapter.js +2 -1
  25. package/dist/legacy-runtime/RuntimeAdapter.js.map +1 -1
  26. package/dist/legacy-runtime/client/ThreadListRuntimeClient.js +3 -3
  27. package/dist/legacy-runtime/client/ThreadListRuntimeClient.js.map +1 -1
  28. package/dist/legacy-runtime/runtime/RuntimeBindings.d.ts +0 -4
  29. package/dist/legacy-runtime/runtime/RuntimeBindings.d.ts.map +1 -1
  30. package/dist/legacy-runtime/runtime/ThreadListRuntime.d.ts +3 -3
  31. package/dist/legacy-runtime/runtime/ThreadListRuntime.d.ts.map +1 -1
  32. package/dist/legacy-runtime/runtime/ThreadListRuntime.js +4 -5
  33. package/dist/legacy-runtime/runtime/ThreadListRuntime.js.map +1 -1
  34. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  35. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +3 -5
  36. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  37. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts +10 -12
  38. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts.map +1 -1
  39. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.js +14 -19
  40. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.js.map +1 -1
  41. package/dist/legacy-runtime/runtime-cores/core/ThreadListRuntimeCore.d.ts +1 -1
  42. package/dist/legacy-runtime/runtime-cores/core/ThreadListRuntimeCore.d.ts.map +1 -1
  43. package/dist/legacy-runtime/runtime-cores/external-store/ExternalStoreThreadListRuntimeCore.d.ts +1 -1
  44. package/dist/legacy-runtime/runtime-cores/external-store/ExternalStoreThreadListRuntimeCore.d.ts.map +1 -1
  45. package/dist/legacy-runtime/runtime-cores/external-store/ExternalStoreThreadListRuntimeCore.js +1 -1
  46. package/dist/legacy-runtime/runtime-cores/external-store/ExternalStoreThreadListRuntimeCore.js.map +1 -1
  47. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.d.ts.map +1 -1
  48. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.js +32 -4
  49. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.js.map +1 -1
  50. package/dist/legacy-runtime/runtime-cores/local/LocalThreadListRuntimeCore.d.ts +1 -1
  51. package/dist/legacy-runtime/runtime-cores/local/LocalThreadListRuntimeCore.d.ts.map +1 -1
  52. package/dist/legacy-runtime/runtime-cores/local/LocalThreadListRuntimeCore.js +1 -1
  53. package/dist/legacy-runtime/runtime-cores/local/LocalThreadListRuntimeCore.js.map +1 -1
  54. package/dist/legacy-runtime/runtime-cores/remote-thread-list/BaseSubscribable.d.ts.map +1 -1
  55. package/dist/legacy-runtime/runtime-cores/remote-thread-list/BaseSubscribable.js +3 -0
  56. package/dist/legacy-runtime/runtime-cores/remote-thread-list/BaseSubscribable.js.map +1 -1
  57. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts +3 -1
  58. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  59. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.js +8 -5
  60. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.js.map +1 -1
  61. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts +1 -1
  62. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  63. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.js +2 -2
  64. package/dist/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  65. package/dist/model-context/index.d.ts +1 -0
  66. package/dist/model-context/index.d.ts.map +1 -1
  67. package/dist/model-context/index.js +1 -0
  68. package/dist/model-context/index.js.map +1 -1
  69. package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -1
  70. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  71. package/dist/primitives/index.d.ts +1 -0
  72. package/dist/primitives/index.d.ts.map +1 -1
  73. package/dist/primitives/index.js +1 -0
  74. package/dist/primitives/index.js.map +1 -1
  75. package/dist/primitives/message/MessageParts.d.ts +11 -0
  76. package/dist/primitives/message/MessageParts.d.ts.map +1 -1
  77. package/dist/primitives/message/MessageParts.js +18 -2
  78. package/dist/primitives/message/MessageParts.js.map +1 -1
  79. package/dist/primitives/suggestion/SuggestionDescription.d.ts +18 -0
  80. package/dist/primitives/suggestion/SuggestionDescription.d.ts.map +1 -0
  81. package/dist/primitives/suggestion/SuggestionDescription.js +19 -0
  82. package/dist/primitives/suggestion/SuggestionDescription.js.map +1 -0
  83. package/dist/primitives/suggestion/SuggestionTitle.d.ts +18 -0
  84. package/dist/primitives/suggestion/SuggestionTitle.d.ts.map +1 -0
  85. package/dist/primitives/suggestion/SuggestionTitle.js +19 -0
  86. package/dist/primitives/suggestion/SuggestionTitle.js.map +1 -0
  87. package/dist/primitives/suggestion/SuggestionTrigger.d.ts +49 -0
  88. package/dist/primitives/suggestion/SuggestionTrigger.d.ts.map +1 -0
  89. package/dist/primitives/suggestion/SuggestionTrigger.js +45 -0
  90. package/dist/primitives/suggestion/SuggestionTrigger.js.map +1 -0
  91. package/dist/primitives/suggestion/index.d.ts +4 -0
  92. package/dist/primitives/suggestion/index.d.ts.map +1 -0
  93. package/dist/primitives/suggestion/index.js +4 -0
  94. package/dist/primitives/suggestion/index.js.map +1 -0
  95. package/dist/primitives/thread/ThreadSuggestions.d.ts +53 -0
  96. package/dist/primitives/thread/ThreadSuggestions.d.ts.map +1 -0
  97. package/dist/primitives/thread/ThreadSuggestions.js +58 -0
  98. package/dist/primitives/thread/ThreadSuggestions.js.map +1 -0
  99. package/dist/primitives/thread/index.d.ts +1 -0
  100. package/dist/primitives/thread/index.d.ts.map +1 -1
  101. package/dist/primitives/thread/index.js +1 -0
  102. package/dist/primitives/thread/index.js.map +1 -1
  103. package/dist/types/scopes/index.d.ts +2 -0
  104. package/dist/types/scopes/index.d.ts.map +1 -1
  105. package/dist/types/scopes/suggestion.d.ts +20 -0
  106. package/dist/types/scopes/suggestion.d.ts.map +1 -0
  107. package/dist/types/scopes/suggestion.js +2 -0
  108. package/dist/types/scopes/suggestion.js.map +1 -0
  109. package/dist/types/scopes/suggestions.d.ts +20 -0
  110. package/dist/types/scopes/suggestions.d.ts.map +1 -0
  111. package/dist/types/scopes/suggestions.js +2 -0
  112. package/dist/types/scopes/suggestions.js.map +1 -0
  113. package/dist/types/store-augmentation.d.ts +4 -0
  114. package/dist/types/store-augmentation.d.ts.map +1 -1
  115. package/dist/utils/idUtils.d.ts +2 -0
  116. package/dist/utils/idUtils.d.ts.map +1 -1
  117. package/dist/utils/idUtils.js +3 -0
  118. package/dist/utils/idUtils.js.map +1 -1
  119. package/package.json +10 -10
  120. package/src/client/Suggestions.ts +74 -0
  121. package/src/client/Tools.ts +10 -1
  122. package/src/context/providers/SuggestionByIndexProvider.tsx +23 -0
  123. package/src/context/providers/ThreadListItemProvider.tsx +6 -8
  124. package/src/context/providers/index.ts +2 -1
  125. package/src/legacy-runtime/AssistantRuntimeProvider.tsx +8 -5
  126. package/src/legacy-runtime/RuntimeAdapter.ts +2 -1
  127. package/src/legacy-runtime/client/ThreadListRuntimeClient.ts +3 -3
  128. package/src/legacy-runtime/runtime/RuntimeBindings.ts +0 -4
  129. package/src/legacy-runtime/runtime/ThreadListRuntime.ts +7 -8
  130. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.tsx +3 -4
  131. package/src/legacy-runtime/runtime-cores/assistant-transport/utils.ts +19 -24
  132. package/src/legacy-runtime/runtime-cores/core/ThreadListRuntimeCore.tsx +1 -1
  133. package/src/legacy-runtime/runtime-cores/external-store/ExternalStoreThreadListRuntimeCore.tsx +1 -1
  134. package/src/legacy-runtime/runtime-cores/external-store/external-message-converter.tsx +42 -7
  135. package/src/legacy-runtime/runtime-cores/local/LocalThreadListRuntimeCore.tsx +1 -1
  136. package/src/legacy-runtime/runtime-cores/remote-thread-list/BaseSubscribable.tsx +3 -0
  137. package/src/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListHookInstanceManager.tsx +20 -6
  138. package/src/legacy-runtime/runtime-cores/remote-thread-list/RemoteThreadListThreadListRuntimeCore.tsx +2 -2
  139. package/src/model-context/index.ts +2 -0
  140. package/src/primitives/assistantModal/AssistantModalRoot.tsx +1 -1
  141. package/src/primitives/index.ts +1 -0
  142. package/src/primitives/message/MessageParts.tsx +45 -1
  143. package/src/primitives/suggestion/SuggestionDescription.tsx +33 -0
  144. package/src/primitives/suggestion/SuggestionTitle.tsx +33 -0
  145. package/src/primitives/suggestion/SuggestionTrigger.tsx +79 -0
  146. package/src/primitives/suggestion/index.ts +3 -0
  147. package/src/primitives/thread/ThreadSuggestions.tsx +109 -0
  148. package/src/primitives/thread/index.ts +4 -0
  149. package/src/tests/external-message-converter.test.ts +105 -16
  150. package/src/types/scopes/index.ts +12 -0
  151. package/src/types/scopes/suggestion.ts +20 -0
  152. package/src/types/scopes/suggestions.ts +21 -0
  153. package/src/types/store-augmentation.ts +4 -0
  154. package/src/utils/idUtils.tsx +4 -0
@@ -28,7 +28,7 @@ const useAssistantModalOpenState = ({
28
28
  return aui.on("thread.runStart", () => {
29
29
  setOpen(true);
30
30
  });
31
- }, [unstable_openOnRunStart, setOpen, aui]);
31
+ }, [unstable_openOnRunStart, aui]);
32
32
 
33
33
  return state;
34
34
  };
@@ -8,6 +8,7 @@ export * as MessagePartPrimitive from "./messagePart";
8
8
  export * as ErrorPrimitive from "./error";
9
9
  export * as MessagePrimitive from "./message";
10
10
  export * as ThreadPrimitive from "./thread";
11
+ export * as SuggestionPrimitive from "./suggestion";
11
12
  export * as ThreadListPrimitive from "./threadList";
12
13
  export * as ThreadListItemPrimitive from "./threadListItem";
13
14
  export * as ThreadListItemMorePrimitive from "./threadListItemMore";
@@ -242,6 +242,17 @@ export namespace MessagePrimitiveParts {
242
242
  ReasoningGroup?: ReasoningGroupComponent;
243
243
  }
244
244
  | undefined;
245
+ /**
246
+ * When enabled, shows the Empty component if the last part in the message
247
+ * is anything other than Text or Reasoning.
248
+ *
249
+ * This can be useful to ensure there's always a visible element at the end
250
+ * of messages that end with non-text content like tool calls or images.
251
+ *
252
+ * @experimental This API is experimental and may change in future versions.
253
+ * @default true
254
+ */
255
+ unstable_showEmptyOnNonTextEnd?: boolean | undefined;
245
256
  };
246
257
  }
247
258
 
@@ -429,6 +440,30 @@ const EmptyParts = memo(
429
440
  prev.components?.Text === next.components?.Text,
430
441
  );
431
442
 
443
+ const ConditionalEmptyImpl: FC<{
444
+ components: MessagePrimitiveParts.Props["components"];
445
+ enabled: boolean;
446
+ }> = ({ components, enabled }) => {
447
+ const shouldShowEmpty = useAuiState(({ message }) => {
448
+ if (!enabled) return false;
449
+ if (message.parts.length === 0) return false;
450
+
451
+ const lastPart = message.parts[message.parts.length - 1];
452
+ return lastPart?.type !== "text" && lastPart?.type !== "reasoning";
453
+ });
454
+
455
+ if (!shouldShowEmpty) return null;
456
+ return <EmptyParts components={components} />;
457
+ };
458
+
459
+ const ConditionalEmpty = memo(
460
+ ConditionalEmptyImpl,
461
+ (prev, next) =>
462
+ prev.enabled === next.enabled &&
463
+ prev.components?.Empty === next.components?.Empty &&
464
+ prev.components?.Text === next.components?.Text,
465
+ );
466
+
432
467
  /**
433
468
  * Renders the parts of a message with support for multiple content types.
434
469
  *
@@ -455,6 +490,7 @@ const EmptyParts = memo(
455
490
  */
456
491
  export const MessagePrimitiveParts: FC<MessagePrimitiveParts.Props> = ({
457
492
  components,
493
+ unstable_showEmptyOnNonTextEnd = true,
458
494
  }) => {
459
495
  const contentLength = useAuiState(({ message }) => message.parts.length);
460
496
  const messageRanges = useMessagePartsGroups();
@@ -520,7 +556,15 @@ export const MessagePrimitiveParts: FC<MessagePrimitiveParts.Props> = ({
520
556
  });
521
557
  }, [messageRanges, components, contentLength]);
522
558
 
523
- return <>{partsElements}</>;
559
+ return (
560
+ <>
561
+ {partsElements}
562
+ <ConditionalEmpty
563
+ components={components}
564
+ enabled={unstable_showEmptyOnNonTextEnd}
565
+ />
566
+ </>
567
+ );
524
568
  };
525
569
 
526
570
  MessagePrimitiveParts.displayName = "MessagePrimitive.Parts";
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { Primitive } from "@radix-ui/react-primitive";
4
+ import { type ElementRef, forwardRef, ComponentPropsWithoutRef } from "react";
5
+ import { useAuiState } from "@assistant-ui/store";
6
+
7
+ export namespace SuggestionPrimitiveDescription {
8
+ export type Element = ElementRef<typeof Primitive.span>;
9
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.span>;
10
+ }
11
+
12
+ /**
13
+ * Renders the description/label of the suggestion.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <SuggestionPrimitive.Description />
18
+ * ```
19
+ */
20
+ export const SuggestionPrimitiveDescription = forwardRef<
21
+ SuggestionPrimitiveDescription.Element,
22
+ SuggestionPrimitiveDescription.Props
23
+ >((props, ref) => {
24
+ const label = useAuiState(({ suggestion }) => suggestion.label);
25
+
26
+ return (
27
+ <Primitive.span {...props} ref={ref}>
28
+ {props.children ?? label}
29
+ </Primitive.span>
30
+ );
31
+ });
32
+
33
+ SuggestionPrimitiveDescription.displayName = "SuggestionPrimitive.Description";
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { Primitive } from "@radix-ui/react-primitive";
4
+ import { type ElementRef, forwardRef, ComponentPropsWithoutRef } from "react";
5
+ import { useAuiState } from "@assistant-ui/store";
6
+
7
+ export namespace SuggestionPrimitiveTitle {
8
+ export type Element = ElementRef<typeof Primitive.span>;
9
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.span>;
10
+ }
11
+
12
+ /**
13
+ * Renders the title of the suggestion.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <SuggestionPrimitive.Title />
18
+ * ```
19
+ */
20
+ export const SuggestionPrimitiveTitle = forwardRef<
21
+ SuggestionPrimitiveTitle.Element,
22
+ SuggestionPrimitiveTitle.Props
23
+ >((props, ref) => {
24
+ const title = useAuiState(({ suggestion }) => suggestion.title);
25
+
26
+ return (
27
+ <Primitive.span {...props} ref={ref}>
28
+ {props.children ?? title}
29
+ </Primitive.span>
30
+ );
31
+ });
32
+
33
+ SuggestionPrimitiveTitle.displayName = "SuggestionPrimitive.Title";
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import {
4
+ ActionButtonElement,
5
+ ActionButtonProps,
6
+ createActionButton,
7
+ } from "../../utils/createActionButton";
8
+ import { useCallback } from "react";
9
+ import { useAuiState, useAui } from "@assistant-ui/store";
10
+
11
+ const useSuggestionTrigger = ({
12
+ send,
13
+ clearComposer = true,
14
+ }: {
15
+ /**
16
+ * When true, automatically sends the message.
17
+ * When false, replaces or appends the composer text with the suggestion - depending on the value of `clearComposer`.
18
+ */
19
+ send?: boolean | undefined;
20
+
21
+ /**
22
+ * Whether to clear the composer after sending.
23
+ * When send is set to false, determines if composer text is replaced with suggestion (true, default),
24
+ * or if it's appended to the composer text (false).
25
+ *
26
+ * @default true
27
+ */
28
+ clearComposer?: boolean | undefined;
29
+ }) => {
30
+ const aui = useAui();
31
+ const disabled = useAuiState(({ thread }) => thread.isDisabled);
32
+ const prompt = useAuiState(({ suggestion }) => suggestion.prompt);
33
+
34
+ const resolvedSend = send ?? false;
35
+
36
+ const callback = useCallback(() => {
37
+ const isRunning = aui.thread().getState().isRunning;
38
+
39
+ if (resolvedSend && !isRunning) {
40
+ aui.thread().append(prompt);
41
+ if (clearComposer) {
42
+ aui.composer().setText("");
43
+ }
44
+ } else {
45
+ if (clearComposer) {
46
+ aui.composer().setText(prompt);
47
+ } else {
48
+ const currentText = aui.composer().getState().text;
49
+ aui
50
+ .composer()
51
+ .setText(currentText.trim() ? `${currentText} ${prompt}` : prompt);
52
+ }
53
+ }
54
+ }, [aui, resolvedSend, clearComposer, prompt]);
55
+
56
+ if (disabled) return null;
57
+ return callback;
58
+ };
59
+
60
+ export namespace SuggestionPrimitiveTrigger {
61
+ export type Element = ActionButtonElement;
62
+ export type Props = ActionButtonProps<typeof useSuggestionTrigger>;
63
+ }
64
+
65
+ /**
66
+ * A button that triggers the suggestion action (send or insert into composer).
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * <SuggestionPrimitive.Trigger send>
71
+ * Click me
72
+ * </SuggestionPrimitive.Trigger>
73
+ * ```
74
+ */
75
+ export const SuggestionPrimitiveTrigger = createActionButton(
76
+ "SuggestionPrimitive.Trigger",
77
+ useSuggestionTrigger,
78
+ ["send", "clearComposer"],
79
+ );
@@ -0,0 +1,3 @@
1
+ export { SuggestionPrimitiveTitle as Title } from "./SuggestionTitle";
2
+ export { SuggestionPrimitiveDescription as Description } from "./SuggestionDescription";
3
+ export { SuggestionPrimitiveTrigger as Trigger } from "./SuggestionTrigger";
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { type ComponentType, type FC, memo, useMemo } from "react";
4
+ import { useAuiState } from "@assistant-ui/store";
5
+ import { SuggestionByIndexProvider } from "../../context/providers";
6
+
7
+ export namespace ThreadPrimitiveSuggestions {
8
+ export type Props = {
9
+ /**
10
+ * Component to render for each suggestion.
11
+ */
12
+ components: {
13
+ /** Component used to render each suggestion */
14
+ Suggestion: ComponentType;
15
+ };
16
+ };
17
+ }
18
+
19
+ type SuggestionComponentProps = {
20
+ components: ThreadPrimitiveSuggestions.Props["components"];
21
+ };
22
+
23
+ const SuggestionComponent: FC<SuggestionComponentProps> = ({ components }) => {
24
+ const Component = components.Suggestion;
25
+ return <Component />;
26
+ };
27
+
28
+ export namespace ThreadPrimitiveSuggestionByIndex {
29
+ export type Props = {
30
+ index: number;
31
+ components: ThreadPrimitiveSuggestions.Props["components"];
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Renders a single suggestion at the specified index.
37
+ *
38
+ * This component provides suggestion context for a specific suggestion
39
+ * and renders it using the provided component configuration.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * <ThreadPrimitive.SuggestionByIndex
44
+ * index={0}
45
+ * components={{
46
+ * Suggestion: MySuggestion
47
+ * }}
48
+ * />
49
+ * ```
50
+ */
51
+ export const ThreadPrimitiveSuggestionByIndex: FC<ThreadPrimitiveSuggestionByIndex.Props> =
52
+ memo(
53
+ ({ index, components }) => {
54
+ return (
55
+ <SuggestionByIndexProvider index={index}>
56
+ <SuggestionComponent components={components} />
57
+ </SuggestionByIndexProvider>
58
+ );
59
+ },
60
+ (prev, next) =>
61
+ prev.index === next.index &&
62
+ prev.components.Suggestion === next.components.Suggestion,
63
+ );
64
+
65
+ ThreadPrimitiveSuggestionByIndex.displayName =
66
+ "ThreadPrimitive.SuggestionByIndex";
67
+
68
+ /**
69
+ * Renders all suggestions using the provided component configuration.
70
+ *
71
+ * This component automatically renders all suggestions from the suggestions scope,
72
+ * providing the appropriate suggestion context for each one.
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * <ThreadPrimitive.Suggestions
77
+ * components={{
78
+ * Suggestion: MySuggestion
79
+ * }}
80
+ * />
81
+ * ```
82
+ */
83
+ export const ThreadPrimitiveSuggestionsImpl: FC<
84
+ ThreadPrimitiveSuggestions.Props
85
+ > = ({ components }) => {
86
+ const suggestionsLength = useAuiState(
87
+ ({ suggestions }) => suggestions.suggestions.length,
88
+ );
89
+
90
+ const suggestionElements = useMemo(() => {
91
+ if (suggestionsLength === 0) return null;
92
+ return Array.from({ length: suggestionsLength }, (_, index) => (
93
+ <ThreadPrimitiveSuggestionByIndex
94
+ key={index}
95
+ index={index}
96
+ components={components}
97
+ />
98
+ ));
99
+ }, [suggestionsLength, components]);
100
+
101
+ return suggestionElements;
102
+ };
103
+
104
+ ThreadPrimitiveSuggestionsImpl.displayName = "ThreadPrimitive.Suggestions";
105
+
106
+ export const ThreadPrimitiveSuggestions = memo(
107
+ ThreadPrimitiveSuggestionsImpl,
108
+ (prev, next) => prev.components.Suggestion === next.components.Suggestion,
109
+ );
@@ -9,3 +9,7 @@ export { ThreadPrimitiveMessages as Messages } from "./ThreadMessages";
9
9
  export { ThreadPrimitiveMessageByIndex as MessageByIndex } from "./ThreadMessages";
10
10
  export { ThreadPrimitiveScrollToBottom as ScrollToBottom } from "./ThreadScrollToBottom";
11
11
  export { ThreadPrimitiveSuggestion as Suggestion } from "./ThreadSuggestion";
12
+ export {
13
+ ThreadPrimitiveSuggestions as Suggestions,
14
+ ThreadPrimitiveSuggestionByIndex as SuggestionByIndex,
15
+ } from "./ThreadSuggestions";
@@ -1,16 +1,10 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { convertExternalMessages } from "../legacy-runtime/runtime-cores/external-store/external-message-converter";
3
3
  import type { useExternalMessageConverter } from "../legacy-runtime/runtime-cores/external-store/external-message-converter";
4
+ import { isErrorMessageId } from "../utils/idUtils";
4
5
 
5
- /**
6
- * Tests for the external message converter, specifically the joinExternalMessages logic.
7
- */
8
6
  describe("convertExternalMessages", () => {
9
7
  describe("reasoning part merging", () => {
10
- /**
11
- * Tests that reasoning parts with the same parentId are merged together.
12
- * The text should be concatenated with a double newline separator.
13
- */
14
8
  it("should merge reasoning parts with the same parentId", () => {
15
9
  const messages = [
16
10
  {
@@ -56,9 +50,6 @@ describe("convertExternalMessages", () => {
56
50
  expect((reasoningParts[0] as any).parentId).toBe("parent1");
57
51
  });
58
52
 
59
- /**
60
- * Tests that reasoning parts without parentId remain separate.
61
- */
62
53
  it("should keep reasoning parts without parentId separate", () => {
63
54
  const messages = [
64
55
  {
@@ -89,9 +80,6 @@ describe("convertExternalMessages", () => {
89
80
  expect((reasoningParts[1] as any).text).toBe("Second reasoning");
90
81
  });
91
82
 
92
- /**
93
- * Tests that reasoning parts with different parentIds remain separate.
94
- */
95
83
  it("should keep reasoning parts with different parentIds separate", () => {
96
84
  const messages = [
97
85
  {
@@ -134,9 +122,6 @@ describe("convertExternalMessages", () => {
134
122
  expect((reasoningParts[1] as any).parentId).toBe("parent2");
135
123
  });
136
124
 
137
- /**
138
- * Tests that tool result merging still works correctly alongside reasoning merging.
139
- */
140
125
  it("should still merge tool results with matching tool calls", () => {
141
126
  const messages = [
142
127
  {
@@ -174,4 +159,108 @@ describe("convertExternalMessages", () => {
174
159
  expect((toolCallParts[0] as any).result).toEqual({ data: "result" });
175
160
  });
176
161
  });
162
+
163
+ describe("synthetic error message", () => {
164
+ it("should create synthetic error message when error exists and no messages", () => {
165
+ const messages: never[] = [];
166
+ const callback: useExternalMessageConverter.Callback<never> = (msg) =>
167
+ msg;
168
+
169
+ const result = convertExternalMessages(messages, callback, false, {
170
+ error: "API key is missing",
171
+ });
172
+
173
+ expect(result).toHaveLength(1);
174
+ expect(result[0]!.role).toBe("assistant");
175
+ expect(result[0]!.content).toHaveLength(0);
176
+ expect(result[0]!.status).toEqual({
177
+ type: "incomplete",
178
+ reason: "error",
179
+ error: "API key is missing",
180
+ });
181
+ expect(isErrorMessageId(result[0]!.id)).toBe(true);
182
+ });
183
+
184
+ it("should create synthetic error message when error exists and last message is user", () => {
185
+ const messages = [
186
+ {
187
+ id: "user1",
188
+ role: "user" as const,
189
+ content: "Hello",
190
+ },
191
+ ];
192
+
193
+ const callback: useExternalMessageConverter.Callback<
194
+ (typeof messages)[number]
195
+ > = (msg) => msg;
196
+
197
+ const result = convertExternalMessages(messages, callback, false, {
198
+ error: { message: "Invalid API key" },
199
+ });
200
+
201
+ expect(result).toHaveLength(2);
202
+ expect(result[0]!.role).toBe("user");
203
+ expect(result[1]!.role).toBe("assistant");
204
+ expect(result[1]!.content).toHaveLength(0);
205
+ expect(result[1]!.status).toEqual({
206
+ type: "incomplete",
207
+ reason: "error",
208
+ error: { message: "Invalid API key" },
209
+ });
210
+ expect(isErrorMessageId(result[1]!.id)).toBe(true);
211
+ });
212
+
213
+ it("should not create synthetic error message when last message is assistant", () => {
214
+ const messages = [
215
+ {
216
+ id: "user1",
217
+ role: "user" as const,
218
+ content: "Hello",
219
+ },
220
+ {
221
+ id: "assistant1",
222
+ role: "assistant" as const,
223
+ content: "Hi there",
224
+ },
225
+ ];
226
+
227
+ const callback: useExternalMessageConverter.Callback<
228
+ (typeof messages)[number]
229
+ > = (msg) => msg;
230
+
231
+ const result = convertExternalMessages(messages, callback, false, {
232
+ error: "Connection error",
233
+ });
234
+
235
+ expect(result).toHaveLength(2);
236
+ expect(result[0]!.role).toBe("user");
237
+ expect(result[1]!.role).toBe("assistant");
238
+ expect(result[1]!.id).toBe("assistant1");
239
+ expect(result[1]!.status).toMatchObject({
240
+ type: "incomplete",
241
+ reason: "error",
242
+ error: "Connection error",
243
+ });
244
+ expect(isErrorMessageId(result[1]!.id)).toBe(false);
245
+ });
246
+
247
+ it("should not create synthetic message when no error", () => {
248
+ const messages = [
249
+ {
250
+ id: "user1",
251
+ role: "user" as const,
252
+ content: "Hello",
253
+ },
254
+ ];
255
+
256
+ const callback: useExternalMessageConverter.Callback<
257
+ (typeof messages)[number]
258
+ > = (msg) => msg;
259
+
260
+ const result = convertExternalMessages(messages, callback, false, {});
261
+
262
+ expect(result).toHaveLength(1);
263
+ expect(result[0]!.role).toBe("user");
264
+ });
265
+ });
177
266
  });
@@ -43,6 +43,18 @@ export type {
43
43
  AttachmentClientSchema,
44
44
  } from "./attachment";
45
45
  export type { ToolsState, ToolsMethods, ToolsClientSchema } from "./tools";
46
+ export type {
47
+ SuggestionsState,
48
+ SuggestionsMethods,
49
+ SuggestionsClientSchema,
50
+ Suggestion,
51
+ } from "./suggestions";
52
+ export type {
53
+ SuggestionState,
54
+ SuggestionMethods,
55
+ SuggestionMeta,
56
+ SuggestionClientSchema,
57
+ } from "./suggestion";
46
58
  export type {
47
59
  ModelContextState,
48
60
  ModelContextMethods,
@@ -0,0 +1,20 @@
1
+ export type SuggestionState = {
2
+ title: string;
3
+ label: string;
4
+ prompt: string;
5
+ };
6
+
7
+ export type SuggestionMethods = {
8
+ getState(): SuggestionState;
9
+ };
10
+
11
+ export type SuggestionMeta = {
12
+ source: "suggestions";
13
+ query: { index: number };
14
+ };
15
+
16
+ export type SuggestionClientSchema = {
17
+ state: SuggestionState;
18
+ methods: SuggestionMethods;
19
+ meta: SuggestionMeta;
20
+ };
@@ -0,0 +1,21 @@
1
+ import type { SuggestionMethods } from "./suggestion";
2
+
3
+ export type Suggestion = {
4
+ title: string;
5
+ label: string;
6
+ prompt: string;
7
+ };
8
+
9
+ export type SuggestionsState = {
10
+ suggestions: Suggestion[];
11
+ };
12
+
13
+ export type SuggestionsMethods = {
14
+ getState(): SuggestionsState;
15
+ suggestion(query: { index: number }): SuggestionMethods;
16
+ };
17
+
18
+ export type SuggestionsClientSchema = {
19
+ state: SuggestionsState;
20
+ methods: SuggestionsMethods;
21
+ };
@@ -9,6 +9,8 @@ import type { ComposerClientSchema } from "./scopes/composer";
9
9
  import type { AttachmentClientSchema } from "./scopes/attachment";
10
10
  import type { ToolsClientSchema } from "./scopes/tools";
11
11
  import type { ModelContextClientSchema } from "./scopes/modelContext";
12
+ import type { SuggestionsClientSchema } from "./scopes/suggestions";
13
+ import type { SuggestionClientSchema } from "./scopes/suggestion";
12
14
 
13
15
  declare module "@assistant-ui/store" {
14
16
  interface ClientRegistry {
@@ -21,5 +23,7 @@ declare module "@assistant-ui/store" {
21
23
  attachment: AttachmentClientSchema;
22
24
  tools: ToolsClientSchema;
23
25
  modelContext: ModelContextClientSchema;
26
+ suggestions: SuggestionsClientSchema;
27
+ suggestion: SuggestionClientSchema;
24
28
  }
25
29
  }
@@ -8,3 +8,7 @@ export const generateId = customAlphabet(
8
8
  const optimisticPrefix = "__optimistic__";
9
9
  export const generateOptimisticId = () => `${optimisticPrefix}${generateId()}`;
10
10
  export const isOptimisticId = (id: string) => id.startsWith(optimisticPrefix);
11
+
12
+ const errorPrefix = "__error__";
13
+ export const generateErrorMessageId = () => `${errorPrefix}${generateId()}`;
14
+ export const isErrorMessageId = (id: string) => id.startsWith(errorPrefix);