@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,324 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { useEditor, type Editor } from "@tiptap/react";
4
+ import React, { act } from "react";
5
+ import { createRoot, type Root } from "react-dom/client";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+
8
+ import { createRichMarkdownExtensions } from "./RichMarkdownEditor.js";
9
+ import { useCollabReconcile, getEditorMarkdown } from "./useCollabReconcile.js";
10
+
11
+ /**
12
+ * Regression for the collab-reconcile escalation loop.
13
+ *
14
+ * When a block's stored markdown is NOT idempotent through the editor's
15
+ * parse/serialize (e.g. raw HTML in a GFM block: `<h1>...</h1>` serializes to
16
+ * the escaped `&lt;h1&gt;...`), the reconcile used to re-`setContent` the value
17
+ * every poll because the raw `value` never equalled the editor's serialized
18
+ * output. Each re-apply re-parsed already-applied content, the autosave echo
19
+ * came back as a "new" value, and the doc kept mutating (`<p>` → `&lt;p&gt;` →
20
+ * `&amp;lt;p&amp;gt;` …) while also fighting active typing.
21
+ *
22
+ * The fix makes the reconcile compare by DOC EQUIVALENCE: it tracks the raw
23
+ * value it last applied AND the editor's serialized output after that apply, so
24
+ * a re-supplied raw value or its own serialized echo is recognized and skipped.
25
+ * These tests drive the REAL hook + a REAL editor across simulated polls and
26
+ * assert the reconcile applies a non-idempotent value AT MOST ONCE and the
27
+ * editor stabilizes (no repeated emit / mutation).
28
+ */
29
+
30
+ let container: HTMLDivElement;
31
+ let root: Root;
32
+
33
+ beforeEach(() => {
34
+ (
35
+ globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }
36
+ ).IS_REACT_ACT_ENVIRONMENT = true;
37
+ container = document.createElement("div");
38
+ document.body.appendChild(container);
39
+ root = createRoot(container);
40
+ });
41
+
42
+ afterEach(() => {
43
+ act(() => root.unmount());
44
+ container.remove();
45
+ });
46
+
47
+ interface HarnessProps {
48
+ value: string;
49
+ contentUpdatedAt: string;
50
+ }
51
+
52
+ interface Captured {
53
+ editor: Editor | null;
54
+ /** Every markdown string the editor emitted via onUpdate (autosave echoes). */
55
+ emitted: string[];
56
+ /** How many times the reconcile/seed called setContent. */
57
+ setContentCalls: number;
58
+ }
59
+
60
+ /**
61
+ * Mirrors the real SharedRichEditor wiring: an `onUpdate` that defers to the
62
+ * hook's guards (so programmatic setContent is ignored and only genuine edits
63
+ * "autosave"), and a `setContent` override that counts applies. Non-collab path
64
+ * (no ydoc) — the escalation reproduces there and keeps the harness simple.
65
+ */
66
+ function makeHarness() {
67
+ const captured: Captured = {
68
+ editor: null,
69
+ emitted: [],
70
+ setContentCalls: 0,
71
+ };
72
+
73
+ function Harness({ value, contentUpdatedAt }: HarnessProps) {
74
+ const guardsRef = React.useRef<ReturnType<
75
+ typeof useCollabReconcile
76
+ > | null>(null);
77
+
78
+ const editor = useEditor({
79
+ extensions: createRichMarkdownExtensions({ dialect: "gfm" }),
80
+ content: value,
81
+ onUpdate: ({ editor, transaction }) => {
82
+ const guards = guardsRef.current;
83
+ if (!guards || guards.shouldIgnoreUpdate(transaction)) return;
84
+ const markdown = getEditorMarkdown(editor);
85
+ if (!guards.registerEmitted(markdown)) return;
86
+ captured.emitted.push(markdown);
87
+ },
88
+ });
89
+ captured.editor = editor;
90
+
91
+ const guards = useCollabReconcile({
92
+ editor,
93
+ value,
94
+ contentUpdatedAt,
95
+ editable: true,
96
+ getMarkdown: getEditorMarkdown,
97
+ // These specs pin the LEGACY whole-document setContent path (the
98
+ // fallback when no parsed doc is available); the surgical path has its
99
+ // own coverage in surgical-apply.spec.ts. Disable it so `setContent`
100
+ // remains the counted apply mechanism.
101
+ parseValue: false,
102
+ // Mirror the hook's (fixed) defaultSetContent: hand the markdown string to
103
+ // tiptap-markdown's setContent override WITHOUT
104
+ // `parseOptions.preserveWhitespace`, which would otherwise route through
105
+ // insertContentAt's double-parse and escape the markdown. Counts applies.
106
+ setContent: (ed, v, options) => {
107
+ captured.setContentCalls += 1;
108
+ if (options.addToHistory === false) {
109
+ ed.chain()
110
+ .command(({ tr }) => {
111
+ tr.setMeta("addToHistory", false);
112
+ return true;
113
+ })
114
+ .setContent(v, { emitUpdate: options.emitUpdate })
115
+ .run();
116
+ return;
117
+ }
118
+ ed.commands.setContent(v);
119
+ },
120
+ });
121
+ guardsRef.current = guards;
122
+
123
+ return React.createElement("div", null);
124
+ }
125
+
126
+ return { captured, Harness };
127
+ }
128
+
129
+ /** Flush the reconcile's timer task + any chained promises. */
130
+ async function flush() {
131
+ await act(async () => {
132
+ await Promise.resolve();
133
+ await new Promise((resolve) => setTimeout(resolve, 0));
134
+ await Promise.resolve();
135
+ });
136
+ }
137
+
138
+ const NON_IDEMPOTENT =
139
+ '<h1>Title</h1><ul class="contains-task-list"><li>One</li></ul>';
140
+
141
+ describe("useCollabReconcile idempotent-safe reconcile", () => {
142
+ it("applies a NON-idempotent value at most once across many polls and stabilizes", async () => {
143
+ const { captured, Harness } = makeHarness();
144
+
145
+ // Initial mount: editor seeded empty, first poll delivers the raw HTML.
146
+ act(() => {
147
+ root.render(
148
+ React.createElement(Harness, {
149
+ value: "",
150
+ contentUpdatedAt: "2024-01-01T00:00:00.000Z",
151
+ }),
152
+ );
153
+ });
154
+ await flush();
155
+
156
+ // First real value arrives.
157
+ act(() => {
158
+ root.render(
159
+ React.createElement(Harness, {
160
+ value: NON_IDEMPOTENT,
161
+ contentUpdatedAt: "2024-01-01T00:00:01.000Z",
162
+ }),
163
+ );
164
+ });
165
+ await flush();
166
+
167
+ const serializedAfterApply = getEditorMarkdown(captured.editor!);
168
+ // The non-idempotent value escapes on apply, so the serialized form differs
169
+ // from the raw input — that divergence is exactly what used to loop.
170
+ expect(serializedAfterApply).not.toBe(NON_IDEMPOTENT);
171
+ const applyCountAfterFirst = captured.setContentCalls;
172
+ expect(applyCountAfterFirst).toBeGreaterThanOrEqual(1);
173
+
174
+ // Simulate the poll loop RE-SUPPLYING the SAME raw value many times (a
175
+ // source-sync / lagging poll that keeps handing back the stored HTML).
176
+ for (let i = 0; i < 6; i++) {
177
+ act(() => {
178
+ root.render(
179
+ React.createElement(Harness, {
180
+ value: NON_IDEMPOTENT,
181
+ contentUpdatedAt: "2024-01-01T00:00:01.000Z",
182
+ }),
183
+ );
184
+ });
185
+ await flush();
186
+ }
187
+
188
+ // It must NOT keep re-applying — the doc-equivalence guard recognizes the
189
+ // re-supplied raw value as already-applied.
190
+ expect(captured.setContentCalls).toBe(applyCountAfterFirst);
191
+ // And the editor's serialized output is stable (did not escalate).
192
+ expect(getEditorMarkdown(captured.editor!)).toBe(serializedAfterApply);
193
+ });
194
+
195
+ it("recognizes its own serialized echo (bumped timestamp) and does not re-apply", async () => {
196
+ const { captured, Harness } = makeHarness();
197
+
198
+ act(() => {
199
+ root.render(
200
+ React.createElement(Harness, {
201
+ value: "",
202
+ contentUpdatedAt: "2024-01-01T00:00:00.000Z",
203
+ }),
204
+ );
205
+ });
206
+ await flush();
207
+
208
+ act(() => {
209
+ root.render(
210
+ React.createElement(Harness, {
211
+ value: NON_IDEMPOTENT,
212
+ contentUpdatedAt: "2024-01-01T00:00:01.000Z",
213
+ }),
214
+ );
215
+ });
216
+ await flush();
217
+
218
+ const serialized = getEditorMarkdown(captured.editor!);
219
+ const applyCount = captured.setContentCalls;
220
+
221
+ // Autosave persists the editor's SERIALIZED output, bumping updatedAt; the
222
+ // next poll hands that serialized echo back as the new authoritative value.
223
+ for (let i = 0; i < 5; i++) {
224
+ act(() => {
225
+ root.render(
226
+ React.createElement(Harness, {
227
+ value: serialized,
228
+ contentUpdatedAt: `2024-01-01T00:00:0${2 + i}.000Z`,
229
+ }),
230
+ );
231
+ });
232
+ await flush();
233
+ }
234
+
235
+ // The echo is doc-equivalent to what the editor already shows -> no re-apply
236
+ // and no escalation.
237
+ expect(captured.setContentCalls).toBe(applyCount);
238
+ expect(getEditorMarkdown(captured.editor!)).toBe(serialized);
239
+ });
240
+
241
+ it("still applies a genuinely-new external edit after a non-idempotent apply", async () => {
242
+ const { captured, Harness } = makeHarness();
243
+
244
+ act(() => {
245
+ root.render(
246
+ React.createElement(Harness, {
247
+ value: "",
248
+ contentUpdatedAt: "2024-01-01T00:00:00.000Z",
249
+ }),
250
+ );
251
+ });
252
+ await flush();
253
+
254
+ act(() => {
255
+ root.render(
256
+ React.createElement(Harness, {
257
+ value: NON_IDEMPOTENT,
258
+ contentUpdatedAt: "2024-01-01T00:00:01.000Z",
259
+ }),
260
+ );
261
+ });
262
+ await flush();
263
+ const applyCount = captured.setContentCalls;
264
+
265
+ // A real, different agent edit (clean idempotent markdown) with a newer
266
+ // timestamp must still reconcile in.
267
+ act(() => {
268
+ root.render(
269
+ React.createElement(Harness, {
270
+ value: "# Brand New Heading\n\nFresh body.",
271
+ contentUpdatedAt: "2024-01-01T00:00:05.000Z",
272
+ }),
273
+ );
274
+ });
275
+ await flush();
276
+
277
+ expect(captured.setContentCalls).toBeGreaterThan(applyCount);
278
+ expect(getEditorMarkdown(captured.editor!)).toBe(
279
+ "# Brand New Heading\n\nFresh body.",
280
+ );
281
+ });
282
+
283
+ it("does not re-apply an idempotent value that is re-polled unchanged", async () => {
284
+ const { captured, Harness } = makeHarness();
285
+ const CLEAN = "# Heading\n\nA paragraph with **bold** text.";
286
+
287
+ act(() => {
288
+ root.render(
289
+ React.createElement(Harness, {
290
+ value: "",
291
+ contentUpdatedAt: "2024-01-01T00:00:00.000Z",
292
+ }),
293
+ );
294
+ });
295
+ await flush();
296
+
297
+ act(() => {
298
+ root.render(
299
+ React.createElement(Harness, {
300
+ value: CLEAN,
301
+ contentUpdatedAt: "2024-01-01T00:00:01.000Z",
302
+ }),
303
+ );
304
+ });
305
+ await flush();
306
+ const applyCount = captured.setContentCalls;
307
+ expect(getEditorMarkdown(captured.editor!)).toBe(CLEAN);
308
+
309
+ for (let i = 0; i < 4; i++) {
310
+ act(() => {
311
+ root.render(
312
+ React.createElement(Harness, {
313
+ value: CLEAN,
314
+ contentUpdatedAt: "2024-01-01T00:00:01.000Z",
315
+ }),
316
+ );
317
+ });
318
+ await flush();
319
+ }
320
+ // Idempotent + unchanged -> the existing equality guard already skips.
321
+ expect(captured.setContentCalls).toBe(applyCount);
322
+ expect(getEditorMarkdown(captured.editor!)).toBe(CLEAN);
323
+ });
324
+ });