@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
@@ -1163,6 +1163,11 @@ const editorToolbarMessages = {
1163
1163
  conflictResolvedFromNotion: "Conflict resolved from Notion.",
1164
1164
  connectNotionWorkspaceToLink:
1165
1165
  "Connect a Notion workspace to link this document.",
1166
+ copiedPageLink: "Copied page link",
1167
+ copyPageLink: "Copy page link",
1168
+ couldNotCopyLink: "Could not copy link",
1169
+ clipboardAccessUnavailable:
1170
+ "Clipboard access is not available in this browser.",
1166
1171
  edited: "Edited",
1167
1172
  export: "Export",
1168
1173
  exportFailed: "Export failed",
@@ -1197,6 +1202,7 @@ const editorToolbarMessages = {
1197
1202
  openInNotion: "Open in Notion",
1198
1203
  orgCanFindAndView: "Anyone in your organization can find and view",
1199
1204
  orgLinkCanView: "Anyone in your organization with the link can view",
1205
+ pageBreadcrumb: "Page breadcrumb",
1200
1206
  printDialogOpened: "Print dialog opened",
1201
1207
  printDialogOpenedDescription: "Choose Save as PDF to finish the export.",
1202
1208
  pulledFromNotion: "Pulled from Notion.",
@@ -1443,6 +1449,7 @@ const enUS = {
1443
1449
  describeWhatToGenerate: "Describe what to generate...",
1444
1450
  enterToSubmit: "Enter to submit",
1445
1451
  failedToCreateDatabase: "Failed to create database",
1452
+ failedToCreatePage: "Failed to create page",
1446
1453
  generateWithAi: "Generate with AI",
1447
1454
  dropMedia: "Drop media",
1448
1455
  editLink: "Edit link",
@@ -7524,21 +7531,130 @@ export const messagesByLocale = {
7524
7531
  } satisfies Record<LocaleCode, Messages>;
7525
7532
 
7526
7533
  const contentExactEnglishTranslations = {
7527
- "zh-CN": { editor: { versionRestoreQuestion: "恢复此版本?" } },
7528
- "es-ES": { editor: { versionRestoreQuestion: "¿Restaurar esta versión?" } },
7529
- "fr-FR": { editor: { versionRestoreQuestion: "Restaurer cette version ?" } },
7534
+ "zh-CN": {
7535
+ editor: {
7536
+ failedToCreatePage: "创建页面失败",
7537
+ toolbar: {
7538
+ copiedPageLink: "已复制页面链接",
7539
+ copyPageLink: "复制页面链接",
7540
+ couldNotCopyLink: "无法复制链接",
7541
+ clipboardAccessUnavailable: "此浏览器无法访问剪贴板。",
7542
+ pageBreadcrumb: "页面面包屑",
7543
+ },
7544
+ versionRestoreQuestion: "恢复此版本?",
7545
+ },
7546
+ },
7547
+ "es-ES": {
7548
+ editor: {
7549
+ failedToCreatePage: "No se pudo crear la página",
7550
+ toolbar: {
7551
+ copiedPageLink: "Enlace de página copiado",
7552
+ copyPageLink: "Copiar enlace de página",
7553
+ couldNotCopyLink: "No se pudo copiar el enlace",
7554
+ clipboardAccessUnavailable:
7555
+ "El acceso al portapapeles no está disponible en este navegador.",
7556
+ pageBreadcrumb: "Ruta de la página",
7557
+ },
7558
+ versionRestoreQuestion: "¿Restaurar esta versión?",
7559
+ },
7560
+ },
7561
+ "fr-FR": {
7562
+ editor: {
7563
+ failedToCreatePage: "Impossible de créer la page",
7564
+ toolbar: {
7565
+ copiedPageLink: "Lien de la page copié",
7566
+ copyPageLink: "Copier le lien de la page",
7567
+ couldNotCopyLink: "Impossible de copier le lien",
7568
+ clipboardAccessUnavailable:
7569
+ "L'accès au presse-papiers n'est pas disponible dans ce navigateur.",
7570
+ pageBreadcrumb: "Fil d'Ariane de la page",
7571
+ },
7572
+ versionRestoreQuestion: "Restaurer cette version ?",
7573
+ },
7574
+ },
7530
7575
  "de-DE": {
7531
- editor: { versionRestoreQuestion: "Diese Version wiederherstellen?" },
7576
+ editor: {
7577
+ failedToCreatePage: "Seite konnte nicht erstellt werden",
7578
+ toolbar: {
7579
+ copiedPageLink: "Seitenlink kopiert",
7580
+ copyPageLink: "Seitenlink kopieren",
7581
+ couldNotCopyLink: "Link konnte nicht kopiert werden",
7582
+ clipboardAccessUnavailable:
7583
+ "Der Zugriff auf die Zwischenablage ist in diesem Browser nicht verfügbar.",
7584
+ pageBreadcrumb: "Seitenpfad",
7585
+ },
7586
+ versionRestoreQuestion: "Diese Version wiederherstellen?",
7587
+ },
7532
7588
  },
7533
7589
  "ja-JP": {
7534
- editor: { versionRestoreQuestion: "このバージョンを復元しますか?" },
7590
+ editor: {
7591
+ failedToCreatePage: "ページを作成できませんでした",
7592
+ toolbar: {
7593
+ copiedPageLink: "ページリンクをコピーしました",
7594
+ copyPageLink: "ページリンクをコピー",
7595
+ couldNotCopyLink: "リンクをコピーできませんでした",
7596
+ clipboardAccessUnavailable:
7597
+ "このブラウザではクリップボードにアクセスできません。",
7598
+ pageBreadcrumb: "ページのパンくずリスト",
7599
+ },
7600
+ versionRestoreQuestion: "このバージョンを復元しますか?",
7601
+ },
7602
+ },
7603
+ "ko-KR": {
7604
+ editor: {
7605
+ failedToCreatePage: "페이지를 만들지 못했습니다",
7606
+ toolbar: {
7607
+ copiedPageLink: "페이지 링크를 복사했습니다",
7608
+ copyPageLink: "페이지 링크 복사",
7609
+ couldNotCopyLink: "링크를 복사하지 못했습니다",
7610
+ clipboardAccessUnavailable:
7611
+ "이 브라우저에서는 클립보드에 접근할 수 없습니다.",
7612
+ pageBreadcrumb: "페이지 이동 경로",
7613
+ },
7614
+ versionRestoreQuestion: "이 버전을 복원할까요?",
7615
+ },
7616
+ },
7617
+ "pt-BR": {
7618
+ editor: {
7619
+ failedToCreatePage: "Não foi possível criar a página",
7620
+ toolbar: {
7621
+ copiedPageLink: "Link da página copiado",
7622
+ copyPageLink: "Copiar link da página",
7623
+ couldNotCopyLink: "Não foi possível copiar o link",
7624
+ clipboardAccessUnavailable:
7625
+ "O acesso à área de transferência não está disponível neste navegador.",
7626
+ pageBreadcrumb: "Caminho da página",
7627
+ },
7628
+ versionRestoreQuestion: "Restaurar esta versão?",
7629
+ },
7535
7630
  },
7536
- "ko-KR": { editor: { versionRestoreQuestion: "이 버전을 복원할까요?" } },
7537
- "pt-BR": { editor: { versionRestoreQuestion: "Restaurar esta versão?" } },
7538
7631
  "hi-IN": {
7539
- editor: { versionRestoreQuestion: "यह संस्करण पुनर्स्थापित करें?" },
7632
+ editor: {
7633
+ failedToCreatePage: "पेज नहीं बनाया जा सका",
7634
+ toolbar: {
7635
+ copiedPageLink: "पेज लिंक कॉपी किया गया",
7636
+ copyPageLink: "पेज लिंक कॉपी करें",
7637
+ couldNotCopyLink: "लिंक कॉपी नहीं किया जा सका",
7638
+ clipboardAccessUnavailable: "इस ब्राउज़र में क्लिपबोर्ड एक्सेस उपलब्ध नहीं है।",
7639
+ pageBreadcrumb: "पेज ब्रेडक्रंब",
7640
+ },
7641
+ versionRestoreQuestion: "यह संस्करण पुनर्स्थापित करें?",
7642
+ },
7643
+ },
7644
+ "ar-SA": {
7645
+ editor: {
7646
+ failedToCreatePage: "تعذر إنشاء الصفحة",
7647
+ toolbar: {
7648
+ copiedPageLink: "تم نسخ رابط الصفحة",
7649
+ copyPageLink: "نسخ رابط الصفحة",
7650
+ couldNotCopyLink: "تعذر نسخ الرابط",
7651
+ clipboardAccessUnavailable:
7652
+ "الوصول إلى الحافظة غير متاح في هذا المتصفح.",
7653
+ pageBreadcrumb: "مسار الصفحة",
7654
+ },
7655
+ versionRestoreQuestion: "استعادة هذا الإصدار؟",
7656
+ },
7540
7657
  },
7541
- "ar-SA": { editor: { versionRestoreQuestion: "استعادة هذا الإصدار؟" } },
7542
7658
  } satisfies Partial<Record<LocaleCode, PartialMessages>>;
7543
7659
 
7544
7660
  for (const [locale, overrides] of Object.entries(
@@ -7549,7 +7665,21 @@ for (const [locale, overrides] of Object.entries(
7549
7665
  for (const [group, groupOverrides] of Object.entries(overrides)) {
7550
7666
  const target = (messages as Record<string, Record<string, unknown>>)[group];
7551
7667
  if (target && groupOverrides && typeof groupOverrides === "object") {
7552
- Object.assign(target, groupOverrides);
7668
+ for (const [key, value] of Object.entries(groupOverrides)) {
7669
+ const nestedTarget = target[key];
7670
+ if (
7671
+ nestedTarget &&
7672
+ value &&
7673
+ typeof nestedTarget === "object" &&
7674
+ typeof value === "object" &&
7675
+ !Array.isArray(nestedTarget) &&
7676
+ !Array.isArray(value)
7677
+ ) {
7678
+ Object.assign(nestedTarget, value);
7679
+ } else {
7680
+ target[key] = value;
7681
+ }
7682
+ }
7553
7683
  }
7554
7684
  }
7555
7685
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Comment cards now track their highlighted text while scrolling and no longer stay open after you click away.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Comments now scroll with the document and stay below the page toolbar.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Creating a database from the slash menu no longer leaves stale command text or a purple selected strip when returning to the page.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Sidebar hover controls no longer leave a lingering fade when moving between pages.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Sidebar page actions are now easier to see when hovering inactive pages.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ Text selections in the editor no longer show white gaps when the formatting toolbar appears.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ The editor toolbar now has a one-click page link copy button next to Share.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ The editor toolbar now shows page breadcrumbs and the latest edit time like Notion.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-25
4
+ ---
5
+
6
+ The /page command now leaves a Notion-style page reference and clears stale cursor labels when you return.
@@ -1798,3 +1798,19 @@ function parseLeafTag(
1798
1798
  export function canonicalizeNfm(nfm: string | null | undefined): string {
1799
1799
  return docToNfm(nfmToDoc(nfm ?? ""));
1800
1800
  }
1801
+
1802
+ export function collapseExactRepeatedNfm(
1803
+ nfm: string,
1804
+ options: { requiredText: string },
1805
+ ): string {
1806
+ if (!options.requiredText || !nfm.includes(options.requiredText)) return nfm;
1807
+
1808
+ const lines = nfm.split("\n");
1809
+ if (lines.length < 2 || lines.length % 2 !== 0) return nfm;
1810
+
1811
+ const midpoint = lines.length / 2;
1812
+ for (let index = 0; index < midpoint; index += 1) {
1813
+ if (lines[index] !== lines[index + midpoint]) return nfm;
1814
+ }
1815
+ return lines.slice(0, midpoint).join("\n");
1816
+ }
@@ -42,7 +42,7 @@ export declare function cleanExpired(map: Map<number, AwarenessEntry>): void;
42
42
  *
43
43
  * Client sends its awareness state and receives other clients' states.
44
44
  *
45
- * Body: { clientId: number, state: string (base64) }
45
+ * Body: { clientId: number, state: string | null (JSON-encoded awareness state, or null to clear) }
46
46
  * Response: { states: Array<{ clientId: number, state: string }> }
47
47
  */
48
48
  export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,CAUN;AAKD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAUD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;;kBAoCa,MAAM;eAAS,MAAM;;GAa1D,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;kBAWM,MAAM;kBAAY,MAAM;;GAMvD,CAAC"}
1
+ {"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,CAUN;AAKD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAUD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;;kBAuCa,MAAM;eAAS,MAAM;;GAa1D,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;kBAWM,MAAM;kBAAY,MAAM;;GAMvD,CAAC"}
@@ -64,7 +64,7 @@ function pruneIfEmpty(docId, map) {
64
64
  *
65
65
  * Client sends its awareness state and receives other clients' states.
66
66
  *
67
- * Body: { clientId: number, state: string (base64) }
67
+ * Body: { clientId: number, state: string | null (JSON-encoded awareness state, or null to clear) }
68
68
  * Response: { states: Array<{ clientId: number, state: string }> }
69
69
  */
70
70
  export const postAwareness = defineEventHandler(async (event) => {
@@ -75,22 +75,26 @@ export const postAwareness = defineEventHandler(async (event) => {
75
75
  }
76
76
  const body = await readBody(event);
77
77
  const { clientId, state } = body;
78
- if (clientId == null || !state) {
78
+ if (clientId == null || state === undefined) {
79
79
  // `!clientId` would wrongly reject clientId === 0, which is a valid
80
- // (if rare) Yjs client id. Only reject missing/null ids here.
80
+ // (if rare) Yjs client id. A null state is valid: it clears this client.
81
81
  setResponseStatus(event, 400);
82
82
  return { error: "clientId and state required" };
83
83
  }
84
84
  const map = getDocAwareness(docId);
85
- // Store this client's state
86
- map.set(clientId, { clientId, state, lastSeen: Date.now() });
85
+ if (state === null) {
86
+ map.delete(clientId);
87
+ }
88
+ else {
89
+ // Store this client's state
90
+ map.set(clientId, { clientId, state, lastSeen: Date.now() });
91
+ }
87
92
  // Clean expired entries, then prune the outer-map entry if it becomes empty.
88
93
  // Without pruning, a deployment with many transient docIds (e.g. one per
89
94
  // session) would grow _awarenessMap without bound.
90
95
  cleanExpired(map);
91
- // map has at least the sender's entry so size >= 1 here; pruneIfEmpty is a
92
- // no-op in the normal path but guards against edge cases (e.g. clientId 0
93
- // that was immediately evicted by a concurrent cleanExpired run).
96
+ // Null-state clears and expiry can empty the map; prune the outer entry so
97
+ // transient document ids do not accumulate.
94
98
  pruneIfEmpty(docId, map);
95
99
  // Build the full list of current states (all clients including sender).
96
100
  const allStates = [];
@@ -1 +1 @@
1
- {"version":3,"file":"awareness.js","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-side awareness state management for collaborative editing.\n *\n * Stores per-client awareness state (cursor positions, user info) in memory.\n * Clients POST their state and receive other clients' states via polling.\n * States expire after 30 seconds of no updates.\n *\n * Fast-path: when a client POSTs new awareness state, the server emits an\n * event on the awareness emitter so SSE-connected peers receive cursor moves\n * push-style instead of waiting for the next poll cycle.\n */\n\nimport { EventEmitter } from \"node:events\";\n\nimport { defineEventHandler, setResponseStatus, getRouterParam } from \"h3\";\nimport type { H3Event } from \"h3\";\n\nimport { readBody } from \"../server/h3-helpers.js\";\n\nconst AWARENESS_TIMEOUT = 30_000; // 30 seconds\n\nexport interface AwarenessEntry {\n clientId: number;\n state: string; // JSON-encoded awareness state object\n lastSeen: number;\n}\n\n// ---------------------------------------------------------------------------\n// Awareness event emitter — fast-path for push delivery to SSE-connected peers.\n// The SSE handler (poll-events) subscribes and forwards events to its stream.\n// ---------------------------------------------------------------------------\n\nexport const AWARENESS_CHANGE_EVENT = \"awareness-change\" as const;\n\nexport interface AwarenessChangeEvent {\n source: \"awareness\";\n type: \"awareness-change\";\n docId: string;\n /** Array of updated states for this document (all non-expired clients). */\n states: Array<{ clientId: number; state: string }>;\n /** Owner email for access-scoped delivery (taken from session if available). */\n owner?: string;\n /** Org ID for org-scoped delivery. */\n orgId?: string;\n}\n\nconst _awarenessEmitter = new EventEmitter();\n_awarenessEmitter.setMaxListeners(0);\n\nexport function getAwarenessEmitter(): EventEmitter {\n return _awarenessEmitter;\n}\n\nexport function emitAwarenessChange(\n docId: string,\n states: Array<{ clientId: number; state: string }>,\n owner?: string,\n orgId?: string,\n): void {\n const event: AwarenessChangeEvent = {\n source: \"awareness\",\n type: \"awareness-change\",\n docId,\n states,\n ...(owner && { owner }),\n ...(orgId && { orgId }),\n };\n _awarenessEmitter.emit(AWARENESS_CHANGE_EVENT, event);\n}\n\n// docId → Map<clientId, AwarenessEntry>\nconst _awarenessMap = new Map<string, Map<number, AwarenessEntry>>();\n\nexport function getDocAwareness(docId: string): Map<number, AwarenessEntry> {\n let map = _awarenessMap.get(docId);\n if (!map) {\n map = new Map();\n _awarenessMap.set(docId, map);\n }\n return map;\n}\n\nexport function cleanExpired(map: Map<number, AwarenessEntry>): void {\n const now = Date.now();\n for (const [clientId, entry] of map) {\n if (now - entry.lastSeen > AWARENESS_TIMEOUT) {\n map.delete(clientId);\n }\n }\n}\n\n// Drop the per-document map from the registry once it has no entries left,\n// so the outer map does not grow unbounded with every docId ever touched.\nfunction pruneIfEmpty(docId: string, map: Map<number, AwarenessEntry>): void {\n if (map.size === 0) {\n _awarenessMap.delete(docId);\n }\n}\n\n/**\n * POST /_agent-native/collab/:docId/awareness\n *\n * Client sends its awareness state and receives other clients' states.\n *\n * Body: { clientId: number, state: string (base64) }\n * Response: { states: Array<{ clientId: number, state: string }> }\n */\nexport const postAwareness = defineEventHandler(async (event: H3Event) => {\n const docId = getRouterParam(event, \"docId\");\n if (!docId) {\n setResponseStatus(event, 400);\n return { error: \"docId required\" };\n }\n\n const body = await readBody(event);\n const { clientId, state } = body as {\n clientId?: number;\n state?: string;\n };\n\n if (clientId == null || !state) {\n // `!clientId` would wrongly reject clientId === 0, which is a valid\n // (if rare) Yjs client id. Only reject missing/null ids here.\n setResponseStatus(event, 400);\n return { error: \"clientId and state required\" };\n }\n\n const map = getDocAwareness(docId);\n\n // Store this client's state\n map.set(clientId, { clientId, state, lastSeen: Date.now() });\n\n // Clean expired entries, then prune the outer-map entry if it becomes empty.\n // Without pruning, a deployment with many transient docIds (e.g. one per\n // session) would grow _awarenessMap without bound.\n cleanExpired(map);\n // map has at least the sender's entry so size >= 1 here; pruneIfEmpty is a\n // no-op in the normal path but guards against edge cases (e.g. clientId 0\n // that was immediately evicted by a concurrent cleanExpired run).\n pruneIfEmpty(docId, map);\n\n // Build the full list of current states (all clients including sender).\n const allStates: Array<{ clientId: number; state: string }> = [];\n const otherStates: Array<{ clientId: number; state: string }> = [];\n for (const [id, entry] of map) {\n allStates.push({ clientId: id, state: entry.state });\n if (id !== clientId) {\n otherStates.push({ clientId: id, state: entry.state });\n }\n }\n\n // Fast-path: push the updated state set to SSE-connected peers so they\n // don't have to wait for the next poll cycle for cursor/selection updates.\n emitAwarenessChange(docId, allStates);\n\n return { states: otherStates };\n});\n\n/**\n * GET /_agent-native/collab/:docId/users\n *\n * Returns the list of active users for a document (for presence bar).\n */\nexport const getActiveUsers = defineEventHandler(async (event: H3Event) => {\n const docId = getRouterParam(event, \"docId\");\n if (!docId) {\n setResponseStatus(event, 400);\n return { error: \"docId required\" };\n }\n\n const map = getDocAwareness(docId);\n cleanExpired(map);\n pruneIfEmpty(docId, map);\n\n const users: Array<{ clientId: number; lastSeen: number }> = [];\n for (const [, entry] of map) {\n users.push({ clientId: entry.clientId, lastSeen: entry.lastSeen });\n }\n\n return { users };\n});\n"]}
1
+ {"version":3,"file":"awareness.js","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-side awareness state management for collaborative editing.\n *\n * Stores per-client awareness state (cursor positions, user info) in memory.\n * Clients POST their state and receive other clients' states via polling.\n * States expire after 30 seconds of no updates.\n *\n * Fast-path: when a client POSTs new awareness state, the server emits an\n * event on the awareness emitter so SSE-connected peers receive cursor moves\n * push-style instead of waiting for the next poll cycle.\n */\n\nimport { EventEmitter } from \"node:events\";\n\nimport { defineEventHandler, setResponseStatus, getRouterParam } from \"h3\";\nimport type { H3Event } from \"h3\";\n\nimport { readBody } from \"../server/h3-helpers.js\";\n\nconst AWARENESS_TIMEOUT = 30_000; // 30 seconds\n\nexport interface AwarenessEntry {\n clientId: number;\n state: string; // JSON-encoded awareness state object\n lastSeen: number;\n}\n\n// ---------------------------------------------------------------------------\n// Awareness event emitter — fast-path for push delivery to SSE-connected peers.\n// The SSE handler (poll-events) subscribes and forwards events to its stream.\n// ---------------------------------------------------------------------------\n\nexport const AWARENESS_CHANGE_EVENT = \"awareness-change\" as const;\n\nexport interface AwarenessChangeEvent {\n source: \"awareness\";\n type: \"awareness-change\";\n docId: string;\n /** Array of updated states for this document (all non-expired clients). */\n states: Array<{ clientId: number; state: string }>;\n /** Owner email for access-scoped delivery (taken from session if available). */\n owner?: string;\n /** Org ID for org-scoped delivery. */\n orgId?: string;\n}\n\nconst _awarenessEmitter = new EventEmitter();\n_awarenessEmitter.setMaxListeners(0);\n\nexport function getAwarenessEmitter(): EventEmitter {\n return _awarenessEmitter;\n}\n\nexport function emitAwarenessChange(\n docId: string,\n states: Array<{ clientId: number; state: string }>,\n owner?: string,\n orgId?: string,\n): void {\n const event: AwarenessChangeEvent = {\n source: \"awareness\",\n type: \"awareness-change\",\n docId,\n states,\n ...(owner && { owner }),\n ...(orgId && { orgId }),\n };\n _awarenessEmitter.emit(AWARENESS_CHANGE_EVENT, event);\n}\n\n// docId → Map<clientId, AwarenessEntry>\nconst _awarenessMap = new Map<string, Map<number, AwarenessEntry>>();\n\nexport function getDocAwareness(docId: string): Map<number, AwarenessEntry> {\n let map = _awarenessMap.get(docId);\n if (!map) {\n map = new Map();\n _awarenessMap.set(docId, map);\n }\n return map;\n}\n\nexport function cleanExpired(map: Map<number, AwarenessEntry>): void {\n const now = Date.now();\n for (const [clientId, entry] of map) {\n if (now - entry.lastSeen > AWARENESS_TIMEOUT) {\n map.delete(clientId);\n }\n }\n}\n\n// Drop the per-document map from the registry once it has no entries left,\n// so the outer map does not grow unbounded with every docId ever touched.\nfunction pruneIfEmpty(docId: string, map: Map<number, AwarenessEntry>): void {\n if (map.size === 0) {\n _awarenessMap.delete(docId);\n }\n}\n\n/**\n * POST /_agent-native/collab/:docId/awareness\n *\n * Client sends its awareness state and receives other clients' states.\n *\n * Body: { clientId: number, state: string | null (JSON-encoded awareness state, or null to clear) }\n * Response: { states: Array<{ clientId: number, state: string }> }\n */\nexport const postAwareness = defineEventHandler(async (event: H3Event) => {\n const docId = getRouterParam(event, \"docId\");\n if (!docId) {\n setResponseStatus(event, 400);\n return { error: \"docId required\" };\n }\n\n const body = await readBody(event);\n const { clientId, state } = body as {\n clientId?: number;\n state?: string | null;\n };\n\n if (clientId == null || state === undefined) {\n // `!clientId` would wrongly reject clientId === 0, which is a valid\n // (if rare) Yjs client id. A null state is valid: it clears this client.\n setResponseStatus(event, 400);\n return { error: \"clientId and state required\" };\n }\n\n const map = getDocAwareness(docId);\n\n if (state === null) {\n map.delete(clientId);\n } else {\n // Store this client's state\n map.set(clientId, { clientId, state, lastSeen: Date.now() });\n }\n\n // Clean expired entries, then prune the outer-map entry if it becomes empty.\n // Without pruning, a deployment with many transient docIds (e.g. one per\n // session) would grow _awarenessMap without bound.\n cleanExpired(map);\n // Null-state clears and expiry can empty the map; prune the outer entry so\n // transient document ids do not accumulate.\n pruneIfEmpty(docId, map);\n\n // Build the full list of current states (all clients including sender).\n const allStates: Array<{ clientId: number; state: string }> = [];\n const otherStates: Array<{ clientId: number; state: string }> = [];\n for (const [id, entry] of map) {\n allStates.push({ clientId: id, state: entry.state });\n if (id !== clientId) {\n otherStates.push({ clientId: id, state: entry.state });\n }\n }\n\n // Fast-path: push the updated state set to SSE-connected peers so they\n // don't have to wait for the next poll cycle for cursor/selection updates.\n emitAwarenessChange(docId, allStates);\n\n return { states: otherStates };\n});\n\n/**\n * GET /_agent-native/collab/:docId/users\n *\n * Returns the list of active users for a document (for presence bar).\n */\nexport const getActiveUsers = defineEventHandler(async (event: H3Event) => {\n const docId = getRouterParam(event, \"docId\");\n if (!docId) {\n setResponseStatus(event, 400);\n return { error: \"docId required\" };\n }\n\n const map = getDocAwareness(docId);\n cleanExpired(map);\n pruneIfEmpty(docId, map);\n\n const users: Array<{ clientId: number; lastSeen: number }> = [];\n for (const [, entry] of map) {\n users.push({ clientId: entry.clientId, lastSeen: entry.lastSeen });\n }\n\n return { users };\n});\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/collab/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAKzB,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC,4EAA4E;IAC5E,IAAI,EAAE,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC;IACnB,uDAAuD;IACvD,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;IAC5B,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,QAAQ,EAAE,OAAO,CAAC;IAClB,sDAAsD;IACtD,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,6EAA6E;IAC7E,WAAW,EAAE,OAAO,CAAC;IACrB,+EAA+E;IAC/E,YAAY,EAAE,OAAO,CAAC;CACvB;AAgBD,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAED,mDAAmD;AACnD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGjD;AAYD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAY1E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,EACvC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,OAAO,CA6BT;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,uBAAuB,EAAE,GACtC;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CA2B3D;AA2ED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,0BAA0B,GAClC,yBAAyB,CAgpB3B"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/collab/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAKzB,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC,4EAA4E;IAC5E,IAAI,EAAE,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC;IACnB,uDAAuD;IACvD,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;IAC5B,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,QAAQ,EAAE,OAAO,CAAC;IAClB,sDAAsD;IACtD,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,6EAA6E;IAC7E,WAAW,EAAE,OAAO,CAAC;IACrB,+EAA+E;IAC/E,YAAY,EAAE,OAAO,CAAC;CACvB;AAgBD,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAED,mDAAmD;AACnD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGjD;AAYD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAY1E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,EACvC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,OAAO,CA6BT;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,uBAAuB,EAAE,GACtC;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CA2B3D;AA6ED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,0BAA0B,GAClC,yBAAyB,CAgpB3B"}
@@ -197,12 +197,13 @@ function scheduleAwarenessPush(baseUrl, docId, clientId, getState) {
197
197
  const timer = setTimeout(() => {
198
198
  _awarenessThrottleTimers.delete(key);
199
199
  const state = getState();
200
- if (!state)
201
- return;
202
200
  fetch(`${baseUrl}/${docId}/awareness`, {
203
201
  method: "POST",
204
202
  headers: { "Content-Type": "application/json" },
205
- body: JSON.stringify({ clientId, state: JSON.stringify(state) }),
203
+ body: JSON.stringify({
204
+ clientId,
205
+ state: state ? JSON.stringify(state) : null,
206
+ }),
206
207
  }).catch(() => { }); // best-effort; poll cycle is the baseline fallback
207
208
  }, 150);
208
209
  _awarenessThrottleTimers.set(key, timer);