@agent-native/toolkit 0.10.5 → 0.10.7
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.
- package/dist/composer/PromptComposer.d.ts +10 -0
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +14 -15
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/PromptComposer.spec.js +24 -1
- package/dist/composer/PromptComposer.spec.js.map +1 -1
- package/dist/editor/useCollabReconcile.concurrent.spec.js +240 -1
- package/dist/editor/useCollabReconcile.concurrent.spec.js.map +1 -1
- package/dist/editor/useCollabReconcile.d.ts.map +1 -1
- package/dist/editor/useCollabReconcile.js +77 -13
- package/dist/editor/useCollabReconcile.js.map +1 -1
- package/package.json +1 -1
- package/src/composer/PromptComposer.spec.ts +38 -1
- package/src/composer/PromptComposer.tsx +23 -20
- package/src/editor/useCollabReconcile.concurrent.spec.ts +270 -1
- package/src/editor/useCollabReconcile.ts +83 -15
|
@@ -33,6 +33,11 @@ export function getEditorMarkdown(editor: Editor): string {
|
|
|
33
33
|
* is identical, so skipping it is safe by construction.
|
|
34
34
|
*/
|
|
35
35
|
const EMITTED_RING_MAX = 16;
|
|
36
|
+
// The hosted awareness transport polls every 2s when its SSE gateway cannot
|
|
37
|
+
// forward presence. Give that first snapshot one poll plus margin before an
|
|
38
|
+
// empty SQL value is allowed to clear a nonempty Y.Doc. This is the same settle
|
|
39
|
+
// window used below when a known peer may deliver an edit through Yjs.
|
|
40
|
+
const PEER_SETTLE_MS = 2500;
|
|
36
41
|
function pushEmittedRing(ring: string[], value: string): void {
|
|
37
42
|
if (!value) return;
|
|
38
43
|
if (ring[ring.length - 1] === value) return;
|
|
@@ -264,6 +269,11 @@ export function useCollabReconcile({
|
|
|
264
269
|
// arrives via Yjs, so external markdown reconcile must defer (avoid applying
|
|
265
270
|
// the same change through both Yjs and setContent).
|
|
266
271
|
const peerCountRef = useRef(0);
|
|
272
|
+
// Briefly gates the first empty-SQL reconcile while Collaboration projects
|
|
273
|
+
// a nonempty fragment. `seededRef` is still released immediately so a real
|
|
274
|
+
// first keystroke can persist; this ref only postpones the ambiguous clear
|
|
275
|
+
// decision until we can distinguish an active/local edit from stale CRDT.
|
|
276
|
+
const emptySnapshotDecisionPendingRef = useRef(false);
|
|
267
277
|
useEffect(() => {
|
|
268
278
|
if (!collab || !awareness || !ydoc) {
|
|
269
279
|
setIsLeadClient(true);
|
|
@@ -299,25 +309,73 @@ export function useCollabReconcile({
|
|
|
299
309
|
if (!collab || !editor || editor.isDestroyed || !ydoc) return;
|
|
300
310
|
if (seededRef.current) return;
|
|
301
311
|
if (!collabSynced) return;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
// app's sentinel-empty filler via a custom `shouldSeed`).
|
|
309
|
-
if (
|
|
310
|
-
!shouldSeed({ value, currentMarkdown, fragmentLength: fragment.length })
|
|
311
|
-
) {
|
|
312
|
+
// An empty SQL value has nothing to seed. Release the first real keystroke
|
|
313
|
+
// immediately, but when a fragment already exists defer the ambiguous
|
|
314
|
+
// reconcile decision for one task: active-peer or just-emitted local content
|
|
315
|
+
// is live and must be adopted; stale persisted CRDT with no active writer is
|
|
316
|
+
// cleared by the canonical SQL snapshot.
|
|
317
|
+
if (!value.trim()) {
|
|
312
318
|
seededRef.current = true;
|
|
313
|
-
|
|
314
|
-
|
|
319
|
+
const fragment = ydoc.getXmlFragment("default");
|
|
320
|
+
const currentMarkdown = getMarkdown(editor);
|
|
321
|
+
if (fragment.length === 0 && !currentMarkdown.trim()) return;
|
|
315
322
|
|
|
323
|
+
emptySnapshotDecisionPendingRef.current = true;
|
|
324
|
+
let cancelled = false;
|
|
325
|
+
const adoptTimer = setTimeout(
|
|
326
|
+
() => {
|
|
327
|
+
if (cancelled || editor.isDestroyed) return;
|
|
328
|
+
const projectedMarkdown = getMarkdown(editor);
|
|
329
|
+
const isOwnFreshEdit =
|
|
330
|
+
projectedMarkdown.trim() &&
|
|
331
|
+
(projectedMarkdown === lastEmittedRef.current ||
|
|
332
|
+
recentEmittedRef.current.includes(projectedMarkdown));
|
|
333
|
+
if (
|
|
334
|
+
projectedMarkdown.trim() &&
|
|
335
|
+
(peerCountRef.current > 0 || isOwnFreshEdit)
|
|
336
|
+
) {
|
|
337
|
+
lastAppliedValueRef.current = value;
|
|
338
|
+
lastAppliedSerializedRef.current = projectedMarkdown;
|
|
339
|
+
if (contentUpdatedAt) {
|
|
340
|
+
lastAppliedUpdatedAtRef.current = contentUpdatedAt;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
emptySnapshotDecisionPendingRef.current = false;
|
|
344
|
+
},
|
|
345
|
+
awareness ? PEER_SETTLE_MS : 0,
|
|
346
|
+
);
|
|
347
|
+
return () => {
|
|
348
|
+
cancelled = true;
|
|
349
|
+
clearTimeout(adoptTimer);
|
|
350
|
+
emptySnapshotDecisionPendingRef.current = false;
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (!isLeadClient) return;
|
|
316
354
|
let cancelled = false;
|
|
317
355
|
// Defer via a timer task (NOT a microtask — microtasks can still run
|
|
318
356
|
// inside React's commit and trigger flushSync-from-lifecycle warnings).
|
|
357
|
+
// Read the editor and fragment INSIDE that task. The collaboration
|
|
358
|
+
// extension can finish projecting an already-populated Y.Doc into
|
|
359
|
+
// ProseMirror after this effect is scheduled. Capturing the pre-projection
|
|
360
|
+
// empty editor here would incorrectly seed the SQL snapshot alongside the
|
|
361
|
+
// existing CRDT content, duplicating the whole document after reload.
|
|
319
362
|
const seedTimer = setTimeout(() => {
|
|
320
363
|
if (cancelled || editor.isDestroyed) return;
|
|
364
|
+
const fragment = ydoc.getXmlFragment("default");
|
|
365
|
+
const currentMarkdown = getMarkdown(editor);
|
|
366
|
+
// Seed only when the shared doc is genuinely empty — either the fragment
|
|
367
|
+
// has no nodes yet, or it holds no semantic markdown (an empty paragraph,
|
|
368
|
+
// or an app's sentinel-empty filler via a custom `shouldSeed`).
|
|
369
|
+
if (
|
|
370
|
+
!shouldSeed({
|
|
371
|
+
value,
|
|
372
|
+
currentMarkdown,
|
|
373
|
+
fragmentLength: fragment.length,
|
|
374
|
+
})
|
|
375
|
+
) {
|
|
376
|
+
seededRef.current = true;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
321
379
|
isSettingContentRef.current = true;
|
|
322
380
|
try {
|
|
323
381
|
setContent(editor, value, {});
|
|
@@ -362,16 +420,26 @@ export function useCollabReconcile({
|
|
|
362
420
|
// With peers present, a peer's edit also arrives via Yjs. Defer one poll
|
|
363
421
|
// cycle (+margin) and re-check before applying via setContent so the same
|
|
364
422
|
// change isn't inserted twice (Yjs + setContent → duplicated region).
|
|
365
|
-
const PEER_SETTLE_MS = 2500;
|
|
366
|
-
|
|
367
423
|
const apply = (deferred = false) => {
|
|
368
424
|
if (cancelled || editor.isDestroyed) return;
|
|
369
425
|
// In collab mode, defer all reconcile until the shared doc is seeded so we
|
|
370
426
|
// never setContent over an unseeded fragment.
|
|
371
|
-
if (collab &&
|
|
427
|
+
if (collab && !collabSynced) {
|
|
372
428
|
retry = setTimeout(() => apply(deferred), 300);
|
|
373
429
|
return;
|
|
374
430
|
}
|
|
431
|
+
// The seed decision itself is deferred one task so Collaboration can
|
|
432
|
+
// project persisted Y.Doc state before we inspect it. Poll that short
|
|
433
|
+
// handoff promptly: waiting the full provider cadence here makes a newer
|
|
434
|
+
// canonical SQL snapshot visibly stale after reload.
|
|
435
|
+
if (collab && !seededRef.current) {
|
|
436
|
+
retry = setTimeout(() => apply(deferred), 25);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (collab && emptySnapshotDecisionPendingRef.current) {
|
|
440
|
+
retry = setTimeout(() => apply(deferred), 50);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
375
443
|
const currentMarkdown = getMarkdown(editor);
|
|
376
444
|
// Compare against the canonical form the editor would emit so a serializer
|
|
377
445
|
// that re-normalizes (Content's NFM) still recognizes "already in sync".
|