@agent-native/toolkit 0.6.0 → 0.7.0

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 (173) hide show
  1. package/README.md +17 -1
  2. package/dist/chat-history/ChatHistoryList.d.ts +103 -0
  3. package/dist/chat-history/ChatHistoryList.d.ts.map +1 -0
  4. package/dist/chat-history/ChatHistoryList.js +109 -0
  5. package/dist/chat-history/ChatHistoryList.js.map +1 -0
  6. package/dist/chat-history/ChatHistoryList.spec.d.ts +2 -0
  7. package/dist/chat-history/ChatHistoryList.spec.d.ts.map +1 -0
  8. package/dist/chat-history/ChatHistoryList.spec.js +283 -0
  9. package/dist/chat-history/ChatHistoryList.spec.js.map +1 -0
  10. package/dist/chat-history/index.d.ts +2 -0
  11. package/dist/chat-history/index.d.ts.map +1 -0
  12. package/dist/chat-history/index.js +2 -0
  13. package/dist/chat-history/index.js.map +1 -0
  14. package/dist/chat-history.css +314 -0
  15. package/package.json +17 -1
  16. package/src/app-shell/header-actions.tsx +84 -0
  17. package/src/app-shell/index.ts +1 -0
  18. package/src/chat-history/ChatHistoryList.spec.tsx +453 -0
  19. package/src/chat-history/ChatHistoryList.tsx +469 -0
  20. package/src/chat-history/index.ts +7 -0
  21. package/src/chat-history.css +314 -0
  22. package/src/collab-ui/AgentPresenceChip.tsx +69 -0
  23. package/src/collab-ui/LiveCursorOverlay.tsx +241 -0
  24. package/src/collab-ui/PresenceBar.tsx +346 -0
  25. package/src/collab-ui/RecentEditHighlights.tsx +225 -0
  26. package/src/collab-ui/RemoteSelectionRings.tsx +227 -0
  27. package/src/collab-ui/agent-identity.ts +16 -0
  28. package/src/collab-ui/index.ts +39 -0
  29. package/src/collab-ui/lead-client.ts +32 -0
  30. package/src/collab-ui/types.spec.ts +18 -0
  31. package/src/collab-ui/types.ts +96 -0
  32. package/src/composer/AgentComposerFrame.tsx +59 -0
  33. package/src/composer/ComposerPlusMenu.tsx +983 -0
  34. package/src/composer/MentionPopover.tsx +412 -0
  35. package/src/composer/PastedTextChip.tsx +150 -0
  36. package/src/composer/PromptComposer.spec.ts +73 -0
  37. package/src/composer/PromptComposer.tsx +718 -0
  38. package/src/composer/RealtimeVoiceMode.spec.tsx +771 -0
  39. package/src/composer/RealtimeVoiceMode.tsx +856 -0
  40. package/src/composer/TiptapComposer.spec.ts +390 -0
  41. package/src/composer/TiptapComposer.tsx +2936 -0
  42. package/src/composer/VoiceButton.spec.ts +33 -0
  43. package/src/composer/VoiceButton.tsx +255 -0
  44. package/src/composer/asset-picker-url.spec.ts +56 -0
  45. package/src/composer/asset-picker-url.ts +80 -0
  46. package/src/composer/attachment-accept.spec.ts +22 -0
  47. package/src/composer/attachment-accept.ts +71 -0
  48. package/src/composer/draft-key.spec.ts +19 -0
  49. package/src/composer/draft-key.ts +7 -0
  50. package/src/composer/extensions/FileReference.tsx +55 -0
  51. package/src/composer/extensions/MentionReference.tsx +93 -0
  52. package/src/composer/extensions/SkillReference.tsx +56 -0
  53. package/src/composer/index.ts +79 -0
  54. package/src/composer/pasted-text.spec.ts +162 -0
  55. package/src/composer/pasted-text.ts +160 -0
  56. package/src/composer/prompt-attachments.spec.ts +80 -0
  57. package/src/composer/prompt-attachments.ts +93 -0
  58. package/src/composer/realtime-voice-audio-level.spec.ts +32 -0
  59. package/src/composer/realtime-voice-audio-level.ts +64 -0
  60. package/src/composer/realtime-voice-transcript.spec.ts +95 -0
  61. package/src/composer/realtime-voice-transcript.ts +134 -0
  62. package/src/composer/runtime-adapters.tsx +343 -0
  63. package/src/composer/types.ts +66 -0
  64. package/src/composer/use-file-search.ts +64 -0
  65. package/src/composer/use-mention-search.ts +90 -0
  66. package/src/composer/use-skills.ts +45 -0
  67. package/src/composer/useRealtimeVoiceMode.spec.ts +818 -0
  68. package/src/composer/useRealtimeVoiceMode.tsx +2117 -0
  69. package/src/composer/useVoiceDictation.spec.ts +45 -0
  70. package/src/composer/useVoiceDictation.ts +1191 -0
  71. package/src/context-ui/ContextMeter.tsx +101 -0
  72. package/src/context-ui/ContextSegmentRow.tsx +184 -0
  73. package/src/context-ui/ContextTreemap.tsx +125 -0
  74. package/src/context-ui/ContextXRayPanel.tsx +377 -0
  75. package/src/context-ui/format.ts +42 -0
  76. package/src/context-ui/index.ts +15 -0
  77. package/src/context-ui/types.ts +43 -0
  78. package/src/design-tweaks/index.ts +1 -0
  79. package/src/design-tweaks/visual-style-controls.tsx +1084 -0
  80. package/src/editor/BubbleToolbar.tsx +253 -0
  81. package/src/editor/CodeBlockNode.tsx +254 -0
  82. package/src/editor/DragHandle.spec.ts +588 -0
  83. package/src/editor/DragHandle.ts +1437 -0
  84. package/src/editor/ImageExtension.roundtrip.spec.ts +117 -0
  85. package/src/editor/ImageExtension.ts +306 -0
  86. package/src/editor/RegistryBlockContext.tsx +107 -0
  87. package/src/editor/RegistryBlockNode.spec.ts +279 -0
  88. package/src/editor/RegistryBlockNode.tsx +652 -0
  89. package/src/editor/RichMarkdownEditor.collab.spec.ts +79 -0
  90. package/src/editor/RichMarkdownEditor.roundtrip.spec.ts +226 -0
  91. package/src/editor/RichMarkdownEditor.tsx +81 -0
  92. package/src/editor/RunId.ts +61 -0
  93. package/src/editor/SharedRichEditor.tsx +321 -0
  94. package/src/editor/SlashCommandMenu.tsx +310 -0
  95. package/src/editor/disableHistory.spec.ts +85 -0
  96. package/src/editor/extensions.ts +402 -0
  97. package/src/editor/gfmDoc.spec.ts +230 -0
  98. package/src/editor/gfmDoc.ts +92 -0
  99. package/src/editor/index.ts +93 -0
  100. package/src/editor/registrySlashCommands.ts +131 -0
  101. package/src/editor/structuralReorderUndo.spec.ts +144 -0
  102. package/src/editor/surgical-apply.spec.ts +279 -0
  103. package/src/editor/surgical-apply.ts +192 -0
  104. package/src/editor/useCollabReconcile.concurrent.spec.ts +422 -0
  105. package/src/editor/useCollabReconcile.idempotent.spec.ts +324 -0
  106. package/src/editor/useCollabReconcile.ts +589 -0
  107. package/src/editor.css +510 -0
  108. package/src/hooks/index.ts +2 -0
  109. package/src/hooks/use-mobile.tsx +21 -0
  110. package/src/hooks/use-toast.ts +188 -0
  111. package/src/index.ts +8 -0
  112. package/src/onboarding/index.ts +1 -0
  113. package/src/provider.tsx +47 -0
  114. package/src/sharing/VisibilityBadge.spec.ts +96 -0
  115. package/src/sharing/VisibilityBadge.tsx +37 -0
  116. package/src/sharing/index.ts +4 -0
  117. package/src/styles.css +21 -0
  118. package/src/ui/accordion.tsx +56 -0
  119. package/src/ui/alert-dialog.tsx +141 -0
  120. package/src/ui/alert.tsx +59 -0
  121. package/src/ui/aspect-ratio.tsx +5 -0
  122. package/src/ui/avatar.tsx +48 -0
  123. package/src/ui/badge.tsx +37 -0
  124. package/src/ui/breadcrumb.tsx +115 -0
  125. package/src/ui/button.tsx +78 -0
  126. package/src/ui/calendar.tsx +214 -0
  127. package/src/ui/card.tsx +86 -0
  128. package/src/ui/carousel.tsx +260 -0
  129. package/src/ui/chart.tsx +375 -0
  130. package/src/ui/checkbox.tsx +28 -0
  131. package/src/ui/collapsible.tsx +9 -0
  132. package/src/ui/command.spec.tsx +51 -0
  133. package/src/ui/command.tsx +174 -0
  134. package/src/ui/context-menu.tsx +202 -0
  135. package/src/ui/date-picker.tsx +64 -0
  136. package/src/ui/dialog.spec.tsx +43 -0
  137. package/src/ui/dialog.tsx +146 -0
  138. package/src/ui/drawer.tsx +126 -0
  139. package/src/ui/dropdown-menu.tsx +218 -0
  140. package/src/ui/form.tsx +177 -0
  141. package/src/ui/hover-card.tsx +27 -0
  142. package/src/ui/index.ts +52 -0
  143. package/src/ui/input-otp.tsx +69 -0
  144. package/src/ui/input.tsx +22 -0
  145. package/src/ui/label.tsx +24 -0
  146. package/src/ui/logo.tsx +36 -0
  147. package/src/ui/menubar.tsx +235 -0
  148. package/src/ui/navigation-menu.tsx +128 -0
  149. package/src/ui/pagination.tsx +121 -0
  150. package/src/ui/popover.tsx +44 -0
  151. package/src/ui/progress.tsx +26 -0
  152. package/src/ui/radio-group.tsx +42 -0
  153. package/src/ui/resizable.tsx +43 -0
  154. package/src/ui/scroll-area.tsx +46 -0
  155. package/src/ui/select.tsx +158 -0
  156. package/src/ui/separator.tsx +29 -0
  157. package/src/ui/sheet.tsx +160 -0
  158. package/src/ui/sidebar.tsx +777 -0
  159. package/src/ui/skeleton.tsx +18 -0
  160. package/src/ui/slider.tsx +26 -0
  161. package/src/ui/sonner.tsx +58 -0
  162. package/src/ui/spinner.tsx +17 -0
  163. package/src/ui/switch.tsx +27 -0
  164. package/src/ui/table.tsx +117 -0
  165. package/src/ui/tabs.tsx +53 -0
  166. package/src/ui/textarea.tsx +23 -0
  167. package/src/ui/toast.tsx +127 -0
  168. package/src/ui/toaster.tsx +33 -0
  169. package/src/ui/toggle-group.tsx +59 -0
  170. package/src/ui/toggle.tsx +43 -0
  171. package/src/ui/tooltip.tsx +53 -0
  172. package/src/ui/use-toast.ts +3 -0
  173. package/src/utils.ts +8 -0
