@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
@@ -21,7 +21,6 @@ import {
21
21
  TooltipTrigger,
22
22
  } from "@/components/ui/tooltip";
23
23
  import {
24
- useComments,
25
24
  useCreateComment,
26
25
  useResolveComment,
27
26
  type CommentThread,
@@ -88,26 +87,35 @@ function formatDate(dateStr: string) {
88
87
  return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
89
88
  }
90
89
 
90
+ function cssEscape(value: string) {
91
+ return globalThis.CSS?.escape
92
+ ? globalThis.CSS.escape(value)
93
+ : value.replace(/["\\]/g, "\\$&");
94
+ }
95
+
91
96
  /**
92
- * Y offset (relative to the scroll container's content) of a thread's inline
97
+ * Y offset (relative to the scroll container's visible viewport) of a thread's inline
93
98
  * highlight. Prefers the real decoration element rendered by the
94
99
  * CommentHighlight plugin (`[data-comment-thread]`); falls back to a text search
95
100
  * by quoted text for legacy/unresolved anchors so older comments still line up.
96
101
  */
97
- function findThreadOffset(
102
+ export function findThreadOffset(
98
103
  threadId: string,
99
104
  quotedText: string | null,
100
105
  scrollContainer: HTMLElement | null,
106
+ positionContainer: HTMLElement | null = scrollContainer,
101
107
  ): number | null {
102
108
  if (!scrollContainer) return null;
103
- const containerRect = scrollContainer.getBoundingClientRect();
109
+ const containerRect = (
110
+ positionContainer ?? scrollContainer
111
+ ).getBoundingClientRect();
104
112
 
105
113
  const marked = scrollContainer.querySelector(
106
- `[data-comment-thread="${CSS.escape(threadId)}"]`,
114
+ `[data-comment-thread="${cssEscape(threadId)}"]`,
107
115
  ) as HTMLElement | null;
108
116
  if (marked) {
109
117
  const rect = marked.getBoundingClientRect();
110
- return rect.top - containerRect.top + scrollContainer.scrollTop;
118
+ return rect.top - containerRect.top;
111
119
  }
112
120
 
113
121
  if (!quotedText) return null;
@@ -125,14 +133,46 @@ function findThreadOffset(
125
133
  const range = window.document.createRange();
126
134
  range.selectNode(node);
127
135
  const rect = range.getBoundingClientRect();
128
- return rect.top - containerRect.top + scrollContainer.scrollTop;
136
+ return rect.top - containerRect.top;
129
137
  }
130
138
  }
131
139
  return null;
132
140
  }
133
141
 
142
+ export function findPendingCommentOffset(
143
+ scrollContainer: HTMLElement | null,
144
+ positionContainer: HTMLElement | null = scrollContainer,
145
+ ): number | null {
146
+ if (!scrollContainer) return null;
147
+ const pending = scrollContainer.querySelector(
148
+ ".comment-highlight--pending",
149
+ ) as HTMLElement | null;
150
+ if (!pending) return null;
151
+ const containerRect = (
152
+ positionContainer ?? scrollContainer
153
+ ).getBoundingClientRect();
154
+ const rect = pending.getBoundingClientRect();
155
+ return rect.top - containerRect.top;
156
+ }
157
+
158
+ export function shouldClearSelectedThreadOnScroll(
159
+ offset: number | null,
160
+ containerHeight: number,
161
+ margin = 40,
162
+ ) {
163
+ return (
164
+ offset == null || offset < -margin || offset > containerHeight + margin
165
+ );
166
+ }
167
+
168
+ export function estimateThreadCardHeight(thread: CommentThread) {
169
+ return 80 + Math.max(0, thread.comments.length - 1) * 44;
170
+ }
171
+
134
172
  interface CommentsSidebarProps {
135
173
  documentId: string;
174
+ threads?: CommentThread[];
175
+ isLoading?: boolean;
136
176
  pendingComment?: {
137
177
  quotedText: string;
138
178
  offsetTop: number;
@@ -142,30 +182,36 @@ interface CommentsSidebarProps {
142
182
  onPendingDone?: () => void;
143
183
  scrollContainerRef?: RefObject<HTMLDivElement | null>;
144
184
  activeThreadId?: string | null;
145
- onActiveThreadChange?: (id: string | null) => void;
185
+ selectedThreadId?: string | null;
186
+ onSelectedThreadChange?: (id: string | null) => void;
187
+ onHoveredThreadChange?: (id: string | null) => void;
146
188
  currentUserEmail?: string;
147
189
  }
148
190
 
149
191
  export function CommentsSidebar({
150
192
  documentId,
193
+ threads = [],
194
+ isLoading = false,
151
195
  pendingComment,
152
196
  onPendingDone,
153
197
  scrollContainerRef,
154
198
  activeThreadId,
155
- onActiveThreadChange,
199
+ selectedThreadId,
200
+ onSelectedThreadChange,
201
+ onHoveredThreadChange,
156
202
  currentUserEmail,
157
203
  }: CommentsSidebarProps) {
158
204
  const t = useT();
159
- const { data: threads, isLoading } = useComments(documentId);
160
205
  const { data: members = [] } = useMentionMembers();
161
206
  const createComment = useCreateComment();
162
207
  const resolveComment = useResolveComment();
163
- const [expandedThread, setExpandedThread] = useState<string | null>(null);
208
+ const [replyingThreadId, setReplyingThreadId] = useState<string | null>(null);
164
209
  const [replyText, setReplyText] = useState("");
165
210
  const [replyMentions, setReplyMentions] = useState<MentionEntry[]>([]);
166
211
  const [pendingText, setPendingText] = useState("");
167
212
  const [pendingMentions, setPendingMentions] = useState<MentionEntry[]>([]);
168
213
  const [showResolved, setShowResolved] = useState(false);
214
+ const sidebarRef = useRef<HTMLDivElement>(null);
169
215
  const pendingInputRef = useRef<HTMLTextAreaElement>(null);
170
216
 
171
217
  const openThreads = useMemo(
@@ -225,7 +271,7 @@ export function CommentsSidebar({
225
271
  });
226
272
  setReplyText("");
227
273
  setReplyMentions([]);
228
- setExpandedThread(null);
274
+ setReplyingThreadId(null);
229
275
  };
230
276
 
231
277
  const handleSendToAI = (thread: CommentThread) => {
@@ -246,25 +292,52 @@ export function CommentsSidebar({
246
292
  const [threadOffsets, setThreadOffsets] = useState<Map<string, number>>(
247
293
  new Map(),
248
294
  );
295
+ const [threadCardHeights, setThreadCardHeights] = useState<
296
+ Map<string, number>
297
+ >(new Map());
298
+ const [pendingOffset, setPendingOffset] = useState<number | null>(null);
249
299
  const openThreadKey = openThreads
250
300
  .map((t) => `${t.threadId}:${t.quotedText ?? ""}`)
251
301
  .join(",");
252
302
 
303
+ const handleThreadCardHeightChange = useCallback(
304
+ (threadId: string, height: number) => {
305
+ setThreadCardHeights((prev) => {
306
+ if (prev.get(threadId) === height) return prev;
307
+ const next = new Map(prev);
308
+ next.set(threadId, height);
309
+ return next;
310
+ });
311
+ },
312
+ [],
313
+ );
314
+
253
315
  const recomputeOffsets = useCallback(() => {
254
316
  const container = scrollContainerRef?.current ?? null;
255
317
  if (!container || openThreads.length === 0) {
256
318
  setThreadOffsets((prev) => (prev.size === 0 ? prev : new Map()));
319
+ setPendingOffset((prev) => {
320
+ const next = pendingComment
321
+ ? findPendingCommentOffset(container, sidebarRef.current ?? container)
322
+ : null;
323
+ return prev === next ? prev : next;
324
+ });
257
325
  return;
258
326
  }
327
+ const positionContainer = sidebarRef.current ?? container;
259
328
  const offsets = new Map<string, number>();
260
329
  for (const thread of openThreads) {
261
330
  const offset = findThreadOffset(
262
331
  thread.threadId,
263
332
  thread.quotedText,
264
333
  container,
334
+ positionContainer,
265
335
  );
266
336
  if (offset != null) offsets.set(thread.threadId, offset);
267
337
  }
338
+ const nextPendingOffset = pendingComment
339
+ ? findPendingCommentOffset(container, positionContainer)
340
+ : null;
268
341
  setThreadOffsets((prev) => {
269
342
  if (
270
343
  prev.size === offsets.size &&
@@ -274,7 +347,28 @@ export function CommentsSidebar({
274
347
  }
275
348
  return offsets;
276
349
  });
277
- }, [openThreads, scrollContainerRef]);
350
+ setPendingOffset((prev) =>
351
+ prev === nextPendingOffset ? prev : nextPendingOffset,
352
+ );
353
+ if (
354
+ selectedThreadId &&
355
+ shouldClearSelectedThreadOnScroll(
356
+ offsets.get(selectedThreadId) ?? null,
357
+ container.clientHeight,
358
+ )
359
+ ) {
360
+ onSelectedThreadChange?.(null);
361
+ setReplyingThreadId(null);
362
+ setReplyText("");
363
+ setReplyMentions([]);
364
+ }
365
+ }, [
366
+ openThreads,
367
+ onSelectedThreadChange,
368
+ pendingComment,
369
+ scrollContainerRef,
370
+ selectedThreadId,
371
+ ]);
278
372
 
279
373
  useEffect(() => {
280
374
  const container = scrollContainerRef?.current ?? null;
@@ -294,34 +388,46 @@ export function CommentsSidebar({
294
388
  subtree: true,
295
389
  characterData: true,
296
390
  });
391
+ container.addEventListener("scroll", schedule, { passive: true });
297
392
  window.addEventListener("resize", schedule);
298
393
 
299
394
  return () => {
300
395
  cancelAnimationFrame(raf);
301
396
  observer.disconnect();
397
+ container.removeEventListener("scroll", schedule);
302
398
  window.removeEventListener("resize", schedule);
303
399
  };
304
400
  // eslint-disable-next-line react-hooks/exhaustive-deps
305
- }, [openThreadKey, recomputeOffsets]);
401
+ }, [openThreadKey, pendingComment, recomputeOffsets]);
306
402
 
307
- // When a thread is activated (e.g. its highlight was clicked), expand it and
308
- // scroll its card into view.
309
403
  useEffect(() => {
310
- if (!activeThreadId) return;
311
- if (!openThreads.some((t) => t.threadId === activeThreadId)) return;
312
- setExpandedThread(activeThreadId);
313
- const card = window.document.querySelector(
314
- `[data-thread-card="${CSS.escape(activeThreadId)}"]`,
315
- );
316
- card?.scrollIntoView({ block: "nearest", behavior: "smooth" });
317
- }, [activeThreadId, openThreads]);
404
+ const openIds = new Set(openThreads.map((thread) => thread.threadId));
405
+ setThreadCardHeights((prev) => {
406
+ if ([...prev.keys()].every((threadId) => openIds.has(threadId))) {
407
+ return prev;
408
+ }
409
+ const next = new Map<string, number>();
410
+ for (const [threadId, height] of prev) {
411
+ if (openIds.has(threadId)) next.set(threadId, height);
412
+ }
413
+ return next;
414
+ });
415
+ }, [openThreads]);
318
416
 
319
- // Surface the resolved list automatically when there's nothing open to show.
320
417
  useEffect(() => {
321
- if (openThreads.length === 0 && !pendingComment && resolvedThreads.length) {
322
- setShowResolved(true);
418
+ if (!selectedThreadId) return;
419
+ if (!openThreads.some((t) => t.threadId === selectedThreadId)) {
420
+ onSelectedThreadChange?.(null);
421
+ setReplyingThreadId(null);
422
+ setReplyText("");
423
+ setReplyMentions([]);
424
+ return;
323
425
  }
324
- }, [openThreads.length, pendingComment, resolvedThreads.length]);
426
+ const card = window.document.querySelector(
427
+ `[data-thread-card="${cssEscape(selectedThreadId)}"]`,
428
+ );
429
+ card?.scrollIntoView({ block: "nearest", behavior: "smooth" });
430
+ }, [onSelectedThreadChange, selectedThreadId, openThreads]);
325
431
 
326
432
  const hasContent =
327
433
  openThreads.length > 0 || !!pendingComment || resolvedThreads.length > 0;
@@ -346,7 +452,10 @@ export function CommentsSidebar({
346
452
  ? 0
347
453
  : 12;
348
454
  items.push({ thread, marginTop });
349
- cursor += marginTop + 80 + (thread.comments.length - 1) * 44;
455
+ cursor +=
456
+ marginTop +
457
+ (threadCardHeights.get(thread.threadId) ??
458
+ estimateThreadCardHeight(thread));
350
459
  }
351
460
 
352
461
  const handleResolve = (thread: CommentThread) => {
@@ -355,7 +464,12 @@ export function CommentsSidebar({
355
464
  documentId,
356
465
  resolved: true,
357
466
  });
358
- if (activeThreadId === thread.threadId) onActiveThreadChange?.(null);
467
+ if (selectedThreadId === thread.threadId) onSelectedThreadChange?.(null);
468
+ if (replyingThreadId === thread.threadId) {
469
+ setReplyingThreadId(null);
470
+ setReplyText("");
471
+ setReplyMentions([]);
472
+ }
359
473
  };
360
474
 
361
475
  const handleReopen = (thread: CommentThread) => {
@@ -367,12 +481,16 @@ export function CommentsSidebar({
367
481
  };
368
482
 
369
483
  return (
370
- <div className="w-80 shrink-0 overflow-auto relative">
484
+ <div
485
+ ref={sidebarRef}
486
+ className="relative w-80 shrink-0 pb-16"
487
+ data-comments-sidebar
488
+ >
371
489
  {/* Pending new comment — positioned at the selection Y offset */}
372
490
  {pendingComment && (
373
491
  <div
374
492
  className="absolute left-2 right-4 rounded-lg bg-popover p-3 shadow-md ring-1 ring-border/50 z-10"
375
- style={{ top: pendingComment.offsetTop }}
493
+ style={{ top: pendingOffset ?? pendingComment.offsetTop }}
376
494
  >
377
495
  <CommentComposer
378
496
  ref={pendingInputRef}
@@ -383,11 +501,6 @@ export function CommentsSidebar({
383
501
  onEscape={() => {
384
502
  if (!pendingText.trim()) handlePendingCancel();
385
503
  }}
386
- onBlur={() => {
387
- setTimeout(() => {
388
- if (!pendingText.trim()) handlePendingCancel();
389
- }, 200);
390
- }}
391
504
  members={members}
392
505
  placeholder={t("comments.add")}
393
506
  autoFocus
@@ -417,25 +530,28 @@ export function CommentsSidebar({
417
530
  thread={thread}
418
531
  marginTop={marginTop}
419
532
  isActive={activeThreadId === thread.threadId}
420
- isExpanded={expandedThread === thread.threadId}
421
- replyText={expandedThread === thread.threadId ? replyText : ""}
533
+ isExpanded={replyingThreadId === thread.threadId}
534
+ replyText={replyingThreadId === thread.threadId ? replyText : ""}
422
535
  onHoverChange={(hovered) =>
423
- onActiveThreadChange?.(hovered ? thread.threadId : null)
536
+ onHoveredThreadChange?.(hovered ? thread.threadId : null)
424
537
  }
425
538
  onExpand={() => {
426
- setExpandedThread(
427
- expandedThread === thread.threadId ? null : thread.threadId,
539
+ onSelectedThreadChange?.(thread.threadId);
540
+ setReplyingThreadId((current) =>
541
+ current === thread.threadId ? null : thread.threadId,
428
542
  );
429
543
  setReplyText("");
430
544
  setReplyMentions([]);
431
545
  }}
432
546
  onCollapse={() => {
433
- setExpandedThread(null);
547
+ setReplyingThreadId(null);
548
+ onSelectedThreadChange?.(null);
434
549
  setReplyText("");
435
550
  setReplyMentions([]);
436
551
  }}
437
552
  onReplyChange={setReplyText}
438
553
  onReplyMentionAdd={(m) => setReplyMentions((prev) => [...prev, m])}
554
+ onHeightChange={handleThreadCardHeightChange}
439
555
  members={members}
440
556
  onSubmitReply={() => handleReply(thread.threadId)}
441
557
  onResolve={() => handleResolve(thread)}
@@ -487,6 +603,7 @@ function ThreadView({
487
603
  onCollapse,
488
604
  onReplyChange,
489
605
  onReplyMentionAdd,
606
+ onHeightChange,
490
607
  onSubmitReply,
491
608
  onResolve,
492
609
  onSendToAI,
@@ -503,12 +620,14 @@ function ThreadView({
503
620
  onCollapse: () => void;
504
621
  onReplyChange: (text: string) => void;
505
622
  onReplyMentionAdd: (entry: MentionEntry) => void;
623
+ onHeightChange: (threadId: string, height: number) => void;
506
624
  onSubmitReply: () => void;
507
625
  onResolve: () => void;
508
626
  onSendToAI: () => void;
509
627
  t: ReturnType<typeof useT>;
510
628
  }) {
511
629
  const replyInputRef = useRef<HTMLTextAreaElement>(null);
630
+ const cardRef = useRef<HTMLDivElement>(null);
512
631
 
513
632
  useEffect(() => {
514
633
  if (isExpanded) {
@@ -516,8 +635,22 @@ function ThreadView({
516
635
  }
517
636
  }, [isExpanded]);
518
637
 
638
+ useEffect(() => {
639
+ const element = cardRef.current;
640
+ if (!element) return;
641
+ const updateHeight = () => {
642
+ onHeightChange(thread.threadId, element.getBoundingClientRect().height);
643
+ };
644
+ updateHeight();
645
+ if (typeof ResizeObserver === "undefined") return;
646
+ const observer = new ResizeObserver(updateHeight);
647
+ observer.observe(element);
648
+ return () => observer.disconnect();
649
+ }, [onHeightChange, thread.threadId]);
650
+
519
651
  return (
520
652
  <div
653
+ ref={cardRef}
521
654
  data-thread-card={thread.threadId}
522
655
  className={`group/thread mx-2 mr-4 rounded-lg bg-popover shadow-md cursor-pointer transition-shadow ${
523
656
  isActive
@@ -586,7 +719,7 @@ function ThreadView({
586
719
  ))}
587
720
  </div>
588
721
 
589
- {/* Expanded: Notion-style reply input — collapses on blur */}
722
+ {/* Expanded: Notion-style reply input */}
590
723
  {isExpanded && (
591
724
  <div
592
725
  className="flex items-center gap-2 px-3 pb-3 pt-1"
@@ -610,11 +743,6 @@ function ThreadView({
610
743
  onMentionAdd={onReplyMentionAdd}
611
744
  onSubmit={onSubmitReply}
612
745
  onEscape={onCollapse}
613
- onBlur={() => {
614
- setTimeout(() => {
615
- if (!replyText.trim()) onCollapse();
616
- }, 200);
617
- }}
618
746
  members={members}
619
747
  placeholder={t("comments.reply")}
620
748
  rows={1}