@agent-native/core 0.77.10 → 0.77.11

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 (46) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +3 -3
  4. package/corpus/core/src/collab/awareness.ts +12 -9
  5. package/corpus/core/src/collab/client.ts +4 -2
  6. package/corpus/templates/clips/chrome-extension/package.json +1 -1
  7. package/corpus/templates/content/actions/list-trashed-content-databases.ts +36 -2
  8. package/corpus/templates/content/actions/move-document.ts +79 -33
  9. package/corpus/templates/content/actions/restore-content-database.ts +40 -3
  10. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +84 -2
  11. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +177 -49
  12. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +393 -219
  13. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +632 -467
  14. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +114 -14
  15. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +56 -15
  16. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +6 -5
  17. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +12 -4
  18. package/corpus/templates/content/app/global.css +9 -0
  19. package/corpus/templates/content/app/hooks/use-create-page.ts +36 -7
  20. package/corpus/templates/content/app/i18n-data.ts +140 -10
  21. package/corpus/templates/content/changelog/2026-06-25-comment-cards-now-track-their-highlighted-text-while-scrolli.md +6 -0
  22. package/corpus/templates/content/changelog/2026-06-25-comments-now-scroll-with-the-document-and-stay-below-the-pag.md +6 -0
  23. package/corpus/templates/content/changelog/2026-06-25-creating-a-database-from-the-slash-menu-no-longer-leaves-sta.md +6 -0
  24. package/corpus/templates/content/changelog/2026-06-25-sidebar-hover-controls-no-longer-leave-a-lingering-fade-when.md +6 -0
  25. package/corpus/templates/content/changelog/2026-06-25-sidebar-page-actions-are-now-easier-to-see-when-hovering-ina.md +6 -0
  26. package/corpus/templates/content/changelog/2026-06-25-text-selections-in-the-editor-no-longer-show-white-gaps-when.md +6 -0
  27. package/corpus/templates/content/changelog/2026-06-25-the-editor-toolbar-now-has-a-one-click-page-link-copy-button.md +6 -0
  28. package/corpus/templates/content/changelog/2026-06-25-the-editor-toolbar-now-shows-page-breadcrumbs-and-the-latest.md +6 -0
  29. package/corpus/templates/content/changelog/2026-06-25-the-page-command-now-leaves-a-notion-style-page-reference-an.md +6 -0
  30. package/corpus/templates/content/shared/nfm.ts +16 -0
  31. package/dist/collab/awareness.d.ts +1 -1
  32. package/dist/collab/awareness.d.ts.map +1 -1
  33. package/dist/collab/awareness.js +12 -8
  34. package/dist/collab/awareness.js.map +1 -1
  35. package/dist/collab/client.d.ts.map +1 -1
  36. package/dist/collab/client.js +4 -3
  37. package/dist/collab/client.js.map +1 -1
  38. package/dist/collab/routes.d.ts +1 -1
  39. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  40. package/dist/notifications/routes.d.ts +2 -2
  41. package/dist/observability/routes.d.ts +7 -7
  42. package/dist/progress/routes.d.ts +1 -1
  43. package/dist/resources/handlers.d.ts +3 -3
  44. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  45. package/dist/server/transcribe-voice.d.ts +1 -1
  46. package/package.json +3 -3
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 1933
31
- - template files: 4293
31
+ - template files: 4302
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.77.11
4
+
5
+ ### Patch Changes
6
+
7
+ - 7560a7c: Clear collaborative awareness state immediately when an editor leaves a document.
8
+
3
9
  ## 0.77.10
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.77.10",
3
+ "version": "0.77.11",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -211,8 +211,8 @@
211
211
  "@react-router/dev": "^8.0.1",
212
212
  "@react-router/fs-routes": "^8.0.1",
213
213
  "@resvg/resvg-js": "^2.6.2",
214
- "@sentry/browser": "^10.61.0",
215
- "@sentry/node": "^10.61.0",
214
+ "@sentry/browser": "10.60.0",
215
+ "@sentry/node": "10.60.0",
216
216
  "@standard-schema/spec": "^1.1.0",
217
217
  "@tanstack/react-table": "^8.21.3",
