@assistant-ui/vue 0.0.0-stage → 0.0.1

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 +50 -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 +23 -0
  67. package/dist/primitives/ThreadPrimitiveMessages.d.ts.map +1 -0
  68. package/dist/primitives/ThreadPrimitiveMessages.js +37 -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 +174 -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 +64 -3
  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 +749 -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 +361 -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 +64 -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 +166 -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 +40 -0
  205. package/src/primitives/ThreadPrimitiveScrollToBottom.ts +65 -0
  206. package/src/primitives/ThreadPrimitiveViewport.ts +233 -0
  207. package/src/primitives/ThreadPrimitiveViewportFooter.test.ts +305 -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,469 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { createApp, defineComponent, h, nextTick, type Component } from "vue";
3
+ import { flushTapSync } from "@assistant-ui/tap";
4
+ import { AuiConfig } from "@assistant-ui/store/client";
5
+ import { RuntimeAdapter } from "@assistant-ui/core/store";
6
+ import type { ExternalStoreAdapter } from "@assistant-ui/core";
7
+ import {
8
+ AssistantRuntimeImpl,
9
+ ExternalStoreRuntimeCore,
10
+ } from "@assistant-ui/core/internal";
11
+ import { AuiProvider } from "../AuiProvider";
12
+ import { useAuiState } from "../useAuiState";
13
+ import { ThreadPrimitiveMessages } from "../primitives/ThreadPrimitiveMessages";
14
+ import { ComposerPrimitiveInput } from "../primitives/ComposerPrimitiveInput";
15
+ import { ComposerPrimitiveSend } from "../primitives/ComposerPrimitiveSend";
16
+ import { ComposerPrimitiveCancel } from "../primitives/ComposerPrimitiveCancel";
17
+ import {
18
+ ActionBarPrimitiveCopy,
19
+ ActionBarPrimitiveEdit,
20
+ ActionBarPrimitiveReload,
21
+ } from "../primitives/actionBar";
22
+ import {
23
+ BranchPickerPrimitiveCount,
24
+ BranchPickerPrimitiveNext,
25
+ BranchPickerPrimitiveNumber,
26
+ BranchPickerPrimitivePrevious,
27
+ } from "../primitives/branchPicker";
28
+
29
+ afterEach(() => {
30
+ vi.restoreAllMocks();
31
+ // vi.restoreAllMocks does not undo hand-installed property descriptors.
32
+ Reflect.deleteProperty(navigator, "clipboard");
33
+ });
34
+
35
+ type DemoMessage = { id: string; role: "user" | "assistant"; text: string };
36
+
37
+ const convertDemoMessage = (message: DemoMessage) => ({
38
+ id: message.id,
39
+ role: message.role,
40
+ content: [{ type: "text" as const, text: message.text }],
41
+ });
42
+
43
+ let nextId = 0;
44
+ const freshId = (prefix: string) => `${prefix}-${nextId++}`;
45
+
46
+ const createEditableRuntime = () => {
47
+ let messages: readonly DemoMessage[] = [];
48
+ let isRunning = false;
49
+ const makeAdapter = (): ExternalStoreAdapter<DemoMessage> => ({
50
+ messages,
51
+ isRunning,
52
+ convertMessage: convertDemoMessage,
53
+ onNew: async () => {},
54
+ setMessages: (next) => {
55
+ messages = next;
56
+ sync();
57
+ },
58
+ onEdit: async (message) => {
59
+ const text = message.content
60
+ .map((part) => (part.type === "text" ? part.text : ""))
61
+ .join("");
62
+ messages = [
63
+ { id: freshId("user"), role: "user", text },
64
+ { id: freshId("assistant"), role: "assistant", text: `Echo: ${text}` },
65
+ ];
66
+ sync();
67
+ },
68
+ onReload: async () => {
69
+ messages = [
70
+ ...messages.slice(0, -1),
71
+ {
72
+ id: freshId("assistant"),
73
+ role: "assistant",
74
+ text: `${messages.at(-1)!.text} (again)`,
75
+ },
76
+ ];
77
+ sync();
78
+ },
79
+ });
80
+ const core = new ExternalStoreRuntimeCore(makeAdapter());
81
+ const runtime = new AssistantRuntimeImpl(core);
82
+ const sync = () => core.setAdapter(makeAdapter());
83
+ const seed = (next: readonly DemoMessage[]) => {
84
+ messages = next;
85
+ sync();
86
+ };
87
+ const setRunning = (value: boolean) => {
88
+ isRunning = value;
89
+ sync();
90
+ };
91
+ return { runtime, seed, setRunning };
92
+ };
93
+
94
+ const MessageView = defineComponent({
95
+ setup() {
96
+ const role = useAuiState((s) => s.message.role);
97
+ const text = useAuiState((s) => {
98
+ const part = s.message.content[0];
99
+ return part?.type === "text" ? part.text : "";
100
+ });
101
+ const copiedProbe = useAuiState((s) => s.message.isCopied);
102
+ return () =>
103
+ h("li", { class: "msg", "data-role": role.value }, [
104
+ h("span", { class: "probe" }, String(copiedProbe.value)),
105
+ h("span", { class: "text" }, text.value),
106
+ h(ActionBarPrimitiveEdit, { class: "edit" }, { default: () => "Edit" }),
107
+ h(
108
+ ActionBarPrimitiveCopy,
109
+ { class: "copy", copiedDuration: 300 },
110
+ {
111
+ default: () => "Copy",
112
+ },
113
+ ),
114
+ h(
115
+ ActionBarPrimitiveReload,
116
+ { class: "reload" },
117
+ { default: () => "Reload" },
118
+ ),
119
+ h(
120
+ BranchPickerPrimitivePrevious,
121
+ { class: "prev" },
122
+ { default: () => "<" },
123
+ ),
124
+ h("span", { class: "pos" }, [
125
+ h(BranchPickerPrimitiveNumber),
126
+ "/",
127
+ h(BranchPickerPrimitiveCount),
128
+ ]),
129
+ h(BranchPickerPrimitiveNext, { class: "next" }, { default: () => ">" }),
130
+ h(ComposerPrimitiveInput, { class: "editor" }),
131
+ h(ComposerPrimitiveSend, { class: "send" }, { default: () => "Save" }),
132
+ h(
133
+ ComposerPrimitiveCancel,
134
+ { class: "cancel" },
135
+ { default: () => "Discard" },
136
+ ),
137
+ ]);
138
+ },
139
+ });
140
+
141
+ const mountChat = (runtime: AssistantRuntimeImpl, view?: Component) => {
142
+ const app = createApp(
143
+ defineComponent({
144
+ setup: () => () =>
145
+ h(
146
+ AuiProvider,
147
+ { config: AuiConfig({ threads: RuntimeAdapter(runtime) }) },
148
+ {
149
+ default: () =>
150
+ h(ThreadPrimitiveMessages, null, {
151
+ default: () => h(view ?? MessageView),
152
+ }),
153
+ },
154
+ ),
155
+ }),
156
+ );
157
+ const el = document.createElement("div");
158
+ app.mount(el);
159
+ return { el, unmount: () => app.unmount() };
160
+ };
161
+
162
+ const userItem = (el: HTMLElement) =>
163
+ el.querySelector<HTMLElement>('li[data-role="user"]')!;
164
+ const assistantItem = (el: HTMLElement) =>
165
+ el.querySelector<HTMLElement>('li[data-role="assistant"]')!;
166
+ const q = <T extends HTMLElement>(scope: HTMLElement, selector: string) =>
167
+ scope.querySelector<T>(selector)!;
168
+
169
+ describe("edit flow and branches", () => {
170
+ it("edits a user message into a second branch and switches between them", async () => {
171
+ const { runtime, seed } = createEditableRuntime();
172
+ const { el, unmount } = mountChat(runtime);
173
+
174
+ flushTapSync(() =>
175
+ seed([
176
+ { id: "u1", role: "user", text: "hello" },
177
+ { id: "a1", role: "assistant", text: "Echo: hello" },
178
+ ]),
179
+ );
180
+ await vi.waitFor(async () => {
181
+ await nextTick();
182
+ expect(el.querySelectorAll("li.msg")).toHaveLength(2);
183
+ });
184
+
185
+ const user = userItem(el);
186
+ expect(q<HTMLTextAreaElement>(user, "textarea.editor").value).toBe("");
187
+
188
+ q(user, "button.edit").click();
189
+ await vi.waitFor(async () => {
190
+ await nextTick();
191
+ expect(q<HTMLTextAreaElement>(user, "textarea.editor").value).toBe(
192
+ "hello",
193
+ );
194
+ });
195
+ expect(q<HTMLButtonElement>(user, "button.edit").disabled).toBe(true);
196
+
197
+ const editor = q<HTMLTextAreaElement>(user, "textarea.editor");
198
+ editor.value = "goodbye";
199
+ flushTapSync(() => editor.dispatchEvent(new Event("input")));
200
+ q(user, "button.send").click();
201
+
202
+ await vi.waitFor(async () => {
203
+ await nextTick();
204
+ expect(q(userItem(el), "span.text").textContent).toBe("goodbye");
205
+ });
206
+ await vi.waitFor(async () => {
207
+ await nextTick();
208
+ expect(q(userItem(el), "span.pos").textContent).toBe("2/2");
209
+ });
210
+ expect(q(assistantItem(el), "span.text").textContent).toBe("Echo: goodbye");
211
+
212
+ const prev = q<HTMLButtonElement>(userItem(el), "button.prev");
213
+ expect(prev.disabled).toBe(false);
214
+ expect(q<HTMLButtonElement>(userItem(el), "button.next").disabled).toBe(
215
+ true,
216
+ );
217
+
218
+ prev.click();
219
+ await vi.waitFor(async () => {
220
+ await nextTick();
221
+ expect(q(userItem(el), "span.text").textContent).toBe("hello");
222
+ });
223
+ expect(q(userItem(el), "span.pos").textContent).toBe("1/2");
224
+
225
+ q<HTMLButtonElement>(userItem(el), "button.next").click();
226
+ await vi.waitFor(async () => {
227
+ await nextTick();
228
+ expect(q(userItem(el), "span.text").textContent).toBe("goodbye");
229
+ });
230
+
231
+ unmount();
232
+ });
233
+
234
+ it("blocks branch switching while a run is in flight", async () => {
235
+ const { runtime, seed, setRunning } = createEditableRuntime();
236
+ const { el, unmount } = mountChat(runtime);
237
+
238
+ flushTapSync(() =>
239
+ seed([
240
+ { id: "u1", role: "user", text: "hello" },
241
+ { id: "a1", role: "assistant", text: "Echo: hello" },
242
+ ]),
243
+ );
244
+ await vi.waitFor(async () => {
245
+ await nextTick();
246
+ expect(el.querySelectorAll("li.msg")).toHaveLength(2);
247
+ });
248
+
249
+ const user = userItem(el);
250
+ q(user, "button.edit").click();
251
+ await vi.waitFor(async () => {
252
+ await nextTick();
253
+ expect(q<HTMLTextAreaElement>(user, "textarea.editor").value).toBe(
254
+ "hello",
255
+ );
256
+ });
257
+ const editor = q<HTMLTextAreaElement>(user, "textarea.editor");
258
+ editor.value = "goodbye";
259
+ flushTapSync(() => editor.dispatchEvent(new Event("input")));
260
+ q(user, "button.send").click();
261
+ await vi.waitFor(async () => {
262
+ await nextTick();
263
+ expect(q(userItem(el), "span.pos").textContent).toBe("2/2");
264
+ });
265
+
266
+ flushTapSync(() => setRunning(true));
267
+ await vi.waitFor(async () => {
268
+ await nextTick();
269
+ expect(q<HTMLButtonElement>(userItem(el), "button.prev").disabled).toBe(
270
+ true,
271
+ );
272
+ });
273
+
274
+ unmount();
275
+ });
276
+
277
+ it("discards an edit through the cancel button", async () => {
278
+ const { runtime, seed } = createEditableRuntime();
279
+ const { el, unmount } = mountChat(runtime);
280
+
281
+ flushTapSync(() => seed([{ id: "u1", role: "user", text: "hello" }]));
282
+ await vi.waitFor(async () => {
283
+ await nextTick();
284
+ expect(el.querySelectorAll("li.msg")).toHaveLength(1);
285
+ });
286
+
287
+ const user = userItem(el);
288
+ q(user, "button.edit").click();
289
+ await vi.waitFor(async () => {
290
+ await nextTick();
291
+ expect(q<HTMLTextAreaElement>(user, "textarea.editor").value).toBe(
292
+ "hello",
293
+ );
294
+ });
295
+
296
+ q(user, "button.cancel").click();
297
+ await vi.waitFor(async () => {
298
+ await nextTick();
299
+ expect(q<HTMLTextAreaElement>(user, "textarea.editor").value).toBe("");
300
+ });
301
+ expect(q<HTMLButtonElement>(user, "button.edit").disabled).toBe(false);
302
+ expect(q(user, "span.text").textContent).toBe("hello");
303
+
304
+ unmount();
305
+ });
306
+
307
+ it("reloads an assistant message into a second branch", async () => {
308
+ const { runtime, seed } = createEditableRuntime();
309
+ const { el, unmount } = mountChat(runtime);
310
+
311
+ flushTapSync(() =>
312
+ seed([
313
+ { id: "u1", role: "user", text: "hello" },
314
+ { id: "a1", role: "assistant", text: "Echo: hello" },
315
+ ]),
316
+ );
317
+ await vi.waitFor(async () => {
318
+ await nextTick();
319
+ expect(el.querySelectorAll("li.msg")).toHaveLength(2);
320
+ });
321
+
322
+ expect(q<HTMLButtonElement>(userItem(el), "button.reload").disabled).toBe(
323
+ true,
324
+ );
325
+ const reload = q<HTMLButtonElement>(assistantItem(el), "button.reload");
326
+ expect(reload.disabled).toBe(false);
327
+
328
+ reload.click();
329
+ await vi.waitFor(async () => {
330
+ await nextTick();
331
+ expect(q(assistantItem(el), "span.text").textContent).toBe(
332
+ "Echo: hello (again)",
333
+ );
334
+ });
335
+ expect(q(assistantItem(el), "span.pos").textContent).toBe("2/2");
336
+
337
+ unmount();
338
+ });
339
+
340
+ it("drops a late copy completion after the branch under the index changes", async () => {
341
+ let resolveWrite!: () => void;
342
+ const writeText = vi.fn(
343
+ () =>
344
+ new Promise<void>((resolve) => {
345
+ resolveWrite = resolve;
346
+ }),
347
+ );
348
+ Object.defineProperty(navigator, "clipboard", {
349
+ value: { writeText },
350
+ configurable: true,
351
+ });
352
+
353
+ const { runtime, seed } = createEditableRuntime();
354
+ const { el, unmount } = mountChat(runtime);
355
+
356
+ flushTapSync(() =>
357
+ seed([
358
+ { id: "u1", role: "user", text: "hello" },
359
+ { id: "a1", role: "assistant", text: "Echo: hello" },
360
+ ]),
361
+ );
362
+ await vi.waitFor(async () => {
363
+ await nextTick();
364
+ expect(el.querySelectorAll("li.msg")).toHaveLength(2);
365
+ });
366
+
367
+ const user = userItem(el);
368
+ q(user, "button.copy").click();
369
+ expect(writeText).toHaveBeenCalledWith("hello");
370
+
371
+ q(user, "button.edit").click();
372
+ await vi.waitFor(async () => {
373
+ await nextTick();
374
+ expect(q<HTMLTextAreaElement>(user, "textarea.editor").value).toBe(
375
+ "hello",
376
+ );
377
+ });
378
+ const editor = q<HTMLTextAreaElement>(user, "textarea.editor");
379
+ editor.value = "goodbye";
380
+ flushTapSync(() => editor.dispatchEvent(new Event("input")));
381
+ q(user, "button.send").click();
382
+ await vi.waitFor(async () => {
383
+ await nextTick();
384
+ expect(q(userItem(el), "span.text").textContent).toBe("goodbye");
385
+ });
386
+
387
+ resolveWrite();
388
+ await new Promise((resolve) => setTimeout(resolve, 30));
389
+ expect(q(userItem(el), "button.copy").hasAttribute("data-copied")).toBe(
390
+ false,
391
+ );
392
+
393
+ unmount();
394
+ });
395
+
396
+ it("stays inert when the clipboard API is unavailable", async () => {
397
+ Object.defineProperty(navigator, "clipboard", {
398
+ value: undefined,
399
+ configurable: true,
400
+ });
401
+ const { runtime, seed } = createEditableRuntime();
402
+ const { el, unmount } = mountChat(runtime);
403
+
404
+ flushTapSync(() =>
405
+ seed([
406
+ { id: "u1", role: "user", text: "hello" },
407
+ { id: "a1", role: "assistant", text: "Echo: hello" },
408
+ ]),
409
+ );
410
+ await vi.waitFor(async () => {
411
+ await nextTick();
412
+ expect(el.querySelectorAll("li.msg")).toHaveLength(2);
413
+ });
414
+
415
+ expect(() =>
416
+ q<HTMLButtonElement>(assistantItem(el), "button.copy").click(),
417
+ ).not.toThrow();
418
+ await new Promise((resolve) => setTimeout(resolve, 30));
419
+ expect(
420
+ q(assistantItem(el), "button.copy").hasAttribute("data-copied"),
421
+ ).toBe(false);
422
+
423
+ unmount();
424
+ });
425
+
426
+ it("copies message text and flags data-copied for the configured window", async () => {
427
+ const writeText = vi.fn(() => Promise.resolve());
428
+ Object.defineProperty(navigator, "clipboard", {
429
+ value: { writeText },
430
+ configurable: true,
431
+ });
432
+
433
+ const { runtime, seed } = createEditableRuntime();
434
+ const { el, unmount } = mountChat(runtime);
435
+
436
+ flushTapSync(() =>
437
+ seed([
438
+ { id: "u1", role: "user", text: "hello" },
439
+ { id: "a1", role: "assistant", text: "Echo: hello" },
440
+ ]),
441
+ );
442
+ await vi.waitFor(async () => {
443
+ await nextTick();
444
+ expect(el.querySelectorAll("li.msg")).toHaveLength(2);
445
+ });
446
+
447
+ const copy = q<HTMLButtonElement>(assistantItem(el), "button.copy");
448
+ expect(copy.disabled).toBe(false);
449
+ copy.click();
450
+
451
+ await vi.waitFor(async () => {
452
+ await nextTick();
453
+ expect(writeText).toHaveBeenCalledWith("Echo: hello");
454
+ expect(q(assistantItem(el), "span.probe").textContent).toBe("true");
455
+ expect(
456
+ q(assistantItem(el), "button.copy").hasAttribute("data-copied"),
457
+ ).toBe(true);
458
+ });
459
+
460
+ await vi.waitFor(async () => {
461
+ await nextTick();
462
+ expect(
463
+ q(assistantItem(el), "button.copy").hasAttribute("data-copied"),
464
+ ).toBe(false);
465
+ });
466
+
467
+ unmount();
468
+ });
469
+ });