@@ -0,0 +1,390 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { Editor } from "@tiptap/core";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import {
7
+ canSubmitComposerContent,
8
+ canRemoveVoicePreview,
9
+ compactComposerModelName,
10
+ compactComposerReasoningEffortLabel,
11
+ createTiptapComposerExtensions,
12
+ displayableComposerModeMessage,
13
+ getComposerSubmitIntentForEnterKey,
14
+ getComposerPopoverPosition,
15
+ getComposerReasoningEffortOptions,
16
+ getOversizedDocumentAttachmentError,
17
+ handleComposerFileDrop,
18
+ insertComposerHardBreakAndScrollIntoView,
19
+ isComposerEditorUsable,
20
+ formatVoiceTranscriptForComposer,
21
+ MODEL_SELECTOR_POPOVER_STYLE,
22
+ resolveContextChipBackspaceAction,
23
+ resolveComposerPrimaryAction,
24
+ shouldShowModelSelectorSkeleton,
25
+ } from "./TiptapComposer.js";
26
+
27
+ describe("createTiptapComposerExtensions", () => {
28
+ it("rejects a truthy editor after BFCache/remount destruction", () => {
29
+ const editor = new Editor({
30
+ element: document.createElement("div"),
31
+ extensions: createTiptapComposerExtensions(() => "Message agent..."),
32
+ });
33
+
34
+ expect(isComposerEditorUsable(editor)).toBe(true);
35
+ editor.destroy();
36
+ expect(editor).toBeTruthy();
37
+ expect(editor.isDestroyed).toBe(true);
38
+ expect(isComposerEditorUsable(editor)).toBe(false);
39
+ expect(() => {
40
+ if (isComposerEditorUsable(editor)) editor.commands.clearContent();
41
+ }).not.toThrow();
42
+ });
43
+
44
+ it("offers explicit reasoning levels without legacy Auto", () => {
45
+ expect(getComposerReasoningEffortOptions("auto")).toEqual([
46
+ "low",
47
+ "medium",
48
+ "high",
49
+ "xhigh",
50
+ "max",
51
+ ]);
52
+ expect(getComposerReasoningEffortOptions("claude-sonnet-5")).not.toContain(
53
+ "auto",
54
+ );
55
+ });
56
+
57
+ it("uses compact GPT-5.6 model and effort names in the collapsed trigger", () => {
58
+ expect(compactComposerModelName("gpt-5.6-sol")).toBe("Sol");
59
+ expect(compactComposerModelName("gpt-5-6-terra")).toBe("Terra");
60
+ expect(compactComposerModelName("openai/gpt-5.6-luna")).toBe("Luna");
61
+ expect(compactComposerModelName("claude-sonnet-5")).toBe("Sonnet 5");
62
+ expect(compactComposerReasoningEffortLabel("medium")).toBe("Med");
63
+ expect(compactComposerReasoningEffortLabel("minimal")).toBe("Min");
64
+ expect(compactComposerReasoningEffortLabel("xhigh")).toBe("XHigh");
65
+ });
66
+
67
+ it("keeps the prompt composer schema minimal and restores legacy draft HTML", () => {
68
+ const editor = new Editor({
69
+ element: document.createElement("div"),
70
+ extensions: createTiptapComposerExtensions(() => "Message agent..."),
71
+ });
72
+
73
+ expect(Object.keys(editor.schema.marks)).toEqual([]);
74
+ expect(Object.keys(editor.schema.nodes).sort()).toEqual([
75
+ "doc",
76
+ "fileReference",
77
+ "hardBreak",
78
+ "mentionReference",
79
+ "paragraph",
80
+ "skillReference",
81
+ "text",
82
+ ]);
83
+
84
+ expect(() => {
85
+ editor.commands.setContent(`
86
+ <h1>Legacy heading</h1>
87
+ <ul><li>Legacy list item</li></ul>
88
+ <p><a href="https://example.com">Legacy link</a></p>
89
+ <p><span data-type="file-reference" path="/tmp/example.ts"></span></p>
90
+ `);
91
+ }).not.toThrow();
92
+
93
+ expect(editor.getText()).toContain("Legacy heading");
94
+ expect(editor.getText()).toContain("Legacy list item");
95
+ expect(editor.getText()).toContain("Legacy link");
96
+ expect(editor.getHTML()).toContain('data-type="file-reference"');
97
+
98
+ editor.destroy();
99
+ });
100
+
101
+ it("allows sending an attachment-only prompt", () => {
102
+ expect(
103
+ canSubmitComposerContent({
104
+ hasEditorContent: false,
105
+ attachmentCount: 1,
106
+ }),
107
+ ).toBe(true);
108
+ expect(
109
+ canSubmitComposerContent({
110
+ hasEditorContent: false,
111
+ attachmentCount: 1,
112
+ disabled: true,
113
+ }),
114
+ ).toBe(false);
115
+ });
116
+
117
+ it("uses one primary action while a response is running", () => {
118
+ expect(
119
+ resolveComposerPrimaryAction({
120
+ canSubmit: false,
121
+ hasStopButton: true,
122
+ }),
123
+ ).toBe("stop");
124
+ expect(
125
+ resolveComposerPrimaryAction({
126
+ canSubmit: true,
127
+ hasStopButton: true,
128
+ }),
129
+ ).toBe("send");
130
+ expect(
131
+ resolveComposerPrimaryAction({
132
+ canSubmit: false,
133
+ hasStopButton: false,
134
+ }),
135
+ ).toBe("send");
136
+ });
137
+
138
+ it("selects and removes context chips one Backspace at a time", () => {
139
+ let contextItemKeys = ["dashboard", "panel"];
140
+ let selectedKey: string | null = null;
141
+
142
+ const selectPanel = resolveContextChipBackspaceAction({
143
+ contextItemKeys,
144
+ selectedKey,
145
+ cursorAtStart: true,
146
+ });
147
+ expect(selectPanel).toEqual({ type: "select", key: "panel" });
148
+ selectedKey = selectPanel?.key ?? null;
149
+
150
+ const removePanel = resolveContextChipBackspaceAction({
151
+ contextItemKeys,
152
+ selectedKey,
153
+ cursorAtStart: true,
154
+ });
155
+ expect(removePanel).toEqual({ type: "remove", key: "panel" });
156
+ contextItemKeys = contextItemKeys.filter((key) => key !== removePanel?.key);
157
+ selectedKey = null;
158
+
159
+ const selectDashboard = resolveContextChipBackspaceAction({
160
+ contextItemKeys,
161
+ selectedKey,
162
+ cursorAtStart: true,
163
+ });
164
+ expect(selectDashboard).toEqual({ type: "select", key: "dashboard" });
165
+ selectedKey = selectDashboard?.key ?? null;
166
+
167
+ expect(
168
+ resolveContextChipBackspaceAction({
169
+ contextItemKeys,
170
+ selectedKey,
171
+ cursorAtStart: true,
172
+ }),
173
+ ).toEqual({ type: "remove", key: "dashboard" });
174
+ });
175
+
176
+ it("leaves context chips alone when the caret is not at the start", () => {
177
+ expect(
178
+ resolveContextChipBackspaceAction({
179
+ contextItemKeys: ["dashboard"],
180
+ selectedKey: null,
181
+ cursorAtStart: false,
182
+ }),
183
+ ).toBeNull();
184
+ });
185
+
186
+ it("uses a visible fallback for attachment-only composer mode prompts", () => {
187
+ expect(
188
+ displayableComposerModeMessage({
189
+ messagePrefix: "Create an extension: ",
190
+ trimmedText: "",
191
+ attachmentCount: 1,
192
+ }),
193
+ ).toBe("Create an extension: Use the attached context.");
194
+ });
195
+
196
+ it("detects oversized PDF attachments before submit", () => {
197
+ const file = new File([new Uint8Array(4 * 1024 * 1024 + 1)], "large.pdf", {
198
+ type: "application/pdf",
199
+ });
200
+
201
+ expect(
202
+ getOversizedDocumentAttachmentError([
203
+ {
204
+ type: "document",
205
+ name: "large.pdf",
206
+ contentType: "application/pdf",
207
+ file,
208
+ },
209
+ ]),
210
+ ).toContain('"large.pdf" is 4.0 MB — PDFs are capped at 4 MB');
211
+ expect(
212
+ getOversizedDocumentAttachmentError([
213
+ {
214
+ type: "image",
215
+ name: "large.png",
216
+ contentType: "image/png",
217
+ file,
218
+ },
219
+ ]),
220
+ ).toBeNull();
221
+ });
222
+
223
+ it("maps Enter keybindings to immediate and queued submit intents", () => {
224
+ const enter = {
225
+ key: "Enter",
226
+ shiftKey: false,
227
+ metaKey: false,
228
+ ctrlKey: false,
229
+ };
230
+
231
+ expect(getComposerSubmitIntentForEnterKey(enter, true)).toBe("immediate");
232
+ expect(getComposerSubmitIntentForEnterKey(enter, false)).toBe("immediate");
233
+ expect(
234
+ getComposerSubmitIntentForEnterKey({ ...enter, metaKey: true }, true),
235
+ ).toBe("queued");
236
+ expect(
237
+ getComposerSubmitIntentForEnterKey({ ...enter, ctrlKey: true }, false),
238
+ ).toBe("queued");
239
+ expect(
240
+ getComposerSubmitIntentForEnterKey(
241
+ { ...enter, shiftKey: true, metaKey: true },
242
+ true,
243
+ ),
244
+ ).toBeNull();
245
+ expect(
246
+ getComposerSubmitIntentForEnterKey({ ...enter, ctrlKey: true }, true),
247
+ ).toBeNull();
248
+ expect(
249
+ getComposerSubmitIntentForEnterKey({ ...enter, metaKey: true }, false),
250
+ ).toBeNull();
251
+ });
252
+
253
+ it("scrolls the composer caret into view for Shift+Enter line breaks", () => {
254
+ const editor = new Editor({
255
+ element: document.createElement("div"),
256
+ extensions: createTiptapComposerExtensions(() => "Message agent..."),
257
+ content: "<p>Hello</p>",
258
+ });
259
+ editor.commands.setTextSelection(editor.state.doc.content.size);
260
+
261
+ const view = editor.view;
262
+ const scrolledTransactions: boolean[] = [];
263
+ const dispatch = view.dispatch.bind(view);
264
+ view.dispatch = (transaction) => {
265
+ scrolledTransactions.push(transaction.scrolledIntoView);
266
+ dispatch(transaction);
267
+ };
268
+
269
+ expect(insertComposerHardBreakAndScrollIntoView(view)).toBe(true);
270
+ expect(scrolledTransactions).toEqual([true]);
271
+ expect(editor.getText()).toBe("Hello\n");
272
+
273
+ editor.destroy();
274
+ });
275
+
276
+ it("guards popover positioning when the editor cannot resolve coordinates", () => {
277
+ expect(
278
+ getComposerPopoverPosition(
279
+ {
280
+ coordsAtPos: () => ({ top: 12, bottom: 20, left: 34, right: 34 }),
281
+ },
282
+ 1,
283
+ ),
284
+ ).toEqual({ top: 12, left: 34 });
285
+ expect(
286
+ getComposerPopoverPosition(
287
+ {
288
+ coordsAtPos: () => {
289
+ throw new TypeError("node.getBoundingClientRect is not a function");
290
+ },
291
+ },
292
+ 1,
293
+ ),
294
+ ).toBeNull();
295
+ expect(
296
+ getComposerPopoverPosition(
297
+ {
298
+ coordsAtPos: () => ({
299
+ top: Number.NaN,
300
+ bottom: 20,
301
+ left: 34,
302
+ right: 34,
303
+ }),
304
+ },
305
+ 1,
306
+ ),
307
+ ).toBeNull();
308
+ });
309
+
310
+ it("consumes composer file drops so parent drop targets do not attach duplicates", () => {
311
+ const file = new File(["fake"], "image.png", { type: "image/png" });
312
+ const added: File[] = [];
313
+ let prevented = false;
314
+ let stopped = false;
315
+ const handled = handleComposerFileDrop({
316
+ event: {
317
+ dataTransfer: { files: [file] },
318
+ preventDefault: () => {
319
+ prevented = true;
320
+ },
321
+ stopPropagation: () => {
322
+ stopped = true;
323
+ },
324
+ } as unknown as DragEvent,
325
+ addAttachment: async (attachment) => {
326
+ added.push(attachment);
327
+ },
328
+ });
329
+
330
+ expect(handled).toBe(true);
331
+ expect(prevented).toBe(true);
332
+ expect(stopped).toBe(true);
333
+ expect(added).toHaveLength(1);
334
+ expect(added[0]?.name).toMatch(/^\d+-[a-z0-9]+-image\.png$/);
335
+ });
336
+
337
+ it("caps the model picker height without forcing empty vertical space", () => {
338
+ expect(MODEL_SELECTOR_POPOVER_STYLE).toMatchObject({
339
+ fontSize: 13,
340
+ maxHeight:
341
+ "min(500px, var(--radix-popover-content-available-height, 500px))",
342
+ });
343
+ expect(MODEL_SELECTOR_POPOVER_STYLE).not.toHaveProperty("height");
344
+ });
345
+
346
+ it("shows the model picker skeleton only while the initial list is loading", () => {
347
+ expect(shouldShowModelSelectorSkeleton(true, 0)).toBe(true);
348
+ expect(shouldShowModelSelectorSkeleton(true, 2)).toBe(false);
349
+ expect(shouldShowModelSelectorSkeleton(false, 0)).toBe(false);
350
+ });
351
+ });
352
+
353
+ describe("voice composer insertion", () => {
354
+ it("adds sentence punctuation and a trailing separator to dictated text", () => {
355
+ expect(formatVoiceTranscriptForComposer(" First sentence ")).toBe(
356
+ "First sentence. ",
357
+ );
358
+ expect(formatVoiceTranscriptForComposer("Already done? ")).toBe(
359
+ "Already done? ",
360
+ );
361
+ expect(formatVoiceTranscriptForComposer(" ")).toBe("");
362
+ });
363
+
364
+ it("only removes a live preview when its range still contains the preview", () => {
365
+ expect(
366
+ canRemoveVoicePreview({
367
+ documentSize: 20,
368
+ anchor: 4,
369
+ previewText: "draft",
370
+ currentText: "draft",
371
+ }),
372
+ ).toBe(true);
373
+ expect(
374
+ canRemoveVoicePreview({
375
+ documentSize: 6,
376
+ anchor: 4,
377
+ previewText: "draft",
378
+ currentText: "",
379
+ }),
380
+ ).toBe(false);
381
+ expect(
382
+ canRemoveVoicePreview({
383
+ documentSize: 20,
384
+ anchor: 4,
385
+ previewText: "draft",
386
+ currentText: "sent",
387
+ }),
388
+ ).toBe(false);
389
+ });
390
+ });