@assistant-ui/vue 0.0.0 → 0.0.2

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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -2
  3. package/dist/AuiIf.d.ts +27 -0
  4. package/dist/AuiIf.d.ts.map +1 -0
  5. package/dist/AuiIf.js +30 -0
  6. package/dist/AuiIf.js.map +1 -0
  7. package/dist/AuiProvider.d.ts +41 -0
  8. package/dist/AuiProvider.d.ts.map +1 -0
  9. package/dist/AuiProvider.js +61 -0
  10. package/dist/AuiProvider.js.map +1 -0
  11. package/dist/context.d.ts +10 -0
  12. package/dist/context.d.ts.map +1 -0
  13. package/dist/context.js +17 -0
  14. package/dist/context.js.map +1 -0
  15. package/dist/index.d.ts +34 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +35 -0
  18. package/dist/primitives/AttachmentByIndexProvider.d.ts +30 -0
  19. package/dist/primitives/AttachmentByIndexProvider.d.ts.map +1 -0
  20. package/dist/primitives/AttachmentByIndexProvider.js +63 -0
  21. package/dist/primitives/AttachmentByIndexProvider.js.map +1 -0
  22. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.d.ts +11 -0
  23. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.d.ts.map +1 -0
  24. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.js +31 -0
  25. package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.js.map +1 -0
  26. package/dist/primitives/ChainOfThoughtPrimitiveParts.d.ts +19 -0
  27. package/dist/primitives/ChainOfThoughtPrimitiveParts.d.ts.map +1 -0
  28. package/dist/primitives/ChainOfThoughtPrimitiveParts.js +85 -0
  29. package/dist/primitives/ChainOfThoughtPrimitiveParts.js.map +1 -0
  30. package/dist/primitives/ComposerPrimitiveCancel.d.ts +12 -0
  31. package/dist/primitives/ComposerPrimitiveCancel.d.ts.map +1 -0
  32. package/dist/primitives/ComposerPrimitiveCancel.js +33 -0
  33. package/dist/primitives/ComposerPrimitiveCancel.js.map +1 -0
  34. package/dist/primitives/ComposerPrimitiveInput.d.ts +26 -0
  35. package/dist/primitives/ComposerPrimitiveInput.d.ts.map +1 -0
  36. package/dist/primitives/ComposerPrimitiveInput.js +57 -0
  37. package/dist/primitives/ComposerPrimitiveInput.js.map +1 -0
  38. package/dist/primitives/ComposerPrimitiveSend.d.ts +13 -0
  39. package/dist/primitives/ComposerPrimitiveSend.d.ts.map +1 -0
  40. package/dist/primitives/ComposerPrimitiveSend.js +31 -0
  41. package/dist/primitives/ComposerPrimitiveSend.js.map +1 -0
  42. package/dist/primitives/MessageByIdProvider.d.ts +23 -0
  43. package/dist/primitives/MessageByIdProvider.d.ts.map +1 -0
  44. package/dist/primitives/MessageByIdProvider.js +70 -0
  45. package/dist/primitives/MessageByIdProvider.js.map +1 -0
  46. package/dist/primitives/MessagePrimitiveParts.d.ts +51 -0
  47. package/dist/primitives/MessagePrimitiveParts.d.ts.map +1 -0
  48. package/dist/primitives/MessagePrimitiveParts.js +93 -0
  49. package/dist/primitives/MessagePrimitiveParts.js.map +1 -0
  50. package/dist/primitives/PartByIndexProvider.d.ts +21 -0
  51. package/dist/primitives/PartByIndexProvider.d.ts.map +1 -0
  52. package/dist/primitives/PartByIndexProvider.js +50 -0
  53. package/dist/primitives/PartByIndexProvider.js.map +1 -0
  54. package/dist/primitives/ThreadListItemPrimitiveRoot.d.ts +7 -0
  55. package/dist/primitives/ThreadListItemPrimitiveRoot.d.ts.map +1 -0
  56. package/dist/primitives/ThreadListItemPrimitiveRoot.js +36 -0
  57. package/dist/primitives/ThreadListItemPrimitiveRoot.js.map +1 -0
  58. package/dist/primitives/ThreadListItemPrimitiveTrigger.d.ts +14 -0
  59. package/dist/primitives/ThreadListItemPrimitiveTrigger.d.ts.map +1 -0
  60. package/dist/primitives/ThreadListItemPrimitiveTrigger.js +58 -0
  61. package/dist/primitives/ThreadListItemPrimitiveTrigger.js.map +1 -0
  62. package/dist/primitives/ThreadListPrimitiveRoot.d.ts +7 -0
  63. package/dist/primitives/ThreadListPrimitiveRoot.d.ts.map +1 -0
  64. package/dist/primitives/ThreadListPrimitiveRoot.js +16 -0
  65. package/dist/primitives/ThreadListPrimitiveRoot.js.map +1 -0
  66. package/dist/primitives/ThreadPrimitiveMessages.d.ts +21 -0
  67. package/dist/primitives/ThreadPrimitiveMessages.d.ts.map +1 -0
  68. package/dist/primitives/ThreadPrimitiveMessages.js +35 -0
  69. package/dist/primitives/ThreadPrimitiveMessages.js.map +1 -0
  70. package/dist/primitives/ThreadPrimitiveScrollToBottom.d.ts +25 -0
  71. package/dist/primitives/ThreadPrimitiveScrollToBottom.d.ts.map +1 -0
  72. package/dist/primitives/ThreadPrimitiveScrollToBottom.js +43 -0
  73. package/dist/primitives/ThreadPrimitiveScrollToBottom.js.map +1 -0
  74. package/dist/primitives/ThreadPrimitiveViewport.d.ts +57 -0
  75. package/dist/primitives/ThreadPrimitiveViewport.d.ts.map +1 -0
  76. package/dist/primitives/ThreadPrimitiveViewport.js +99 -0
  77. package/dist/primitives/ThreadPrimitiveViewport.js.map +1 -0
  78. package/dist/primitives/ThreadPrimitiveViewportFooter.d.ts +13 -0
  79. package/dist/primitives/ThreadPrimitiveViewportFooter.d.ts.map +1 -0
  80. package/dist/primitives/ThreadPrimitiveViewportFooter.js +46 -0
  81. package/dist/primitives/ThreadPrimitiveViewportFooter.js.map +1 -0
  82. package/dist/primitives/actionBar.d.ts +41 -0
  83. package/dist/primitives/actionBar.d.ts.map +1 -0
  84. package/dist/primitives/actionBar.js +110 -0
  85. package/dist/primitives/actionBar.js.map +1 -0
  86. package/dist/primitives/attachment.d.ts +27 -0
  87. package/dist/primitives/attachment.d.ts.map +1 -0
  88. package/dist/primitives/attachment.js +63 -0
  89. package/dist/primitives/attachment.js.map +1 -0
  90. package/dist/primitives/attrDisabled.d.ts +2 -0
  91. package/dist/primitives/attrDisabled.d.ts.map +1 -0
  92. package/dist/primitives/attrDisabled.js +6 -0
  93. package/dist/primitives/attrDisabled.js.map +1 -0
  94. package/dist/primitives/branchPicker.d.ts +18 -0
  95. package/dist/primitives/branchPicker.d.ts.map +1 -0
  96. package/dist/primitives/branchPicker.js +48 -0
  97. package/dist/primitives/branchPicker.js.map +1 -0
  98. package/dist/primitives/composerAttachments.d.ts +57 -0
  99. package/dist/primitives/composerAttachments.d.ts.map +1 -0
  100. package/dist/primitives/composerAttachments.js +132 -0
  101. package/dist/primitives/composerAttachments.js.map +1 -0
  102. package/dist/primitives/error.d.ts +15 -0
  103. package/dist/primitives/error.d.ts.map +1 -0
  104. package/dist/primitives/error.js +29 -0
  105. package/dist/primitives/error.js.map +1 -0
  106. package/dist/primitives/lastValidCache.d.ts +6 -0
  107. package/dist/primitives/lastValidCache.d.ts.map +1 -0
  108. package/dist/primitives/lastValidCache.js +9 -0
  109. package/dist/primitives/lastValidCache.js.map +1 -0
  110. package/dist/primitives/message.d.ts +12 -0
  111. package/dist/primitives/message.d.ts.map +1 -0
  112. package/dist/primitives/message.js +44 -0
  113. package/dist/primitives/message.js.map +1 -0
  114. package/dist/primitives/messageAttachments.d.ts +12 -0
  115. package/dist/primitives/messageAttachments.d.ts.map +1 -0
  116. package/dist/primitives/messageAttachments.js +27 -0
  117. package/dist/primitives/messageAttachments.js.map +1 -0
  118. package/dist/primitives/stableKeys.d.ts +5 -0
  119. package/dist/primitives/stableKeys.d.ts.map +1 -0
  120. package/dist/primitives/stableKeys.js +14 -0
  121. package/dist/primitives/stableKeys.js.map +1 -0
  122. package/dist/primitives/suggestions.d.ts +66 -0
  123. package/dist/primitives/suggestions.d.ts.map +1 -0
  124. package/dist/primitives/suggestions.js +137 -0
  125. package/dist/primitives/suggestions.js.map +1 -0
  126. package/dist/primitives/thread.d.ts +11 -0
  127. package/dist/primitives/thread.d.ts.map +1 -0
  128. package/dist/primitives/thread.js +37 -0
  129. package/dist/primitives/thread.js.map +1 -0
  130. package/dist/primitives/threadList.d.ts +82 -0
  131. package/dist/primitives/threadList.d.ts.map +1 -0
  132. package/dist/primitives/threadList.js +128 -0
  133. package/dist/primitives/threadList.js.map +1 -0
  134. package/dist/primitives/threadListFocusGroup.d.ts +14 -0
  135. package/dist/primitives/threadListFocusGroup.d.ts.map +1 -0
  136. package/dist/primitives/threadListFocusGroup.js +32 -0
  137. package/dist/primitives/threadListFocusGroup.js.map +1 -0
  138. package/dist/primitives/threadListStructural.d.ts +29 -0
  139. package/dist/primitives/threadListStructural.d.ts.map +1 -0
  140. package/dist/primitives/threadListStructural.js +55 -0
  141. package/dist/primitives/threadListStructural.js.map +1 -0
  142. package/dist/primitives/useComposerSendState.d.ts +6 -0
  143. package/dist/primitives/useComposerSendState.d.ts.map +1 -0
  144. package/dist/primitives/useComposerSendState.js +15 -0
  145. package/dist/primitives/useComposerSendState.js.map +1 -0
  146. package/dist/primitives/viewportContext.d.ts +12 -0
  147. package/dist/primitives/viewportContext.d.ts.map +1 -0
  148. package/dist/primitives/viewportContext.js +6 -0
  149. package/dist/primitives/viewportContext.js.map +1 -0
  150. package/dist/useAui.d.ts +15 -0
  151. package/dist/useAui.d.ts.map +1 -0
  152. package/dist/useAui.js +21 -0
  153. package/dist/useAui.js.map +1 -0
  154. package/dist/useAuiEvent.d.ts +17 -0
  155. package/dist/useAuiEvent.d.ts.map +1 -0
  156. package/dist/useAuiEvent.js +52 -0
  157. package/dist/useAuiEvent.js.map +1 -0
  158. package/dist/useAuiState.d.ts +23 -0
  159. package/dist/useAuiState.d.ts.map +1 -0
  160. package/dist/useAuiState.js +42 -0
  161. package/dist/useAuiState.js.map +1 -0
  162. package/dist/useScrollLock.d.ts +3 -0
  163. package/dist/useScrollLock.d.ts.map +1 -0
  164. package/dist/useScrollLock.js +53 -0
  165. package/dist/useScrollLock.js.map +1 -0
  166. package/package.json +57 -6
  167. package/src/AuiIf.ts +34 -0
  168. package/src/AuiProvider.ts +90 -0
  169. package/src/__tests__/AuiIf.test.ts +54 -0
  170. package/src/__tests__/AuiProvider.test.ts +266 -0
  171. package/src/__tests__/byindex-shrink-race.test.ts +411 -0
  172. package/src/__tests__/dist-graph.test.ts +49 -0
  173. package/src/__tests__/fixtures.ts +51 -0
  174. package/src/__tests__/primitives-actionbar.test.ts +469 -0
  175. package/src/__tests__/primitives-attachment.test.ts +493 -0
  176. package/src/__tests__/primitives-content.test.ts +829 -0
  177. package/src/__tests__/primitives-keyed-items.test.ts +270 -0
  178. package/src/__tests__/primitives-keyed-messages.test.ts +264 -0
  179. package/src/__tests__/primitives-structural.test.ts +573 -0
  180. package/src/__tests__/primitives-suggestions.test.ts +374 -0
  181. package/src/__tests__/primitives-threadlist.test.ts +443 -0
  182. package/src/__tests__/primitives-tool-ui.test.ts +579 -0
  183. package/src/__tests__/primitives.test.ts +408 -0
  184. package/src/__tests__/useAuiEvent.test.ts +159 -0
  185. package/src/__tests__/useAuiState.test.ts +124 -0
  186. package/src/context.ts +31 -0
  187. package/src/index.ts +87 -0
  188. package/src/primitives/AttachmentByIndexProvider.ts +88 -0
  189. package/src/primitives/ChainOfThoughtPrimitiveAccordionTrigger.test.ts +105 -0
  190. package/src/primitives/ChainOfThoughtPrimitiveAccordionTrigger.ts +38 -0
  191. package/src/primitives/ChainOfThoughtPrimitiveParts.test.ts +254 -0
  192. package/src/primitives/ChainOfThoughtPrimitiveParts.ts +112 -0
  193. package/src/primitives/ComposerPrimitiveCancel.ts +41 -0
  194. package/src/primitives/ComposerPrimitiveInput.ts +70 -0
  195. package/src/primitives/ComposerPrimitiveSend.ts +39 -0
  196. package/src/primitives/MessageByIdProvider.ts +89 -0
  197. package/src/primitives/MessagePrimitiveParts.tool-interaction.test.ts +88 -0
  198. package/src/primitives/MessagePrimitiveParts.ts +167 -0
  199. package/src/primitives/PartByIndexProvider.ts +62 -0
  200. package/src/primitives/ThreadListItemPrimitiveRoot.ts +57 -0
  201. package/src/primitives/ThreadListItemPrimitiveTrigger.ts +74 -0
  202. package/src/primitives/ThreadListKeyboardNav.test.ts +298 -0
  203. package/src/primitives/ThreadListPrimitiveRoot.ts +18 -0
  204. package/src/primitives/ThreadPrimitiveMessages.ts +38 -0
  205. package/src/primitives/ThreadPrimitiveScrollToBottom.ts +65 -0
  206. package/src/primitives/ThreadPrimitiveViewport.ts +120 -0
  207. package/src/primitives/ThreadPrimitiveViewportFooter.test.ts +400 -0
  208. package/src/primitives/ThreadPrimitiveViewportFooter.ts +60 -0
  209. package/src/primitives/actionBar.ts +159 -0
  210. package/src/primitives/attachment.ts +76 -0
  211. package/src/primitives/attrDisabled.ts +6 -0
  212. package/src/primitives/branchPicker.ts +77 -0
  213. package/src/primitives/composerAttachments.ts +181 -0
  214. package/src/primitives/error.ts +36 -0
  215. package/src/primitives/lastValidCache.ts +12 -0
  216. package/src/primitives/message.ts +54 -0
  217. package/src/primitives/messageAttachments.ts +32 -0
  218. package/src/primitives/stableKeys.test.ts +19 -0
  219. package/src/primitives/stableKeys.ts +21 -0
  220. package/src/primitives/suggestions.ts +179 -0
  221. package/src/primitives/thread.ts +46 -0
  222. package/src/primitives/threadList.ts +163 -0
  223. package/src/primitives/threadListFocusGroup.ts +56 -0
  224. package/src/primitives/threadListStructural.ts +86 -0
  225. package/src/primitives/useComposerSendState.ts +13 -0
  226. package/src/primitives/viewportContext.ts +16 -0
  227. package/src/useAui.ts +18 -0
  228. package/src/useAuiEvent.ts +64 -0
  229. package/src/useAuiState.ts +55 -0
  230. package/src/useScrollLock.test.ts +207 -0
  231. package/src/useScrollLock.ts +79 -0