218
218
  "@tiptap/core": "3.27.1",
@@ -102,7 +102,7 @@ function pruneIfEmpty(docId: string, map: Map<number, AwarenessEntry>): void {
102
102
  *
103
103
  * Client sends its awareness state and receives other clients' states.
104
104
  *
105
- * Body: { clientId: number, state: string (base64) }
105
+ * Body: { clientId: number, state: string | null (JSON-encoded awareness state, or null to clear) }
106
106
  * Response: { states: Array<{ clientId: number, state: string }> }
107
107
  */
108
108
  export const postAwareness = defineEventHandler(async (event: H3Event) => {
@@ -115,28 +115,31 @@ export const postAwareness = defineEventHandler(async (event: H3Event) => {
115
115
  const body = await readBody(event);
116
116
  const { clientId, state } = body as {
117
117
  clientId?: number;
118
- state?: string;
118
+ state?: string | null;
119
119
  };
120
120
 
121
- if (clientId == null || !state) {
121
+ if (clientId == null || state === undefined) {
122
122
  // `!clientId` would wrongly reject clientId === 0, which is a valid
123
- // (if rare) Yjs client id. Only reject missing/null ids here.
123
+ // (if rare) Yjs client id. A null state is valid: it clears this client.
124
124
  setResponseStatus(event, 400);
125
125
  return { error: "clientId and state required" };
126
126
  }
127
127
 
128
128
  const map = getDocAwareness(docId);
129
129
 
130
- // Store this client's state
131
- map.set(clientId, { clientId, state, lastSeen: Date.now() });
130
+ if (state === null) {
131
+ map.delete(clientId);
132
+ } else {
133
+ // Store this client's state
134
+ map.set(clientId, { clientId, state, lastSeen: Date.now() });
135
+ }
132
136
 
133
137
  // Clean expired entries, then prune the outer-map entry if it becomes empty.
134
138
  // Without pruning, a deployment with many transient docIds (e.g. one per
135
139
  // session) would grow _awarenessMap without bound.
136
140
  cleanExpired(map);
137
- // map has at least the sender's entry so size >= 1 here; pruneIfEmpty is a
138
- // no-op in the normal path but guards against edge cases (e.g. clientId 0
139
- // that was immediately evicted by a concurrent cleanExpired run).
141
+ // Null-state clears and expiry can empty the map; prune the outer entry so
142
+ // transient document ids do not accumulate.
140
143
  pruneIfEmpty(docId, map);
141
144
 
142
145
  // Build the full list of current states (all clients including sender).
@@ -276,11 +276,13 @@ function scheduleAwarenessPush(
276
276
  const timer = setTimeout(() => {
277
277
  _awarenessThrottleTimers.delete(key);
278
278
  const state = getState();
279
- if (!state) return;
280
279
  fetch(`${baseUrl}/${docId}/awareness`, {
281
280
  method: "POST",
282
281
  headers: { "Content-Type": "application/json" },
283
- body: JSON.stringify({ clientId, state: JSON.stringify(state) }),
282
+ body: JSON.stringify({
283
+ clientId,
284
+ state: state ? JSON.stringify(state) : null,
285
+ }),
284
286
  }).catch(() => {}); // best-effort; poll cycle is the baseline fallback
285
287
  }, 150);
286
288
 
@@ -11,7 +11,7 @@
11
11
  "package": "pnpm build && tsx scripts/package.ts"
12
12
  },
13
13
  "dependencies": {
14
- "@sentry/browser": "^10.61.0"
14
+ "@sentry/browser": "10.60.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/chrome": "^0.2.0",
@@ -1,6 +1,7 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { accessFilter } from "@agent-native/core/sharing";
3
- import { and, desc, eq, isNotNull } from "drizzle-orm";
3
+ import { and, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm";
4
+ import { alias } from "drizzle-orm/sqlite-core";
4
5
  import { z } from "zod";
5
6
 
6
7
  import { getDb, schema } from "../server/db/index.js";
@@ -14,6 +15,16 @@ export default defineAction({
14
15
  readOnly: true,
15
16
  run: async (): Promise<ListTrashedContentDatabasesResponse> => {
16
17
  const db = getDb();
18
+ const hostDocuments = alias(schema.documents, "host_documents");
19
+ const isBlockOwned = and(
20
+ isNotNull(schema.contentDatabases.ownerDocumentId),
21
+ eq(schema.documents.parentId, schema.contentDatabases.ownerDocumentId),
22
+ );
23
+ const isNotBlockOwned = or(
24
+ isNull(schema.contentDatabases.ownerDocumentId),
25
+ isNull(schema.documents.parentId),
26
+ ne(schema.documents.parentId, schema.contentDatabases.ownerDocumentId),
27
+ );
17
28
  const rows = await db
18
29
  .select({
19
30
  databaseId: schema.contentDatabases.id,
@@ -28,10 +39,33 @@ export default defineAction({
28
39
  schema.documents,
29
40
  eq(schema.documents.id, schema.contentDatabases.documentId),
30
41
  )
42
+ .leftJoin(
43
+ hostDocuments,
44
+ eq(hostDocuments.id, schema.contentDatabases.ownerDocumentId),
45
+ )
31
46
  .where(
32
47
  and(
33
48
  isNotNull(schema.contentDatabases.deletedAt),
34
- accessFilter(schema.documents, schema.documentShares),
49
+ or(
50
+ and(
51
+ isBlockOwned,
52
+ accessFilter(
53
+ hostDocuments,
54
+ schema.documentShares,
55
+ undefined,
56
+ "editor",
57
+ ),
58
+ ),
59
+ and(
60
+ isNotBlockOwned,
61
+ accessFilter(
62
+ schema.documents,
63
+ schema.documentShares,
64
+ undefined,
65
+ "admin",
66
+ ),
67
+ ),
68
+ ),
35
69
  ),
36
70
  )
37
71
  .orderBy(desc(schema.contentDatabases.deletedAt));
@@ -1,6 +1,6 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
- import { assertAccess } from "@agent-native/core/sharing";
3
+ import { assertAccess, type Visibility } from "@agent-native/core/sharing";
4
4
  import { and, eq, sql } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
 
@@ -54,19 +54,17 @@ async function assertParentIsNotDescendant({
54
54
  }
55
55
  }
56
56
 
57
- async function clearBlockDatabaseOwnershipAfterParentChange({
57
+ async function preflightBlockDatabaseOwnershipClearance({
58
58
  db,
59
59
  documentId,
60
60
  ownerEmail,
61
61
  parentId,
62
- updatedAt,
63
62
  }: {
64
63
  db: ReturnType<typeof getDb>;
65
64
  documentId: string;
66
65
  ownerEmail: string;
67
66
  parentId: string | null;
68
- updatedAt: string;
69
- }) {
67
+ }): Promise<string | null> {
70
68
  const [database] = await db
71
69
  .select({
72
70
  id: schema.contentDatabases.id,
@@ -81,11 +79,24 @@ async function clearBlockDatabaseOwnershipAfterParentChange({
81
79
  );
82
80
 
83
81
  if (!database?.ownerDocumentId || database.ownerDocumentId === parentId) {
84
- return;
82
+ return null;
85
83
  }
86
84
 
87
85
  await assertAccess("document", database.ownerDocumentId, "editor");
86
+ return database.id;
87
+ }
88
88
 
89
+ async function clearBlockDatabaseOwnership({
90
+ db,
91
+ databaseId,
92
+ ownerEmail,
93
+ updatedAt,
94
+ }: {
95
+ db: Pick<ReturnType<typeof getDb>, "update">;
96
+ databaseId: string;
97
+ ownerEmail: string;
98
+ updatedAt: string;
99
+ }) {
89
100
  await db
90
101
  .update(schema.contentDatabases)
91
102
  .set({
@@ -95,23 +106,44 @@ async function clearBlockDatabaseOwnershipAfterParentChange({
95
106
  })
96
107
  .where(
97
108
  and(
98
- eq(schema.contentDatabases.id, database.id),
109
+ eq(schema.contentDatabases.id, databaseId),
99
110
  eq(schema.contentDatabases.ownerEmail, ownerEmail),
100
111
  ),
101
112
  );
102
113
  }
103
114
 
115
+ function sameRootSection(
116
+ left: { visibility: Visibility; orgId?: string | null },
117
+ right: { visibility: Visibility; orgId?: string | null },
118
+ ) {
119
+ return left.visibility === right.visibility && left.orgId === right.orgId;
120
+ }
121
+
122
+ function rootSectionFilter(document: {
123
+ visibility: Visibility;
124
+ orgId?: string | null;
125
+ }) {
126
+ return and(
127
+ eq(schema.documents.visibility, document.visibility),
128
+ document.orgId
129
+ ? eq(schema.documents.orgId, document.orgId)
130
+ : sql`${schema.documents.orgId} IS NULL`,
131
+ );
132
+ }
133
+
104
134
  async function resolveSiblingPositionsAfterMove({
105
135
  db,
106
136
  ownerEmail,
107
137
  id,
108
138
  parentId,
139
+ rootSection,
109
140
  position,
110
141
  }: {
111
142
  db: ReturnType<typeof getDb>;
112
143
  ownerEmail: string;
113
144
  id: string;
114
145
  parentId: string | null;
146
+ rootSection: { visibility: Visibility; orgId?: string | null };
115
147
  position: number;
116
148
  }) {
117
149
  const siblings = await db
@@ -129,6 +161,7 @@ async function resolveSiblingPositionsAfterMove({
129
161
  )
130
162
  : and(
131
163
  eq(schema.documents.ownerEmail, ownerEmail),
164
+ rootSectionFilter(rootSection),
132
165
  sql`parent_id IS NULL`,
133
166
  ),
134
167
  );
@@ -209,6 +242,9 @@ export default defineAction({
209
242
  if (parentAccess.resource.ownerEmail !== ownerEmail) {
210
243
  throw new Error("Parent document must belong to the same owner");
211
244
  }
245
+ if (!sameRootSection(parentAccess.resource, existing)) {
246
+ throw new Error("Parent document must be in the same section");
247
+ }
212
248
  await assertParentIsNotDescendant({
213
249
  db,
214
250
  ownerEmail,
@@ -221,6 +257,15 @@ export default defineAction({
221
257
 
222
258
  const targetParentId =
223
259
  args.parentId !== undefined ? args.parentId : existing.parentId;
260
+ const blockDatabaseIdToDetach =
261
+ args.parentId !== undefined
262
+ ? await preflightBlockDatabaseOwnershipClearance({
263
+ db,
264
+ documentId: id,
265
+ ownerEmail,
266
+ parentId: args.parentId,
267
+ })
268
+ : null;
224
269
  const normalizedSiblingPositions =
225
270
  args.position !== undefined
226
271
  ? await resolveSiblingPositionsAfterMove({
@@ -228,6 +273,7 @@ export default defineAction({
228
273
  ownerEmail,
229
274
  id,
230
275
  parentId: targetParentId,
276
+ rootSection: existing,
231
277
  position: args.position,
232
278
  })
233
279
  : null;
@@ -249,36 +295,36 @@ export default defineAction({
249
295
  )
250
296
  : and(
251
297
  eq(schema.documents.ownerEmail, ownerEmail),
298
+ rootSectionFilter(existing),
252
299
  sql`parent_id IS NULL`,
253
300
  ),
254
301
  );
255
302
  updates.position = (maxPos[0]?.max ?? -1) + 1;
256
303
  }
257
304
 
258
- await db
259
- .update(schema.documents)
260
- .set(updates)
261
- .where(
262
- and(
263
- eq(schema.documents.id, id),
264
- eq(schema.documents.ownerEmail, ownerEmail),
265
- ),
266
- );
305
+ await db.transaction(async (tx) => {
306
+ await tx
307
+ .update(schema.documents)
308
+ .set(updates)
309
+ .where(
310
+ and(
311
+ eq(schema.documents.id, id),
312
+ eq(schema.documents.ownerEmail, ownerEmail),
313
+ ),
314
+ );
267
315
 
268
- if (args.parentId !== undefined) {
269
- await clearBlockDatabaseOwnershipAfterParentChange({
270
- db,
271
- documentId: id,
272
- ownerEmail,
273
- parentId: args.parentId,
274
- updatedAt,
275
- });
276
- }
316
+ if (args.parentId !== undefined && blockDatabaseIdToDetach) {
317
+ await clearBlockDatabaseOwnership({
318
+ db: tx,
319
+ databaseId: blockDatabaseIdToDetach,
320
+ ownerEmail,
321
+ updatedAt,
322
+ });
323
+ }
277
324
 
278
- if (normalizedSiblingPositions) {
279
- await Promise.all(
280
- normalizedSiblingPositions.map((document) =>
281
- db
325
+ if (normalizedSiblingPositions) {
326
+ for (const document of normalizedSiblingPositions) {
327
+ await tx
282
328
  .update(schema.documents)
283
329
  .set({ position: document.position })
284
330
  .where(
@@ -286,10 +332,10 @@ export default defineAction({
286
332
  eq(schema.documents.id, document.id),
287
333
  eq(schema.documents.ownerEmail, ownerEmail),
288
334
  ),
289
- ),
290
- ),
291
- );
292
- }
335
+ );
336
+ }
337
+ }
338
+ });
293
339
 
294
340
  const [doc] = await db
295
341
  .select()
@@ -1,10 +1,37 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
+ import { resolveAccess } from "@agent-native/core/sharing";
3
4
  import { eq } from "drizzle-orm";
4
5
  import { z } from "zod";
5
6
 
6
7
  import { getDb, schema } from "../server/db/index.js";
7
- import { assertContentDatabaseLifecycleAccess } from "./_content-database-lifecycle.js";
8
+ import {
9
+ assertContentDatabaseLifecycleAccess,
10
+ collectInlineDatabaseOwnerBlockIds,
11
+ } from "./_content-database-lifecycle.js";
12
+ import pullDocumentAction from "./pull-document.js";
13
+
14
+ async function shouldClearStaleInlineOwnership(args: {
15
+ ownerDocumentId: string | null;
16
+ ownerBlockId: string | null;
17
+ }) {
18
+ if (!args.ownerDocumentId || !args.ownerBlockId) return false;
19
+ let content: string | null = null;
20
+ try {
21
+ const host = await pullDocumentAction.run({
22
+ id: args.ownerDocumentId,
23
+ format: "markdown",
24
+ });
25
+ content = String(host.content ?? "");
26
+ } catch {
27
+ const hostAccess = await resolveAccess("document", args.ownerDocumentId);
28
+ if (!hostAccess) return false;
29
+ content = String(hostAccess.resource.content ?? "");
30
+ }
31
+
32
+ const parsed = await collectInlineDatabaseOwnerBlockIds(content);
33
+ return parsed.ok && !parsed.ownerBlockIds.has(args.ownerBlockId);
34
+ }
8
35
 
9
36
  export default defineAction({
10
37
  description: "Restore a soft-deleted content database.",
@@ -12,13 +39,23 @@ export default defineAction({
12
39
  databaseId: z.string().describe("Content database ID"),
13
40
  }),
14
41
  run: async ({ databaseId }) => {
15
- await assertContentDatabaseLifecycleAccess(databaseId);
42
+ const ownership = await assertContentDatabaseLifecycleAccess(databaseId);
16
43
  const db = getDb();
17
44
  const now = new Date().toISOString();
45
+ const clearInlineOwnership = await shouldClearStaleInlineOwnership({
46
+ ownerDocumentId: ownership.database.ownerDocumentId,
47
+ ownerBlockId: ownership.database.ownerBlockId,
48
+ });
18
49
 
19
50
  await db
20
51
  .update(schema.contentDatabases)
21
- .set({ deletedAt: null, updatedAt: now })
52
+ .set({
53
+ deletedAt: null,
54
+ updatedAt: now,
55
+ ...(clearInlineOwnership
56
+ ? { ownerDocumentId: null, ownerBlockId: null }
57
+ : {}),
58
+ })
22
59
  .where(eq(schema.contentDatabases.id, databaseId));
23
60
 
24
61
  await writeAppState("refresh-signal", { ts: Date.now() });
@@ -11,10 +11,16 @@ import {
11
11
  IconH3,
12
12
  IconH4,
13
13
  } from "@tabler/icons-react";
14
- import { NodeSelection, type EditorState } from "@tiptap/pm/state";
14
+ import {
15
+ NodeSelection,
16
+ Plugin,
17
+ PluginKey,
18
+ type EditorState,
19
+ } from "@tiptap/pm/state";
20
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
15
21
  import type { Editor } from "@tiptap/react";
16
22
  import { BubbleMenu } from "@tiptap/react/menus";
17
- import { useState } from "react";
23
+ import { useEffect, useState } from "react";
18
24
 
19
25
  import {
20
26
  Tooltip,
@@ -44,6 +50,15 @@ const BUBBLE_TOOLBAR_EXCLUDED_NODE_TYPES = new Set([
44
50
  "localMdxComponent",
45
51
  ]);
46
52
 
53
+ type SelectionFillRange = {
54
+ from: number;
55
+ to: number;
56
+ };
57
+
58
+ const selectionFillPluginKey = new PluginKey<SelectionFillRange | null>(
59
+ "contentSelectionFill",
60
+ );
61
+
47
62
  function selectionIncludesBubbleToolbarExcludedNode(
48
63
  state: EditorState,
49
64
  from: number,
@@ -72,6 +87,73 @@ export function BubbleToolbar({ editor, onComment }: BubbleToolbarProps) {
72
87
  const [showLinkInput, setShowLinkInput] = useState(false);
73
88
  const [linkUrl, setLinkUrl] = useState("");
74
89
 
90
+ useEffect(() => {
91
+ const plugin = new Plugin<SelectionFillRange | null>({
92
+ key: selectionFillPluginKey,
93
+ state: {
94
+ init: () => null,
95
+ apply: (tr, value) => {
96
+ const meta = tr.getMeta(selectionFillPluginKey);
97
+ if (meta !== undefined) return meta;
98
+ return value
99
+ ? {
100
+ from: tr.mapping.map(value.from),
101
+ to: tr.mapping.map(value.to),
102
+ }
103
+ : null;
104
+ },
105
+ },
106
+ props: {
107
+ decorations(state) {
108
+ const range = selectionFillPluginKey.getState(state);
109
+ if (!range || range.from === range.to) return DecorationSet.empty;
110
+ return DecorationSet.create(state.doc, [
111
+ Decoration.inline(range.from, range.to, {
112
+ class: "notion-selection-fill",
113
+ }),
114
+ ]);
115
+ },
116
+ },
117
+ });
118
+
119
+ editor.registerPlugin(plugin);
120
+
121
+ const syncSelectionFill = () => {
122
+ const { state } = editor;
123
+ const { from, to } = state.selection;
124
+ const nextRange =
125
+ editor.isFocused &&
126
+ from !== to &&
127
+ !selectionIncludesBubbleToolbarExcludedNode(state, from, to)
128
+ ? { from, to }
129
+ : null;
130
+ const currentRange = selectionFillPluginKey.getState(state);
131
+ if (
132
+ currentRange?.from === nextRange?.from &&
133
+ currentRange?.to === nextRange?.to
134
+ ) {
135
+ return;
136
+ }
137
+ editor.view.dispatch(
138
+ state.tr
139
+ .setMeta(selectionFillPluginKey, nextRange)
140
+ .setMeta("addToHistory", false),
141
+ );
142
+ };
143
+
144
+ editor.on("selectionUpdate", syncSelectionFill);
145
+ editor.on("focus", syncSelectionFill);
146
+ editor.on("blur", syncSelectionFill);
147
+ syncSelectionFill();
148
+
149
+ return () => {
150
+ editor.off("selectionUpdate", syncSelectionFill);
151
+ editor.off("focus", syncSelectionFill);
152
+ editor.off("blur", syncSelectionFill);
153
+ editor.unregisterPlugin(selectionFillPluginKey);
154
+ };
155
+ }, [editor]);
156
+
75
157
  const handleSetLink = () => {
76
158
  if (linkUrl.trim()) {
77
159
  editor