@agent-native/core 0.159.2 → 0.159.3

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 (34) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +11 -6
  3. package/corpus/templates/clips/AGENTS.md +1 -0
  4. package/corpus/templates/clips/actions/lib/meeting-content.ts +15 -4
  5. package/corpus/templates/clips/actions/list-meetings.ts +179 -42
  6. package/corpus/templates/clips/actions/search-meetings.ts +277 -0
  7. package/corpus/templates/clips/app/components/meetings/agenda-card.tsx +273 -0
  8. package/corpus/templates/clips/app/components/meetings/day-grouped-card.tsx +113 -0
  9. package/corpus/templates/clips/app/components/meetings/meeting-history-row.tsx +117 -0
  10. package/corpus/templates/clips/app/hooks/use-navigation-state.ts +12 -2
  11. package/corpus/templates/clips/app/i18n/en-US.ts +7 -2
  12. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +263 -357
  13. package/corpus/templates/clips/changelog/2026-08-14-meetings-history-is-searchable-again.md +6 -0
  14. package/corpus/templates/clips/desktop/design-refs/granola-ux.md +17 -0
  15. package/corpus/templates/content/actions/_database-source-utils.ts +29 -2
  16. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +11 -11
  17. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +11 -50
  18. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +57 -20
  19. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +30 -0
  20. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +204 -51
  21. package/corpus/templates/content/app/global.css +4 -1
  22. package/corpus/templates/content/app/i18n-data.ts +30 -0
  23. package/corpus/templates/content/changelog/2026-08-12-toggle-blocks-now-follow-notion-style-enter-and-shift-tab-be.md +6 -0
  24. package/corpus/templates/content/docs/solutions/2026-08-12-toggle-summary-focus-persistence-shape.md +733 -0
  25. package/corpus/templates/content/shared/builder-mdx.ts +44 -4
  26. package/corpus/templates/slides/actions/get-layout-overflows.ts +65 -2
  27. package/dist/deploy/build.d.ts +6 -4
  28. package/dist/deploy/build.js +27 -11
  29. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  30. package/dist/notifications/routes.d.ts +3 -3
  31. package/dist/observability/routes.d.ts +2 -2
  32. package/dist/secrets/routes.d.ts +6 -6
  33. package/package.json +2 -2
  34. package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +0 -333
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-14
4
+ ---
5
+
6
+ Meetings now opens on your history instead of a wall of upcoming cards: past meetings with notes but no linked recording are back in the list, older ones keep loading as you go, and search reaches attendee names and transcript text so you can find a call by what was said in it.
@@ -91,6 +91,23 @@ Primary (cited inline below):
91
91
  - **+N attendee overflow** is used in the homepage marketing example: `Jim, Michaela +5`. So attendees collapse past ~2 named to a `+N` count.
92
92
  - Empty state copy and detail are not exposed publicly; design ours to match Apple-Notes minimalism (single illustrated/iconic prompt, no marketing fluff).
93
93
 
94
+ ### Home layout, observed 2026-08-14
95
+
96
+ From a first-party screenshot of the current app (supersedes the review-derived
97
+ notes above where they conflict):
98
+
99
+ - Upcoming collapses into **one bordered "Coming up" card** at the top, roughly
100
+ 300px tall for three days. Each day is a left column (`14 / August / Fri`)
101
+ beside its events; only the in-progress event carries a button (`Start now`).
102
+ - Everything below is history, in day groups (`Tue, Aug 11`, `Thu, Aug 6`) that
103
+ **skip empty days** and scroll back indefinitely — no pager, no visible cap.
104
+ - History rows are one line: avatar, title, attendee subtitle (`Elaine`,
105
+ `Lisa, Cody & 2 others`), right-aligned time. Notes with no attendees get a
106
+ document icon and `Me`. No status chips anywhere in the list.
107
+ - An **"Ask anything" composer is pinned to the bottom** of the home view with
108
+ a suggestion chip (`List recent todos`) — the same bar as the note view, so
109
+ the agent is reachable over the whole corpus, not just one meeting.
110
+
94
111
  ### Sidebar organisation