@@ -0,0 +1,57 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ mergeProps,
5
+ type SlotsType,
6
+ type VNodeChild,
7
+ } from "vue";
8
+ import { useAuiState } from "../useAuiState";
9
+ import {
10
+ provideThreadListItemFocus,
11
+ useThreadListCollection,
12
+ } from "./threadListFocusGroup";
13
+
14
+ export const ThreadListItemPrimitiveRoot = defineComponent({
15
+ name: "ThreadListItemPrimitiveRoot",
16
+ inheritAttrs: false,
17
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
18
+ setup(_, { attrs, slots }) {
19
+ const active = useAuiState(
20
+ (s) => s.threads.mainThreadId === s.threadListItem.id,
21
+ );
22
+ const focus = provideThreadListItemFocus();
23
+ const collection = useThreadListCollection();
24
+ const onKeydown = (event: KeyboardEvent) => {
25
+ if (event.defaultPrevented) return;
26
+ const trigger = focus.trigger.value;
27
+ if (
28
+ !trigger ||
29
+ event.target !== trigger ||
30
+ (event.key !== "ArrowDown" && event.key !== "ArrowUp")
31
+ ) {
32
+ return;
33
+ }
34
+ const triggers = collection?.getTriggers();
35
+ if (!triggers) return;
36
+ const next =
37
+ triggers[
38
+ triggers.indexOf(trigger) + (event.key === "ArrowDown" ? 1 : -1)
39
+ ];
40
+ if (!next) return;
41
+ next.focus();
42
+ event.preventDefault();
43
+ };
44
+ return () =>
45
+ h(
46
+ "div",
47
+ mergeProps(attrs, {
48
+ ...(active.value && {
49
+ "data-active": "true",
50
+ "aria-current": "true",
51
+ }),
52
+ onKeydown,
53
+ }),
54
+ slots.default?.(),
55
+ );
56
+ },
57
+ });
@@ -0,0 +1,74 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ mergeProps,
5
+ onScopeDispose,
6
+ type ComponentPublicInstance,
7
+ type SlotsType,
8
+ type VNodeChild,
9
+ } from "vue";
10
+ import { isAttrDisabled } from "./attrDisabled";
11
+ import { useAui } from "../useAui";
12
+ import { useAuiState } from "../useAuiState";
13
+ import {
14
+ useThreadListCollection,
15
+ useThreadListItemFocus,
16
+ } from "./threadListFocusGroup";
17
+
18
+ /**
19
+ * A button that switches to the current thread-list item's thread. Carries
20
+ * `data-active` and `aria-current` while that thread is the main one, for
21
+ * standalone use; when nested under `ThreadListItemPrimitiveRoot` the root
22
+ * stamps them too, diverging from React's root-only pattern to keep existing
23
+ * standalone consumers styled.
24
+ */
25
+ export const ThreadListItemPrimitiveTrigger = defineComponent({
26
+ name: "ThreadListItemPrimitiveTrigger",
27
+ inheritAttrs: false,
28
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
29
+ setup(_, { attrs, slots }) {
30
+ const aui = useAui();
31
+ const active = useAuiState(
32
+ (s) => s.threads.mainThreadId === s.threadListItem.id,
33
+ );
34
+ const collection = useThreadListCollection();
35
+ const focus = useThreadListItemFocus();
36
+ const key = Symbol();
37
+ let trigger: HTMLButtonElement | null = null;
38
+ let unregister: (() => void) | undefined;
39
+ const setTrigger = (element: Element | ComponentPublicInstance | null) => {
40
+ const next = element instanceof HTMLButtonElement ? element : null;
41
+ const previous = trigger;
42
+ if (previous === next) return;
43
+ unregister?.();
44
+ if (focus && (next || focus.trigger.value === previous)) {
45
+ focus.trigger.value = next;
46
+ }
47
+ trigger = next;
48
+ unregister = next ? collection?.registerTrigger(key, next) : undefined;
49
+ };
50
+ onScopeDispose(() => {
51
+ unregister?.();
52
+ if (focus?.trigger.value === trigger) focus.trigger.value = null;
53
+ });
54
+ const onClick = (event: MouseEvent) => {
55
+ if (event.defaultPrevented || isAttrDisabled(attrs)) return;
56
+ aui.threadListItem.switchTo();
57
+ };
58
+ return () =>
59
+ h(
60
+ "button",
61
+ mergeProps(attrs, {
62
+ ref: setTrigger,
63
+ type: "button",
64
+ disabled: isAttrDisabled(attrs),
65
+ ...(active.value && {
66
+ "data-active": "true",
67
+ "aria-current": "true",
68
+ }),
69
+ onClick,
70
+ }),
71
+ slots.default?.(),
72
+ );
73
+ },
74
+ });
@@ -0,0 +1,298 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ createApp,
4
+ defineComponent,
5
+ h,
6
+ nextTick,
7
+ ref,
8
+ type Component,
9
+ } from "vue";
10
+ import { AuiConfig } from "@assistant-ui/store/client";
11
+ import { RuntimeAdapter } from "@assistant-ui/core/store";
12
+ import type { ExternalStoreAdapter } from "@assistant-ui/core";
13
+ import {
14
+ AssistantRuntimeImpl,
15
+ ExternalStoreRuntimeCore,
16
+ } from "@assistant-ui/core/internal";
17
+ import { AuiProvider } from "../AuiProvider";
18
+ import { ThreadListItemByIndexProvider } from "./threadList";
19
+ import { ThreadListItemPrimitiveRoot } from "./ThreadListItemPrimitiveRoot";
20
+ import { ThreadListItemPrimitiveTrigger } from "./ThreadListItemPrimitiveTrigger";
21
+ import { ThreadListPrimitiveRoot } from "./ThreadListPrimitiveRoot";
22
+
23
+ type Message = { id: string; role: "user"; text: string };
24
+
25
+ const createRuntime = () => {
26
+ let mainThreadId = "first";
27
+ let core!: ExternalStoreRuntimeCore;
28
+ const onSwitchToThread = vi.fn((threadId: string) => {
29
+ mainThreadId = threadId;
30
+ core.setAdapter(createAdapter());
31
+ });
32
+ const createAdapter = (): ExternalStoreAdapter<Message> => ({
33
+ messages: [],
34
+ convertMessage: (message) => ({
35
+ id: message.id,
36
+ role: message.role,
37
+ content: [{ type: "text", text: message.text }],
38
+ }),
39
+ onNew: async () => {},
40
+ adapters: {
41
+ threadList: {
42
+ threadId: mainThreadId,
43
+ threads: ["first", "second", "third"].map((id) => ({
44
+ status: "regular" as const,
45
+ id,
46
+ title: id,
47
+ })),
48
+ onSwitchToThread,
49
+ },
50
+ },
51
+ });
52
+ core = new ExternalStoreRuntimeCore(createAdapter());
53
+ return {
54
+ runtime: new AssistantRuntimeImpl(core),
55
+ onSwitchToThread,
56
+ };
57
+ };
58
+
59
+ const mount = (runtime: AssistantRuntimeImpl, view: Component) => {
60
+ const app = createApp(
61
+ defineComponent({
62
+ setup: () => () =>
63
+ h(
64
+ AuiProvider,
65
+ { config: AuiConfig({ threads: RuntimeAdapter(runtime) }) },
66
+ { default: () => h(view) },
67
+ ),
68
+ }),
69
+ );
70
+ const el = document.createElement("div");
71
+ document.body.append(el);
72
+ app.mount(el);
73
+ return {
74
+ el,
75
+ unmount: () => {
76
+ app.unmount();
77
+ el.remove();
78
+ },
79
+ };
80
+ };
81
+
82
+ const keydown = (element: HTMLElement, key: string) =>
83
+ element.dispatchEvent(
84
+ new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key }),
85
+ );
86
+
87
+ const buttons = (el: HTMLElement) => [
88
+ ...el.querySelectorAll<HTMLButtonElement>("button"),
89
+ ];
90
+
91
+ describe("thread list keyboard navigation", () => {
92
+ it("moves through triggers in DOM order without wrapping", async () => {
93
+ const { runtime } = createRuntime();
94
+ const View = defineComponent({
95
+ setup: () => () =>
96
+ h(
97
+ ThreadListPrimitiveRoot,
98
+ { class: "list" },
99
+ {
100
+ default: () =>
101
+ [0, 1, 2].map((index) =>
102
+ h(
103
+ ThreadListItemByIndexProvider,
104
+ { index, key: index },
105
+ {
106
+ default: () =>
107
+ h(
108
+ ThreadListItemPrimitiveRoot,
109
+ { class: "item" },
110
+ {
111
+ default: () =>
112
+ h(ThreadListItemPrimitiveTrigger, null, {
113
+ default: () => `item ${index}`,
114
+ }),
115
+ },
116
+ ),
117
+ },
118
+ ),
119
+ ),
120
+ },
121
+ ),
122
+ });
123
+ const { el, unmount } = mount(runtime, View);
124
+
125
+ await nextTick();
126
+ const [first, second, third] = buttons(el);
127
+ const list = el.querySelector<HTMLElement>(".list")!;
128
+ const [firstItem, , thirdItem] = el.querySelectorAll<HTMLElement>(".item");
129
+ list.insertBefore(thirdItem!, firstItem!);
130
+
131
+ third!.focus();
132
+ expect(keydown(third!, "ArrowDown")).toBe(false);
133
+ expect(document.activeElement).toBe(first);
134
+ expect(keydown(first!, "ArrowDown")).toBe(false);
135
+ expect(document.activeElement).toBe(second);
136
+ expect(keydown(second!, "ArrowDown")).toBe(true);
137
+ expect(document.activeElement).toBe(second);
138
+ expect(keydown(first!, "ArrowUp")).toBe(false);
139
+ expect(document.activeElement).toBe(third);
140
+ expect(keydown(third!, "ArrowUp")).toBe(true);
141
+ expect(document.activeElement).toBe(third);
142
+
143
+ unmount();
144
+ });
145
+
146
+ it("lets a caller veto collection navigation", async () => {
147
+ const { runtime } = createRuntime();
148
+ const View = defineComponent({
149
+ setup: () => () =>
150
+ h(ThreadListPrimitiveRoot, null, {
151
+ default: () =>
152
+ [0, 1].map((index) =>
153
+ h(
154
+ ThreadListItemByIndexProvider,
155
+ { index, key: index },
156
+ {
157
+ default: () =>
158
+ h(
159
+ ThreadListItemPrimitiveRoot,
160
+ {
161
+ onKeydown: (event: KeyboardEvent) =>
162
+ event.preventDefault(),
163
+ },
164
+ {
165
+ default: () =>
166
+ h(ThreadListItemPrimitiveTrigger, null, {
167
+ default: () => `item ${index}`,
168
+ }),
169
+ },
170
+ ),
171
+ },
172
+ ),
173
+ ),
174
+ }),
175
+ });
176
+ const { el, unmount } = mount(runtime, View);
177
+
178
+ await nextTick();
179
+ const [first] = buttons(el);
180
+ first!.focus();
181
+ expect(keydown(first!, "ArrowDown")).toBe(false);
182
+ expect(document.activeElement).toBe(first);
183
+
184
+ unmount();
185
+ });
186
+
187
+ it("tracks the main thread on the item root", async () => {
188
+ const { runtime, onSwitchToThread } = createRuntime();
189
+ const View = defineComponent({
190
+ setup: () => () =>
191
+ h(ThreadListPrimitiveRoot, null, {
192
+ default: () =>
193
+ [0, 1].map((index) =>
194
+ h(
195
+ ThreadListItemByIndexProvider,
196
+ { index, key: index },
197
+ {
198
+ default: () =>
199
+ h(
200
+ ThreadListItemPrimitiveRoot,
201
+ { class: "item" },
202
+ {
203
+ default: () =>
204
+ h(ThreadListItemPrimitiveTrigger, null, {
205
+ default: () => `item ${index}`,
206
+ }),
207
+ },
208
+ ),
209
+ },
210
+ ),
211
+ ),
212
+ }),
213
+ });
214
+ const { el, unmount } = mount(runtime, View);
215
+
216
+ await nextTick();
217
+ const items = el.querySelectorAll<HTMLElement>(".item");
218
+ expect(items[0]!.getAttribute("data-active")).toBe("true");
219
+ expect(items[0]!.getAttribute("aria-current")).toBe("true");
220
+ buttons(el)[1]!.click();
221
+ await vi.waitFor(() => {
222
+ expect(onSwitchToThread).toHaveBeenCalledWith("second");
223
+ expect(items[1]!.getAttribute("data-active")).toBe("true");
224
+ expect(items[1]!.getAttribute("aria-current")).toBe("true");
225
+ });
226
+ expect(items[0]!.hasAttribute("data-active")).toBe(false);
227
+ expect(items[0]!.hasAttribute("aria-current")).toBe(false);
228
+
229
+ unmount();
230
+ });
231
+
232
+ it("keeps the trigger working without either root", async () => {
233
+ const { runtime, onSwitchToThread } = createRuntime();
234
+ const View = defineComponent({
235
+ setup: () => () =>
236
+ [0, 1].map((index) =>
237
+ h(
238
+ ThreadListItemByIndexProvider,
239
+ { index, key: index },
240
+ {
241
+ default: () =>
242
+ h(ThreadListItemPrimitiveTrigger, null, {
243
+ default: () => `item ${index}`,
244
+ }),
245
+ },
246
+ ),
247
+ ),
248
+ });
249
+ const { el, unmount } = mount(runtime, View);
250
+
251
+ await nextTick();
252
+ buttons(el)[1]!.click();
253
+ await vi.waitFor(() => {
254
+ expect(onSwitchToThread).toHaveBeenCalledWith("second");
255
+ });
256
+
257
+ unmount();
258
+ });
259
+
260
+ it("unregisters a trigger when its item unmounts", async () => {
261
+ const { runtime } = createRuntime();
262
+ const showSecond = ref(true);
263
+ const View = defineComponent({
264
+ setup: () => () =>
265
+ h(ThreadListPrimitiveRoot, null, {
266
+ default: () =>
267
+ [0, 1, 2]
268
+ .filter((index) => index !== 1 || showSecond.value)
269
+ .map((index) =>
270
+ h(
271
+ ThreadListItemByIndexProvider,
272
+ { index, key: index },
273
+ {
274
+ default: () =>
275
+ h(ThreadListItemPrimitiveRoot, null, {
276
+ default: () =>
277
+ h(ThreadListItemPrimitiveTrigger, null, {
278
+ default: () => `item ${index}`,
279
+ }),
280
+ }),
281
+ },
282
+ ),
283
+ ),
284
+ }),
285
+ });
286
+ const { el, unmount } = mount(runtime, View);
287
+
288
+ await nextTick();
289
+ showSecond.value = false;
290
+ await nextTick();
291
+ const [first, third] = buttons(el);
292
+ first!.focus();
293
+ expect(keydown(first!, "ArrowDown")).toBe(false);
294
+ expect(document.activeElement).toBe(third);
295
+
296
+ unmount();
297
+ });
298
+ });
@@ -0,0 +1,18 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ mergeProps,
5
+ type SlotsType,
6
+ type VNodeChild,
7
+ } from "vue";
8
+ import { provideThreadListCollection } from "./threadListFocusGroup";
9
+
10
+ export const ThreadListPrimitiveRoot = defineComponent({
11
+ name: "ThreadListPrimitiveRoot",
12
+ inheritAttrs: false,
13
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
14
+ setup(_, { attrs, slots }) {
15
+ provideThreadListCollection();
16
+ return () => h("div", mergeProps(attrs), slots.default?.());
17
+ },
18
+ });
@@ -0,0 +1,38 @@
1
+ import { defineComponent, h, type SlotsType, type VNodeChild } from "vue";
2
+ import type {} from "@assistant-ui/core/store";
3
+ import { useAuiState } from "../useAuiState";
4
+ import { MessageByIdProvider } from "./MessageByIdProvider";
5
+ import { useStableKeys } from "./stableKeys";
6
+
7
+ /**
8
+ * Renders the default slot once per message in the current thread, each
9
+ * instance scoped to its message through {@link MessageByIdProvider} and
10
+ * keyed by the message id. A stable id keeps its row component instance across
11
+ * updates; a new id remounts the row. Because rows render inside this
12
+ * component, an outer `<TransitionGroup>` cannot animate individual messages.
13
+ *
14
+ * @example
15
+ * ```html
16
+ * <ThreadPrimitiveMessages>
17
+ * <ChatMessage />
18
+ * </ThreadPrimitiveMessages>
19
+ * ```
20
+ */
21
+ export const ThreadPrimitiveMessages = defineComponent({
22
+ name: "ThreadPrimitiveMessages",
23
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
24
+ setup(_, { slots }) {
25
+ const messages = useAuiState((s) => s.thread.messages);
26
+ const ids = useStableKeys(() =>
27
+ messages.value.map((message) => message.id),
28
+ );
29
+ return () =>
30
+ ids.value.map((id) =>
31
+ h(
32
+ MessageByIdProvider,
33
+ { id, key: id },
34
+ { default: () => slots.default?.() },
35
+ ),
36
+ );
37
+ },
38
+ });
@@ -0,0 +1,65 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ inject,
5
+ mergeProps,
6
+ type PropType,
7
+ type SlotsType,
8
+ type VNodeChild,
9
+ } from "vue";
10
+ import { isDevelopment } from "@assistant-ui/core/store/internal";
11
+ import { isAttrDisabled } from "./attrDisabled";
12
+ import { viewportInjectionKey } from "./viewportContext";
13
+
14
+ let warnedOutsideViewport = false;
15
+
16
+ export const clearScrollToBottomWarningForTesting = () => {
17
+ warnedOutsideViewport = false;
18
+ };
19
+
20
+ /**
21
+ * A button that scrolls the surrounding {@link ThreadPrimitiveViewport} to
22
+ * the bottom. Disabled while the viewport is already at the bottom, or when
23
+ * no viewport provides the channel.
24
+ */
25
+ export const ThreadPrimitiveScrollToBottom = defineComponent({
26
+ name: "ThreadPrimitiveScrollToBottom",
27
+ inheritAttrs: false,
28
+ props: {
29
+ behavior: {
30
+ type: String as PropType<ScrollBehavior>,
31
+ default: "auto",
32
+ },
33
+ },
34
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
35
+ setup(props, { attrs, slots }) {
36
+ const viewport = inject(viewportInjectionKey, null);
37
+ if (isDevelopment && !viewport && !warnedOutsideViewport) {
38
+ warnedOutsideViewport = true;
39
+ console.warn(
40
+ "ThreadPrimitiveScrollToBottom: no surrounding ThreadPrimitiveViewport provides the scroll channel; the button stays disabled. Place it inside the viewport.",
41
+ );
42
+ }
43
+ const onClick = (event: MouseEvent) => {
44
+ if (
45
+ event.defaultPrevented ||
46
+ !viewport ||
47
+ viewport.isAtBottom.value ||
48
+ isAttrDisabled(attrs)
49
+ )
50
+ return;
51
+ viewport.scrollToBottom(props.behavior);
52
+ };
53
+ return () =>
54
+ h(
55
+ "button",
56
+ mergeProps(attrs, {
57
+ type: "button",
58
+ disabled:
59
+ !viewport || viewport.isAtBottom.value || isAttrDisabled(attrs),
60
+ onClick,
61
+ }),
62
+ slots.default?.(),
63
+ );
64
+ },
65
+ });
@@ -0,0 +1,120 @@
1
+ import {
2
+ defineComponent,
3
+ h,
4
+ onMounted,
5
+ onScopeDispose,
6
+ provide,
7
+ shallowRef,
8
+ watch,
9
+ type SlotsType,
10
+ type VNodeChild,
11
+ } from "vue";
12
+ import type {} from "@assistant-ui/core/store";
13
+ import { useAuiEvent } from "../useAuiEvent";
14
+ import { useAuiState } from "../useAuiState";
15
+ import { createThreadViewportAutoScroll } from "@assistant-ui/store/client";
16
+ import { viewportInjectionKey } from "./viewportContext";
17
+
18
+ /**
19
+ * A scrollable container that keeps the thread pinned to the bottom: content
20
+ * growth scrolls back down while the user sits at the bottom, a run start
21
+ * scrolls down, and scrolling up unpins until the user returns to the bottom.
22
+ * The four options mirror the React hook and are independent: `autoScroll`
23
+ * covers follow-on-content-growth, and the other three gate the
24
+ * first-messages, run-start, and thread-switch scrolls. Provides the
25
+ * scroll-to-bottom channel that {@link ThreadPrimitiveScrollToBottom} drives;
26
+ * the React viewport's top-anchor system stays in the React viewport store
27
+ * and is not ported.
28
+ */
29
+ export const ThreadPrimitiveViewport = defineComponent({
30
+ name: "ThreadPrimitiveViewport",
31
+ props: {
32
+ autoScroll: {
33
+ type: Boolean,
34
+ default: true,
35
+ },
36
+ scrollToBottomOnInitialize: {
37
+ type: Boolean,
38
+ default: true,
39
+ },
40
+ scrollToBottomOnRunStart: {
41
+ type: Boolean,
42
+ default: true,
43
+ },
44
+ scrollToBottomOnThreadSwitch: {
45
+ type: Boolean,
46
+ default: true,
47
+ },
48
+ },
49
+ slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
50
+ setup(props, { slots }) {
51
+ const divRef = shallowRef<HTMLElement | null>(null);
52
+ const contentInsetEntries = new Map<symbol, number>();
53
+ const isAtBottom = shallowRef(true);
54
+ const autoScroll = createThreadViewportAutoScroll({
55
+ getOptions: () => ({
56
+ autoScroll: props.autoScroll,
57
+ scrollToBottomOnInitialize: props.scrollToBottomOnInitialize,
58
+ scrollToBottomOnRunStart: props.scrollToBottomOnRunStart,
59
+ scrollToBottomOnThreadSwitch: props.scrollToBottomOnThreadSwitch,
60
+ }),
61
+ onAtBottomChange: (value) => {
62
+ isAtBottom.value = value;
63
+ },
64
+ });
65
+
66
+ const updateContentInset = () => {
67
+ let total = 0;
68
+ for (const height of contentInsetEntries.values()) total += height;
69
+ autoScroll.setContentInset(total);
70
+ };
71
+
72
+ const registerContentInset = () => {
73
+ const id = Symbol();
74
+ contentInsetEntries.set(id, 0);
75
+
76
+ return {
77
+ setHeight: (height: number) => {
78
+ if (contentInsetEntries.get(id) === height) return;
79
+ contentInsetEntries.set(id, height);
80
+ updateContentInset();
81
+ },
82
+ unregister: () => {
83
+ if (!contentInsetEntries.delete(id)) return;
84
+ updateContentInset();
85
+ },
86
+ };
87
+ };
88
+
89
+ let detach: (() => void) | undefined;
90
+ onMounted(() => {
91
+ const div = divRef.value;
92
+ if (!div) return;
93
+ detach = autoScroll.attach(div);
94
+ });
95
+ onScopeDispose(() => {
96
+ detach?.();
97
+ autoScroll.dispose();
98
+ });
99
+
100
+ const hasMessages = useAuiState((s) => s.thread.messages.length > 0);
101
+ watch(
102
+ [hasMessages, () => props.scrollToBottomOnInitialize],
103
+ ([has]) => autoScroll.setHasMessages(has),
104
+ { immediate: true },
105
+ );
106
+
107
+ useAuiEvent("thread.runStart", () => autoScroll.runStarted());
108
+
109
+ useAuiEvent("threads.selectionChanged", () => autoScroll.threadSwitched());
110
+
111
+ provide(viewportInjectionKey, {
112
+ isAtBottom,
113
+ scrollToBottom: (behavior: ScrollBehavior = "auto") =>
114
+ autoScroll.scrollToBottom(behavior),
115
+ registerContentInset,
116
+ });
117
+
118
+ return () => h("div", { ref: divRef }, slots.default?.());
119
+ },
120
+ });