95
112
 
96
113
  - Folders: "private" or "shared" (Business Dive, Wonder Tools).
@@ -2965,6 +2965,7 @@ export async function withBuilderBodiesSourceValues(
2965
2965
  export async function builderBodyChangeForLocalContent(args: {
2966
2966
  row: Pick<ContentDatabaseSourceRecordRowDb, "sourceValuesJson">;
2967
2967
  localContent: string | null | undefined;
2968
+ usesCurrentHydrationCodec?: boolean;
2968
2969
  }): Promise<ContentDatabaseSourceBodyChange | null> {
2969
2970
  const sourceValues =
2970
2971
  parseObject<Record<string, DocumentPropertyValue>>(
@@ -2995,6 +2996,14 @@ export async function builderBodyChangeForLocalContent(args: {
2995
2996
  builderBodyUsesCurrentMediaConverter(localContent);
2996
2997
  const normalizedLocalContent =
2997
2998
  normalizeBuilderBodyBaselineContent(localContent);
2999
+ if (
3000
+ args.usesCurrentHydrationCodec &&
3001
+ normalizedLocalContent &&
3002
+ normalizedLocalContent ===
3003
+ normalizeBuilderBodyBaselineContent(currentContent)
3004
+ ) {
3005
+ return null;
3006
+ }
2998
3007
  if (
2999
3008
  !usesCurrentMediaConverter &&
3000
3009
  normalizedLocalContent &&
@@ -3027,7 +3036,11 @@ export async function builderBodyChangeForLocalContent(args: {
3027
3036
  }
3028
3037
 
3029
3038
  try {
3030
- const proposed = usesCurrentMediaConverter
3039
+ const canMergeReadableBaseline =
3040
+ !!losslessContent &&
3041
+ !localContent.includes("<Builder") &&
3042
+ (args.usesCurrentHydrationCodec || !usesCurrentMediaConverter);
3043
+ const proposed = !canMergeReadableBaseline
3031
3044
  ? {
3032
3045
  blocks: await builderMdxBodyToBuilderBlocks(
3033
3046
  normalizeUnsourcedBuilderCreateMdx(localContent),
@@ -3035,7 +3048,7 @@ export async function builderBodyChangeForLocalContent(args: {
3035
3048
  ),
3036
3049
  warnings: [] as string[],
3037
3050
  }
3038
- : losslessContent && !localContent.includes("<Builder")
3051
+ : canMergeReadableBaseline
3039
3052
  ? await builderReadableBodyToBuilderBlocks({
3040
3053
  localContent,
3041
3054
  losslessContent,
@@ -3158,6 +3171,7 @@ export async function builderBodyChangeForSourceSnapshotDocument(args: {
3158
3171
  sourceValuesJson: string;
3159
3172
  };
3160
3173
  isHydrated: boolean;
3174
+ bodyHydrationVersion?: string | null;
3161
3175
  allowUnsourcedCreate: boolean;
3162
3176
  localContent: string | null | undefined;
3163
3177
  }): Promise<ContentDatabaseSourceBodyChange | null> {
@@ -3175,6 +3189,9 @@ export async function builderBodyChangeForSourceSnapshotDocument(args: {
3175
3189
  return builderBodyChangeForLocalContent({
3176
3190
  row: args.row,
3177
3191
  localContent: args.localContent,
3192
+ usesCurrentHydrationCodec:
3193
+ Boolean(args.bodyHydrationVersion) &&
3194
+ !builderBodyHydrationIsCodecMigration(args.bodyHydrationVersion),
3178
3195
  });
3179
3196
  }
3180
3197
  if (!args.allowUnsourcedCreate) return null;
@@ -3973,6 +3990,8 @@ async function readSourceSnapshotRowsOnce(args: {
3973
3990
  id: schema.contentDatabaseItems.id,
3974
3991
  documentId: schema.contentDatabaseItems.documentId,
3975
3992
  bodyHydrationStatus: schema.contentDatabaseItems.bodyHydrationStatus,
3993
+ bodyHydrationVersion:
3994
+ schema.contentDatabaseItems.bodyHydrationVersion,
3976
3995
  })
3977
3996
  .from(schema.contentDatabaseItems)
3978
3997
  .where(
@@ -4398,6 +4417,12 @@ async function loadSourceSnapshot(
4398
4417
  .filter((item) => item.bodyHydrationStatus === "hydrated")
4399
4418
  .map((item) => item.documentId),
4400
4419
  );
4420
+ const bodyHydrationVersionByDocumentId = new Map(
4421
+ databaseItemRows.map((item) => [
4422
+ item.documentId,
4423
+ item.bodyHydrationVersion,
4424
+ ]),
4425
+ );
4401
4426
  await Promise.all(
4402
4427
  allDocumentIds.map(async (documentId) => {
4403
4428
  const row = sourceRowByDocumentId.get(documentId);
@@ -4412,6 +4437,8 @@ async function loadSourceSnapshot(
4412
4437
  bodyChange = await builderBodyChangeForSourceSnapshotDocument({
4413
4438
  row,
4414
4439
  isHydrated: hydratedDocumentIds.has(documentId),
4440
+ bodyHydrationVersion:
4441
+ bodyHydrationVersionByDocumentId.get(documentId) ?? null,
4415
4442
  allowUnsourcedCreate,
4416
4443
  localContent: documentContentById.get(documentId),
4417
4444
  });
@@ -439,16 +439,17 @@ const commands: CommandTemplate[] = [
439
439
  descriptionKey: "editor.slash.toggleDescription",
440
440
  shortcut: ">",
441
441
  icon: IconChevronRight,
442
- action: (editor) => {
443
- editor
444
- .chain()
445
- .focus()
446
- .insertContent({
447
- type: "notionToggle",
448
- attrs: { summary: "", open: true },
449
- content: [{ type: "paragraph" }],
450
- })
451
- .run();
442
+ preserveSlashRange: true,
443
+ action: (editor, { slashRange }) => {
444
+ const toggle = {
445
+ type: "notionToggle",
446
+ attrs: { summary: "", open: true },
447
+ };
448
+ if (slashRange) {
449
+ editor.chain().focus().insertContentAt(slashRange, toggle).run();
450
+ } else {
451
+ editor.chain().focus().insertContent(toggle).run();
452
+ }
452
453
  focusMostRecentEmptyToggleSummary(editor);
453
454
  },
454
455
  },
@@ -553,7 +554,6 @@ const turnIntoCommands: CommandTemplate[] = [
553
554
  .insertContent({
554
555
  type: "notionToggle",
555
556
  attrs: { summary: text, open: true },
556
- content: [{ type: "paragraph" }],
557
557
  })
558
558
  .run();
559
559
  if (!text) focusMostRecentEmptyToggleSummary(editor);
@@ -76,7 +76,6 @@ import {
76
76
  LocalMdxComponentNode,
77
77
  } from "./extensions/LocalMdxComponentNode";
78
78
  import {
79
- EMPTY_TOGGLE_BODY_PLACEHOLDER,
80
79
  createNotionEditorExtensions,
81
80
  focusMostRecentEmptyToggleSummary,
82
81
  type NotionPageLink,
@@ -447,10 +446,7 @@ const NotionMarkdownShortcuts = Extension.create({
447
446
  .replaceWith(
448
447
  shortcut.blockFrom,
449
448
  shortcut.blockTo,
450
- toggle.create(
451
- { summary: "", open: true },
452
- paragraph.create(),
453
- ),
449
+ toggle.create({ summary: "", open: true }),
454
450
  )
455
451
  .scrollIntoView(),
456
452
  );
@@ -478,47 +474,6 @@ const NotionMarkdownShortcuts = Extension.create({
478
474
  },
479
475
  });
480
476
 
481
- const NotionToggleBodyPlaceholder = Extension.create({
482
- name: "notionToggleBodyPlaceholder",
483
-
484
- addProseMirrorPlugins() {
485
- return [
486
- new Plugin({
487
- key: new PluginKey("notionToggleBodyPlaceholder"),
488
- props: {
489
- decorations: ({ doc, selection }) => {
490
- const decorations: Decoration[] = [];
491
-
492
- doc.descendants((node, pos, parent) => {
493
- const selectionIsInsideNode =
494
- selection.from >= pos && selection.to <= pos + node.nodeSize;
495
-
496
- if (
497
- node.type.name !== "paragraph" ||
498
- parent?.type.name !== "notionToggle" ||
499
- node.content.size > 0 ||
500
- node.textContent.trim() ||
501
- selectionIsInsideNode
502
- ) {
503
- return;
504
- }
505
-
506
- decorations.push(
507
- Decoration.node(pos, pos + node.nodeSize, {
508
- class: "is-empty notion-toggle__body-placeholder",
509
- "data-placeholder": EMPTY_TOGGLE_BODY_PLACEHOLDER,
510
- }),
511
- );
512
- });
513
-
514
- return DecorationSet.create(doc, decorations);
515
- },
516
- },
517
- }),
518
- ];
519
- },
520
- });
521
-
522
477
  /**
523
478
  * Tab / Shift-Tab indents any block (paragraph, heading, blockquote, etc.)
524
479
  * by wrapping it in a blockquote — which the NFM pipeline already serializes
@@ -1363,9 +1318,7 @@ function getVisualEditorPlaceholder({
1363
1318
  hasAncestorType(editor, pos, "notionToggle");
1364
1319
 
1365
1320
  if (isToggleBody) {
1366
- return hasAnchor && editor.isFocused
1367
- ? emptyBlockPlaceholder
1368
- : EMPTY_TOGGLE_BODY_PLACEHOLDER;
1321
+ return hasAnchor && editor.isFocused ? emptyBlockPlaceholder : "";
1369
1322
  }
1370
1323
 
1371
1324
  if (node.type.name === "heading") {
@@ -1671,7 +1624,6 @@ export function createVisualEditorExtensions({
1671
1624
  VisualEditorPlaceholder.configure({
1672
1625
  emptyBlockPlaceholder,
1673
1626
  }),
1674
- NotionToggleBodyPlaceholder,
1675
1627
  Link.configure({
1676
1628
  openOnClick: false,
1677
1629
  HTMLAttributes: { class: "notion-link" },
@@ -2044,6 +1996,14 @@ export function VisualEditor({
2044
1996
  ) ?? null
2045
1997
  );
2046
1998
  }, []);
1999
+ const isVisualEditorFocused = useCallback((editor: CoreEditor) => {
2000
+ if (editor.isFocused) return true;
2001
+ const activeElement = editor.view.dom.ownerDocument.activeElement;
2002
+ return Boolean(
2003
+ activeElement?.matches(".notion-toggle__summary") &&
2004
+ editor.view.dom.contains(activeElement),
2005
+ );
2006
+ }, []);
2047
2007
 
2048
2008
  // Reuse the synced Awareness instance when provided; fall back for tests or
2049
2009
  // non-template embedders that only pass a Y.Doc.
@@ -2468,6 +2428,7 @@ export function VisualEditor({
2468
2428
  value: content,
2469
2429
  contentUpdatedAt,
2470
2430
  editable,
2431
+ isEditorFocused: isVisualEditorFocused,
2471
2432
  getMarkdown: (e) => docToNfm(e.getJSON() as any),
2472
2433
  // Read-only viewers join the shared Y.Doc purely to RECEIVE live edits and
2473
2434
  // cursors; their editor content comes from the server state fetch + peer Yjs
@@ -890,6 +890,8 @@ function DatabaseTable({
890
890
  >(null);
891
891
  const [searchOpen, setSearchOpen] = useState(false);
892
892
  const [settingsOpen, setSettingsOpen] = useState(false);
893
+ const [preserveSourceNavigationOnClose, setPreserveSourceNavigationOnClose] =
894
+ useState(false);
893
895
  const sourceHandoffActiveRef = useRef(false);
894
896
  const addPropertyOpenRequestSequenceRef = useRef(0);
895
897
  const [addPropertyOpenRequestId, setAddPropertyOpenRequestId] = useState(0);
@@ -940,12 +942,35 @@ function DatabaseTable({
940
942
  };
941
943
  async function runSourceHandoffMutation<T>(
942
944
  mutation: () => Promise<T>,
945
+ options: { revealOptimisticPreview?: boolean } = {},
943
946
  ): Promise<T | null> {
947
+ const returnToAddProperty = sourceHandoffActiveRef.current;
948
+ if (options.revealOptimisticPreview) {
949
+ sourceHandoffActiveRef.current = false;
950
+ setPreserveSourceNavigationOnClose(true);
951
+ setSettingsOpen(false);
952
+ }
944
953
  try {
945
954
  const result = await mutation();
946
- completeSourceHandoff();
955
+ if (options.revealOptimisticPreview) {
956
+ setPreserveSourceNavigationOnClose(false);
957
+ if (returnToAddProperty) {
958
+ addPropertyOpenRequestSequenceRef.current += 1;
959
+ setAddPropertyOpenRequestId(
960
+ addPropertyOpenRequestSequenceRef.current,
961
+ );
962
+ }
963
+ } else {
964
+ completeSourceHandoff();
965
+ }
947
966
  return result;
948
967
  } catch (error) {
968
+ if (options.revealOptimisticPreview) {
969
+ sourceHandoffActiveRef.current = returnToAddProperty;
970
+ setSettingsPanel("source");
971
+ setSettingsOpen(true);
972
+ setPreserveSourceNavigationOnClose(false);
973
+ }
949
974
  toast.error(dbText("failedToAttachSource"), {
950
975
  description:
951
976
  error instanceof Error ? error.message : dbText("somethingWentWrong"),
@@ -3067,21 +3092,28 @@ function DatabaseTable({
3067
3092
  onClose={closeDatabaseSettings}
3068
3093
  onPanelChange={changeDatabaseSettingsPanel}
3069
3094
  onAttachBuilderSource={(model, relationshipMode) =>
3070
- runSourceHandoffMutation(() =>
3071
- attachSource.mutateAsync({
3072
- documentId: document.id,
3073
- sourceType: "builder-cms",
3074
- sourceName: model.displayName,
3075
- sourceTable: model.name,
3076
- builderFieldPaths: model.fields.map((field) => field.name),
3077
- relationshipMode,
3078
- mode:
3079
- relationshipMode === "items"
3080
- ? "add"
3081
- : sources.length > 0 || source
3082
- ? undefined
3083
- : "replace",
3084
- }),
3095
+ runSourceHandoffMutation(
3096
+ async () => {
3097
+ const result = await attachSource.mutateAsync({
3098
+ documentId: document.id,
3099
+ sourceType: "builder-cms",
3100
+ sourceName: model.displayName,
3101
+ sourceTable: model.name,
3102
+ builderFieldPaths: model.fields.map((field) => field.name),
3103
+ relationshipMode,
3104
+ mode:
3105
+ relationshipMode === "items"
3106
+ ? "add"
3107
+ : sources.length > 0 || source
3108
+ ? undefined
3109
+ : "replace",
3110
+ });
3111
+ if ("responseProjection" in result) {
3112
+ runBuilderHydration(result.sourceId);
3113
+ }
3114
+ return result;
3115
+ },
3116
+ { revealOptimisticPreview: true },
3085
3117
  )
3086
3118
  }
3087
3119
  onFederateSource={(candidate, join) =>
@@ -3194,6 +3226,7 @@ function DatabaseTable({
3194
3226
  setSourceWriteMode.isPending
3195
3227
  }
3196
3228
  sourcePendingOperations={sourcePendingOperations}
3229
+ preserveSourceNavigationOnClose={preserveSourceNavigationOnClose}
3197
3230
  onViewTypeChange={(type) =>
3198
3231
  setViewConfig(updateDatabaseViewType(viewConfig, activeView.id, type))
3199
3232
  }
@@ -7634,6 +7667,7 @@ function DatabaseSettingsPanelSheet({
7634
7667
  onSetBuilderLiveWrites,
7635
7668
  sourceActionPending,
7636
7669
  sourcePendingOperations,
7670
+ preserveSourceNavigationOnClose,
7637
7671
  onViewTypeChange,
7638
7672
  onWrapCellsChange,
7639
7673
  onOpenPagesInChange,
@@ -7681,6 +7715,7 @@ function DatabaseSettingsPanelSheet({
7681
7715
  onSetBuilderLiveWrites: (settings: BuilderSourceWriteSettingsInput) => void;
7682
7716
  sourceActionPending: boolean;
7683
7717
  sourcePendingOperations: DatabaseSourcePendingOperations;
7718
+ preserveSourceNavigationOnClose: boolean;
7684
7719
  onViewTypeChange: (type: ContentDatabaseViewType) => void;
7685
7720
  onWrapCellsChange: (wrapCells: boolean) => void;
7686
7721
  onOpenPagesInChange: (openPagesIn: ContentDatabaseOpenPagesIn) => void;
@@ -7730,10 +7765,12 @@ function DatabaseSettingsPanelSheet({
7730
7765
  );
7731
7766
  }, [builderAttachPreview.data, documentId, queryClient, sourceActionPending]);
7732
7767
  useEffect(() => {
7733
- // Always re-enter the Sources panel at its root, and don't retain a path
7734
- // across close/reopen.
7735
- if (!open || panel !== "source") setSourceNavStack([]);
7736
- }, [open, panel]);
7768
+ // Ordinary close/reopen returns to the root. The optimistic attach handoff
7769
+ // keeps the leaf only long enough to restore a failed mutation in place.
7770
+ if (panel !== "source" || (!open && !preserveSourceNavigationOnClose)) {
7771
+ setSourceNavStack([]);
7772
+ }
7773
+ }, [open, panel, preserveSourceNavigationOnClose]);
7737
7774
 
7738
7775
  if (!open) return null;
7739
7776
 
@@ -801,6 +801,36 @@ export function BuilderSourceReviewDialog({
801
801
  <div className="font-medium">
802
802
  {row.bodyChange.summary}
803
803
  </div>
804
+ {row.bodyChange.currentExcerpt !== null ||
805
+ row.bodyChange.proposedExcerpt !== null ? (
806
+ <div
807
+ className="mt-2 grid gap-2 text-muted-foreground sm:grid-cols-2"
808
+ aria-label={t(
809
+ "database.bodyChangeComparison",
810
+ )}
811
+ >
812
+ {row.bodyChange.currentExcerpt !== null ? (
813
+ <div className="min-w-0">
814
+ <div className="mb-1 font-medium text-foreground">
815
+ {t("database.currentBody")}
816
+ </div>
817
+ <div className="whitespace-pre-wrap break-words">
818
+ {row.bodyChange.currentExcerpt}
819
+ </div>
820
+ </div>
821
+ ) : null}
822
+ {row.bodyChange.proposedExcerpt !== null ? (
823
+ <div className="min-w-0">
824
+ <div className="mb-1 font-medium text-foreground">
825
+ {t("database.proposedBody")}
826
+ </div>
827
+ <div className="whitespace-pre-wrap break-words">
828
+ {row.bodyChange.proposedExcerpt}
829
+ </div>
830
+ </div>
831
+ ) : null}
832
+ </div>
833
+ ) : null}
804
834
  {row.bodyChange.warnings?.length ? (
805
835
  <div className="mt-1 text-muted-foreground">
806
836
  {row.bodyChange.warnings.join(" ")}