@agent-native/core 0.185.0 → 0.186.0-nightly-20260923174435

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 (63) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/actions/delete-event.ts +6 -4
  3. package/corpus/templates/calendar/actions/delete-events.ts +10 -11
  4. package/corpus/templates/calendar/actions/event-action-helpers.ts +53 -0
  5. package/corpus/templates/calendar/actions/get-event.ts +59 -14
  6. package/corpus/templates/calendar/actions/rsvp-event.ts +5 -3
  7. package/corpus/templates/calendar/actions/update-event.ts +15 -7
  8. package/corpus/templates/calendar/actions/update-events.ts +29 -11
  9. package/corpus/templates/calendar/changelog/2026-09-21-events-stay-editable-when-their-calendar-is-connected-throug.md +6 -0
  10. package/corpus/templates/calendar/server/lib/google-calendar.ts +70 -18
  11. package/corpus/templates/calendar/shared/google-calendar-sources.ts +47 -0
  12. package/corpus/templates/clips/actions/finalize-recording.ts +35 -1
  13. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +5 -1
  14. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +18 -1
  15. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -2
  16. package/corpus/templates/clips/app/routes/record.tsx +1 -0
  17. package/corpus/templates/clips/changelog/2026-09-23-long-recording-menus-scroll-within-the-visible-space-so-ever.md +6 -0
  18. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -4
  19. package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +9 -1
  20. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +16 -2
  21. package/corpus/templates/content/app/components/editor/registrySlashItems.ts +61 -40
  22. package/corpus/templates/content/app/i18n-data.ts +12 -0
  23. package/corpus/templates/content/app/routes/_app.settings.tsx +36 -9
  24. package/corpus/templates/content/docs/product/capabilities/content.renderer.custom-block.md +1 -1
  25. package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
  26. package/corpus/templates/content/shared/labs.ts +35 -1
  27. package/corpus/templates/slides/actions/get-design-system.ts +4 -2
  28. package/dist/agent/engine/openai-compatible-endpoint.d.ts +7 -0
  29. package/dist/agent/engine/openai-compatible-endpoint.js +9 -0
  30. package/dist/agent/engine/provider-endpoint-validation.d.ts +8 -1
  31. package/dist/agent/engine/provider-endpoint-validation.js +47 -10
  32. package/dist/agent/engine/registry.js +8 -5
  33. package/dist/automation/index.d.ts +1 -1
  34. package/dist/cli/skills.d.ts +3 -0
  35. package/dist/cli/skills.js +45 -9
  36. package/dist/client/agent-engine-key.d.ts +7 -0
  37. package/dist/client/agent-engine-key.js +37 -0
  38. package/dist/client/agent-provider-catalog.js +1 -1
  39. package/dist/client/settings/ProviderSetupForm.js +80 -7
  40. package/dist/client/settings/SettingsPanel.js +149 -11
  41. package/dist/client/use-chat-models.js +31 -0
  42. package/dist/collab/awareness.d.ts +2 -2
  43. package/dist/collab/routes.d.ts +1 -1
  44. package/dist/labs/actions/set-lab.js +1 -0
  45. package/dist/labs/registry.js +6 -1
  46. package/dist/localization/default-messages.d.ts +7 -1
  47. package/dist/localization/default-messages.js +7 -1
  48. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  49. package/dist/notifications/routes.d.ts +3 -3
  50. package/dist/observability/routes.d.ts +3 -3
  51. package/dist/org/index.d.ts +1 -1
  52. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  53. package/dist/provider-api/actions/provider-api.d.ts +3 -3
  54. package/dist/scripts/agent-engines/test-agent-engine.js +9 -7
  55. package/dist/secrets/routes.d.ts +3 -3
  56. package/dist/server/agent-engine-api-key-route.d.ts +3 -3
  57. package/dist/server/agent-engine-api-key-route.js +8 -5
  58. package/dist/server/agent-engine-ollama-models-route.d.ts +15 -0
  59. package/dist/server/agent-engine-ollama-models-route.js +105 -0
  60. package/dist/server/core-routes-plugin.js +5 -0
  61. package/dist/server/credential-provider.d.ts +13 -0
  62. package/dist/server/credential-provider.js +19 -0
  63. package/package.json +3 -3
@@ -3,6 +3,53 @@ export interface GoogleCalendarSourceIdentity {
3
3
  calendarId: string;
4
4
  }
5
5
 
6
+ export interface GoogleAccountEventIdentity {
7
+ accountEmail: string;
8
+ googleEventId: string;
9
+ }
10
+
11
+ const GOOGLE_ACCOUNT_EVENT_PREFIX = "google-account-event:";
12
+
13
+ export function createGoogleAccountEventId({
14
+ accountEmail,
15
+ googleEventId,
16
+ }: GoogleAccountEventIdentity): string {
17
+ return `${GOOGLE_ACCOUNT_EVENT_PREFIX}${Buffer.from(
18
+ JSON.stringify([accountEmail.trim().toLowerCase(), googleEventId]),
19
+ ).toString("base64url")}`;
20
+ }
21
+
22
+ export function parseGoogleAccountEventId(
23
+ id: string,
24
+ ): GoogleAccountEventIdentity | null {
25
+ if (!id.startsWith(GOOGLE_ACCOUNT_EVENT_PREFIX)) return null;
26
+ try {
27
+ const parsed: unknown = JSON.parse(
28
+ Buffer.from(
29
+ id.slice(GOOGLE_ACCOUNT_EVENT_PREFIX.length),
30
+ "base64url",
31
+ ).toString("utf8"),
32
+ );
33
+ if (
34
+ !Array.isArray(parsed) ||
35
+ parsed.length !== 2 ||
36
+ typeof parsed[0] !== "string" ||
37
+ typeof parsed[1] !== "string" ||
38
+ !parsed[0].trim() ||
39
+ !parsed[1]
40
+ ) {
41
+ return null;
42
+ }
43
+ return {
44
+ accountEmail: parsed[0].trim().toLowerCase(),
45
+ googleEventId: parsed[1],
46
+ };
47
+ } catch {
48
+ // coercion-ok: malformed opaque input is a typed invalid-id result.
49
+ return null;
50
+ }
51
+ }
52
+
6
53
  /**
7
54
  * Calendar ids are provider data and may contain separators, so encode the
8
55
  * complete identity rather than joining two user-controlled strings.
@@ -323,8 +323,35 @@ async function failStoredButUnservableRecording(params: {
323
323
  eq(schema.recordings.status, "processing"),
324
324
  ),
325
325
  )
326
- .returning({ id: schema.recordings.id });
326
+ .returning({
327
+ id: schema.recordings.id,
328
+ uploadAttemptId: schema.recordings.uploadAttemptId,
329
+ });
327
330
  if (failed.length !== 1) return false;
331
+ try {
332
+ track(
333
+ "clips_upload_blocking_failure",
334
+ {
335
+ app: "clips",
336
+ template: "clips",
337
+ surface: "media_verification",
338
+ stage: "media_verification",
339
+ outcome: "failed",
340
+ failure_type: "media_verification",
341
+ failure_code: "media_verification_failed",
342
+ output_id: id,
343
+ output_type: "clip",
344
+ recording_id: id,
345
+ recording_attempt_id: id,
346
+ ...(failed[0]?.uploadAttemptId
347
+ ? { upload_attempt_id: failed[0].uploadAttemptId }
348
+ : {}),
349
+ },
350
+ { userId: ownerEmail },
351
+ );
352
+ } catch {
353
+ // coercion-ok: analytics is best-effort and must not change media recovery behavior.
354
+ }
328
355
  const uploadStateRaw = await readAppState(`recording-upload-${id}`).catch(
329
356
  () => null,
330
357
  );
@@ -605,6 +632,7 @@ async function markRecordingReady(params: {
605
632
  finalHeight: number;
606
633
  finalHasAudio: boolean;
607
634
  finalHasCamera: boolean;
635
+ recordingAttemptId?: string | null;
608
636
  existingTitle: string;
609
637
  // Whether a seekable rewrite (MP4 faststart / WebM Cues remux) was already
610
638
  // applied to the uploaded bytes. When false, a best-effort background repair
@@ -623,6 +651,7 @@ async function markRecordingReady(params: {
623
651
  finalHeight,
624
652
  finalHasAudio,
625
653
  finalHasCamera,
654
+ recordingAttemptId,
626
655
  existingTitle,
627
656
  seekableApplied,
628
657
  } = params;
@@ -716,6 +745,8 @@ async function markRecordingReady(params: {
716
745
  template_name: "clips",
717
746
  output_id: id,
718
747
  output_type: "clip",
748
+ recording_attempt_id: id,
749
+ ...(recordingAttemptId ? { upload_attempt_id: recordingAttemptId } : {}),
719
750
  duration_s: Math.round(finalDurationMs / 1000),
720
751
  video_format: videoFormat,
721
752
  has_audio: finalHasAudio,
@@ -864,6 +895,7 @@ async function retryPendingMediaVerification(params: {
864
895
  const db = getDb();
865
896
  const [recording] = await db
866
897
  .select({
898
+ uploadAttemptId: schema.recordings.uploadAttemptId,
867
899
  status: schema.recordings.status,
868
900
  videoUrl: schema.recordings.videoUrl,
869
901
  editsJson: schema.recordings.editsJson,
@@ -919,6 +951,7 @@ async function retryPendingMediaVerification(params: {
919
951
  finalHeight: candidate.finalHeight,
920
952
  finalHasAudio: candidate.finalHasAudio,
921
953
  finalHasCamera: candidate.finalHasCamera,
954
+ recordingAttemptId: recording.uploadAttemptId,
922
955
  seekableApplied: candidate.seekableApplied,
923
956
  });
924
957
  if (result.status === "ready" && result.transitionedToReady) {
@@ -1196,6 +1229,7 @@ export default defineAction({
1196
1229
  finalHeight,
1197
1230
  finalHasAudio,
1198
1231
  finalHasCamera,
1232
+ recordingAttemptId: existing.uploadAttemptId,
1199
1233
  existingTitle: existing.title,
1200
1234
  };
1201
1235
 
@@ -105,7 +105,11 @@ export function RecordingOptionsMenu({
105
105
  </DropdownMenuTrigger>
106
106
  <DropdownMenuContent
107
107
  align="end"
108
- className={showCustomItems ? "w-64" : "w-44"}
108
+ className={
109
+ showCustomItems
110
+ ? "w-64 max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-x-hidden overflow-y-auto"
111
+ : "w-44"
112
+ }
109
113
  onCloseAutoFocus={(event) => {
110
114
  // Opening the AlertDialog while this menu is still tearing down
111
115
  // leaves `pointer-events: none` stuck on <body>: two dismissable
@@ -399,12 +399,27 @@ function isRetryableChunkUploadStatus(status: number): boolean {
399
399
  }
400
400
 
401
401
  function trackClipUploadBlockingFailure(props: Record<string, unknown>): void {
402
+ const recordingId =
403
+ typeof props.recordingId === "string" ? props.recordingId : undefined;
404
+ const uploadAttemptId =
405
+ typeof props.uploadAttemptId === "string"
406
+ ? props.uploadAttemptId
407
+ : undefined;
402
408
  try {
403
409
  trackEvent("clips_upload_blocking_failure", {
410
+ ...props,
404
411
  app: "clips",
405
412
  template: "clips",
406
413
  surface: "web_recorder",
407
- ...props,
414
+ output_id: recordingId,
415
+ output_type: "clip",
416
+ recording_id: recordingId,
417
+ recording_attempt_id: recordingId,
418
+ ...(uploadAttemptId ? { upload_attempt_id: uploadAttemptId } : {}),
419
+ failure_code:
420
+ typeof props.failureKind === "string"
421
+ ? props.failureKind
422
+ : "upload_failed",
408
423
  });
409
424
  } catch {
410
425
  // Analytics should never change recording behavior.
@@ -2623,6 +2638,7 @@ export class RecorderEngine {
2623
2638
  stage: "chunk_upload",
2624
2639
  failureKind: "no_response",
2625
2640
  recordingId: this.opts.recordingId,
2641
+ uploadAttemptId: this.uploadAttemptId,
2626
2642
  chunkIndex: index,
2627
2643
  isFinal: extra.isFinal === true,
2628
2644
  chunkBytes: blob.size,
@@ -2663,6 +2679,7 @@ export class RecorderEngine {
2663
2679
  stage: "chunk_upload",
2664
2680
  failureKind: "http_error",
2665
2681
  recordingId: this.opts.recordingId,
2682
+ uploadAttemptId: this.uploadAttemptId,
2666
2683
  chunkIndex: index,
2667
2684
  isFinal: extra.isFinal === true,
2668
2685
  httpStatus: res.status,
@@ -2347,7 +2347,7 @@ export default function RecordingPage() {
2347
2347
  <DropdownMenuSubTrigger>
2348
2348
  {t("recordingPage.enhanceRecording")}
2349
2349
  </DropdownMenuSubTrigger>
2350
- <DropdownMenuSubContent className="w-56">
2350
+ <DropdownMenuSubContent className="w-56 max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-x-hidden overflow-y-auto">
2351
2351
  <DropdownMenuItem
2352
2352
  disabled={requestTranscript.isPending}
2353
2353
  onSelect={() =>
@@ -2380,7 +2380,7 @@ export default function RecordingPage() {
2380
2380
  <DropdownMenuSubTrigger>
2381
2381
  {t("recordingPage.createFromClip")}
2382
2382
  </DropdownMenuSubTrigger>
2383
- <DropdownMenuSubContent className="w-64">
2383
+ <DropdownMenuSubContent className="w-64 max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-x-hidden overflow-y-auto">
2384
2384
  {WORKFLOW_MENU_ITEMS.map((item) => {
2385
2385
  const menuItem = (
2386
2386
  <DropdownMenuItem
@@ -2112,6 +2112,7 @@ export default function RecordRoute() {
2112
2112
  app_name: "clips",
2113
2113
  template_name: "clips",
2114
2114
  output_id: pendingRef.current?.id,
2115
+ recording_attempt_id: pendingRef.current?.id,
2115
2116
  capture_type:
2116
2117
  recordingMode === "camera"
2117
2118
  ? "camera"
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-23
4
+ ---
5
+
6
+ Long recording menus scroll within the visible space so every action stays reachable.
@@ -180,6 +180,8 @@ function expectedDataChunksForFinalPost(
180
180
 
181
181
  function trackUploadBlockingFailure(
182
182
  ownerEmail: string,
183
+ recordingId: string,
184
+ attemptId: string | null,
183
185
  properties: Record<string, unknown>,
184
186
  ): void {
185
187
  try {
@@ -189,6 +191,14 @@ function trackUploadBlockingFailure(
189
191
  app: "clips",
190
192
  template: "clips",
191
193
  surface: "server_upload",
194
+ output_id: recordingId,
195
+ output_type: "clip",
196
+ recording_id: recordingId,
197
+ // Keep this stable for the recording lifecycle; upload_attempt_id
198
+ // disambiguates resumable retries without breaking the join to start.
199
+ recording_attempt_id: recordingId,
200
+ ...(attemptId ? { upload_attempt_id: attemptId } : {}),
201
+ failure_code: properties.failure_code ?? properties.failure_type,
192
202
  ...properties,
193
203
  },
194
204
  { userId: ownerEmail },
@@ -693,7 +703,7 @@ export async function handleRecordingChunk(
693
703
  });
694
704
  if ((result as any)?.status === "failed") {
695
705
  const failure = finalizeResultFailure(result);
696
- trackUploadBlockingFailure(ownerEmail, {
706
+ trackUploadBlockingFailure(ownerEmail, recordingId, attemptId, {
697
707
  stage: "finalize_recording",
698
708
  outcome: failure.outcome,
699
709
  failure_type: failure.failure_type,
@@ -812,7 +822,7 @@ export async function handleRecordingChunk(
812
822
  return acceptedProcessingResponse(event, recordingId, pendingState);
813
823
  }
814
824
  }
815
- trackUploadBlockingFailure(ownerEmail, {
825
+ trackUploadBlockingFailure(ownerEmail, recordingId, attemptId, {
816
826
  stage: "finalize_recording",
817
827
  outcome: "failed",
818
828
  failure_type: classifyTrackingFailure(err),
@@ -1273,7 +1283,7 @@ async function handleResumableChunk(
1273
1283
  );
1274
1284
  if ((result as any)?.status === "failed") {
1275
1285
  const failure = finalizeResultFailure(result);
1276
- trackUploadBlockingFailure(ownerEmail, {
1286
+ trackUploadBlockingFailure(ownerEmail, recordingId, attemptId, {
1277
1287
  stage: "finalize_recording",
1278
1288
  outcome: failure.outcome,
1279
1289
  failure_type: failure.failure_type,
@@ -1380,7 +1390,7 @@ async function handleResumableChunk(
1380
1390
  }
1381
1391
  }
1382
1392
 
1383
- trackUploadBlockingFailure(ownerEmail, {
1393
+ trackUploadBlockingFailure(ownerEmail, recordingId, attemptId, {
1384
1394
  stage: "finalize_recording",
1385
1395
  outcome: "failed",
1386
1396
  failure_type: classifyTrackingFailure(err),
@@ -54,7 +54,15 @@ export const contentBlockRegistry = new BlockRegistry();
54
54
  // every other block keeps its canonical core metadata, so adding a 14th library
55
55
  // block in core lands in content automatically.
56
56
  registerLibraryBlocks(contentBlockRegistry, {
57
- overrides: { table: { type: "table-block" } },
57
+ overrides: {
58
+ table: { type: "table-block" },
59
+ code: { empty: () => ({ code: "" }) },
60
+ "code-tabs": {
61
+ empty: () => ({
62
+ tabs: [{ id: "tab-1", label: "file.ts", code: "" }],
63
+ }),
64
+ },
65
+ },
58
66
  });
59
67
  for (const block of builderDocsBlocks) {
60
68
  contentBlockRegistry.register(block);
@@ -1,6 +1,13 @@
1
1
  import { useSendToAgentChat } from "@agent-native/core/client/agent-chat";
2
2
  import { useT } from "@agent-native/core/client/i18n";
3
+ import { useLabs } from "@agent-native/core/client/labs";
3
4
  import type { CreateInlineDatabaseResponse } from "@shared/api";
5
+ import {
6
+ CONTENT_SLASH_ADVANCED_CODE,
7
+ CONTENT_SLASH_DEVELOPER_DOCS,
8
+ CONTENT_SLASH_LAYOUTS,
9
+ CONTENT_SLASH_VISUALS,
10
+ } from "@shared/labs";
4
11
  import { renderMathToHtml } from "@shared/math-rendering";
5
12
  import { collapseExactRepeatedNfm, docToNfm } from "@shared/nfm";
6
13
  import { serializeRegistryBlockToMdx } from "@shared/nfm-registry";
@@ -78,7 +85,7 @@ interface SlashCommandMenuProps {
78
85
  * registry-derived block slash items are filtered to specs that round-trip to
79
86
  * Notion-Flavored Markdown (`spec.notionCompatible`), so authors can't add a
80
87
  * structured block that would silently drop on the next Notion push. When
81
- * unset (the common case), all registry blocks are offered.
88
+ * unset (the common case), Content's Labs and authoring policy applies.
82
89
  */
83
90
  notionPageId?: string | null;
84
91
  }
@@ -665,6 +672,7 @@ export function SlashCommandMenu({
665
672
  onDraftPersisted,
666
673
  }: SlashCommandMenuProps) {
667
674
  const t = useT();
675
+ const labs = useLabs();
668
676
  const { send, isGenerating } = useSendToAgentChat();
669
677
  const navigate = useNavigate();
670
678
  const createPage = useCreatePage({ navigate: false, awaitPersist: true });
@@ -990,8 +998,14 @@ export function SlashCommandMenu({
990
998
  ? []
991
999
  : (buildRegistrySlashItems(contentBlockRegistry, {
992
1000
  notionCompatibleOnly: !!notionPageId,
1001
+ policy: {
1002
+ advancedCode: labs[CONTENT_SLASH_ADVANCED_CODE.key] === true,
1003
+ layouts: labs[CONTENT_SLASH_LAYOUTS.key] === true,
1004
+ visuals: labs[CONTENT_SLASH_VISUALS.key] === true,
1005
+ developerDocs: labs[CONTENT_SLASH_DEVELOPER_DOCS.key] === true,
1006
+ },
993
1007
  }) as unknown as CommandItem[]),
994
- [isTurnInto, notionPageId],
1008
+ [isTurnInto, labs, notionPageId],
995
1009
  );
996
1010
  const localComponentCommands = useMemo<CommandItem[]>(
997
1011
  () =>
@@ -12,11 +12,10 @@ import { createContentBlockId } from "./extensions/registryBlocks";
12
12
  /**
13
13
  * Registry-derived slash command items for content's `SlashCommandMenu`.
14
14
  *
15
- * Every `BlockSpec` whose `placement` includes `"block"` (the shared dev-doc /
16
- * OpenAPI / structured library plan and content both register) becomes one slash
17
- * item. Choosing it inserts a `registryBlock` atom node — the SAME core node plan
18
- * inserts a `planBlock` for — seeded with a fresh `blockId` and the spec's
19
- * `empty()` data.
15
+ * Content applies an explicit authoring policy to the shared registry. Blocks
16
+ * remain registered for rendering and round-tripping even when they are absent
17
+ * from the slash menu. Choosing an allowed item inserts a `registryBlock` atom
18
+ * node seeded with a fresh `blockId` and the spec's `empty()` data.
20
19
  *
21
20
  * Content has NO sidecar block store: a registry block's authority is the inline
22
21
  * MDX preserved on the node as `__raw` (see `VisualEditor`'s `useRegistryBlockStore`).
@@ -31,7 +30,7 @@ import { createContentBlockId } from "./extensions/registryBlocks";
31
30
  * to a Notion page), only specs that round-trip to Notion-Flavored Markdown
32
31
  * (`spec.notionCompatible`, the single registry-level allowlist from T3) are
33
32
  * offered, so authors can't add blocks that would silently drop on push. When it
34
- * is unset, all registry blocks are offered.
33
+ * is unset, Content's authorable registry blocks are offered.
35
34
  */
36
35
 
37
36
  /** The shape content's `SlashCommandMenu` consumes (mirrors its `CommandItem`). */
@@ -43,6 +42,53 @@ export interface RegistrySlashItem {
43
42
  action: (editor: RegistrySlashEditor) => void;
44
43
  }
45
44
 
45
+ export interface ContentRegistrySlashPolicy {
46
+ advancedCode?: boolean;
47
+ layouts?: boolean;
48
+ visuals?: boolean;
49
+ developerDocs?: boolean;
50
+ }
51
+
52
+ const ALWAYS_HIDDEN_BLOCK_TYPES = new Set([
53
+ "checklist",
54
+ "table-block",
55
+ "columns",
56
+ "question-form",
57
+ "visual-questions",
58
+ "inline-database",
59
+ "callout",
60
+ "source-component",
61
+ "builder-text",
62
+ "builder-code-block",
63
+ "builder-code-snippets-v2",
64
+ "builder-tabbed-content",
65
+ "builder-symbol",
66
+ "builder-raw-block",
67
+ ]);
68
+ const ADVANCED_CODE_BLOCK_TYPES = new Set(["code", "code-tabs"]);
69
+ const LAYOUT_BLOCK_TYPES = new Set(["custom-html", "tabs"]);
70
+ const VISUAL_BLOCK_TYPES = new Set(["diagram", "mermaid", "wireframe"]);
71
+ const DEVELOPER_DOC_BLOCK_TYPES = new Set([
72
+ "api-endpoint",
73
+ "openapi-spec",
74
+ "data-model",
75
+ "diff",
76
+ "file-tree",
77
+ "json-explorer",
78
+ "annotated-code",
79
+ ]);
80
+ export function contentRegistryBlockIsAuthorable(
81
+ blockType: string,
82
+ policy: ContentRegistrySlashPolicy = {},
83
+ ) {
84
+ if (ALWAYS_HIDDEN_BLOCK_TYPES.has(blockType)) return false;
85
+ if (ADVANCED_CODE_BLOCK_TYPES.has(blockType)) return !!policy.advancedCode;
86
+ if (LAYOUT_BLOCK_TYPES.has(blockType)) return !!policy.layouts;
87
+ if (VISUAL_BLOCK_TYPES.has(blockType)) return !!policy.visuals;
88
+ if (DEVELOPER_DOC_BLOCK_TYPES.has(blockType)) return !!policy.developerDocs;
89
+ return false;
90
+ }
91
+
46
92
  /**
47
93
  * The minimal Tiptap editor surface a registry slash item drives: a focus +
48
94
  * `insertContent` chain. Kept structural so this module needs no direct
@@ -76,51 +122,26 @@ export function seedRegistryBlockRaw(spec: BlockSpec, blockId: string): string {
76
122
  }
77
123
 
78
124
  /**
79
- * Build the registry-derived slash items from a content `BlockRegistry`. APPEND
80
- * the result to content's hand-authored slash list (Notion toggle/callout/table/
81
- * headings stay first); these add the structured-block library beneath them.
125
+ * Build Content's policy-filtered registry slash items. The caller supplies
126
+ * evaluated Labs settings; unknown types fail closed.
82
127
  */
83
128
  export function buildRegistrySlashItems(
84
129
  registry: BlockRegistry,
85
- options: { notionCompatibleOnly?: boolean } = {},
130
+ options: {
131
+ notionCompatibleOnly?: boolean;
132
+ policy?: ContentRegistrySlashPolicy;
133
+ } = {},
86
134
  ): RegistrySlashItem[] {
87
- // Registry block commands come from the shared core builder so adding a library
88
- // block only touches the registry. Content's per-app parts: a React-component
89
- // `icon`, compact visible descriptions, hidden search text for type/alias
90
- // matching, the default `spec.notionCompatible` gating predicate, and inserting
91
- // a `registryBlock` node seeded with inline `__raw`.
135
+ // The shared builder owns registry enumeration and Notion compatibility.
136
+ // Content owns which registered formats are offered for new insertion.
92
137
  return buildRegistryBlockSlashItems<
93
138
  RegistrySlashItem,
94
139
  RegistrySlashEditor,
95
140
  BlockSpec
96
141
  >(registry, {
97
142
  notionCompatibleOnly: options.notionCompatibleOnly,
98
- // A few shared library blocks are registered (so saved docs and source
99
- // round-trip), but not offered in Content's authoring slash menu:
100
- // - `columns` needs recursive nested block editing before it is usable in
101
- // Content's inline NFM editor.
102
- // - `question-form` / `visual-questions` are agent-intake forms (they
103
- // submit answers back to a planning agent), which is a plan workflow,
104
- // not a content-authoring block.
105
- // - `inline-database` is registered in Phase 2 for render/round-trip
106
- // compatibility; Phase 3 owns the `/database` insertion flow.
107
- // - `callout` duplicates Content's native Notion-style Callout command.
108
- // Keep one unambiguous authoring choice; the registry remains available
109
- // for rendering and round-tripping saved `<Callout>` blocks.
110
- // - `source-component` is emitted by provider hydration to preserve
111
- // source-native blocks; users should not author raw preservation
112
- // markers by hand.
113
- // The genuinely document-friendly rich blocks (decision, diagram,
114
- // wireframe, and the dev-doc/structured set) ARE offered.
115
143
  includeSpec: (spec) =>
116
- ![
117
- "columns",
118
- "question-form",
119
- "visual-questions",
120
- "inline-database",
121
- "callout",
122
- "source-component",
123
- ].includes(spec.type),
144
+ contentRegistryBlockIsAuthorable(spec.type, options.policy),
124
145
  toItem: (spec, insert) => ({
125
146
  title: spec.label,
126
147
  description: getRegistryBlockSlashDescription(spec),
@@ -1085,6 +1085,18 @@ const enUS = {
1085
1085
  labCreativeContext: "Creative Context",
1086
1086
  labCreativeContextDescription:
1087
1087
  "Connect and reuse governed reference context in Content.",
1088
+ labSlashAdvancedCode: "Advanced code blocks",
1089
+ labSlashAdvancedCodeDescription:
1090
+ "Add structured code and code-tabs blocks to the slash menu.",
1091
+ labSlashLayouts: "Layout blocks",
1092
+ labSlashLayoutsDescription:
1093
+ "Add custom HTML and tabs blocks to the slash menu.",
1094
+ labSlashVisuals: "Visual blocks",
1095
+ labSlashVisualsDescription:
1096
+ "Add diagram, Mermaid, and wireframe blocks to the slash menu.",
1097
+ labSlashDeveloperDocs: "Developer documentation blocks",
1098
+ labSlashDeveloperDocsDescription:
1099
+ "Add API and developer-documentation blocks to the slash menu.",
1088
1100
  },
1089
1101
  chat: {
1090
1102
  publicEmptyState: "Ask me anything about this document",
@@ -14,7 +14,14 @@ import {
14
14
  createCreativeContextAgentTab,
15
15
  } from "@agent-native/creative-context/client";
16
16
  import { useSetPageTitle } from "@agent-native/toolkit/app-shell";
17
- import { CONTENT_LABS } from "@shared/labs";
17
+ import {
18
+ CONTENT_CREATIVE_CONTEXT,
19
+ CONTENT_LABS,
20
+ CONTENT_SLASH_ADVANCED_CODE,
21
+ CONTENT_SLASH_DEVELOPER_DOCS,
22
+ CONTENT_SLASH_LAYOUTS,
23
+ CONTENT_SLASH_VISUALS,
24
+ } from "@shared/labs";
18
25
  import { useMemo } from "react";
19
26
  import { toast } from "sonner";
20
27
 
@@ -46,14 +53,34 @@ export default function SettingsRoute() {
46
53
  () =>
47
54
  CONTENT_LABS.map((lab) => ({
48
55
  ...lab,
49
- displayName: t("settings.labCreativeContext", {
50
- defaultValue: lab.displayName ?? "Creative Context",
51
- }),
52
- description: t("settings.labCreativeContextDescription", {
53
- defaultValue:
54
- lab.description ??
55
- "Connect and reuse governed reference context in Content.",
56
- }),
56
+ ...(lab.key === CONTENT_CREATIVE_CONTEXT.key
57
+ ? {
58
+ displayName: t("settings.labCreativeContext"),
59
+ description: t("settings.labCreativeContextDescription"),
60
+ }
61
+ : lab.key === CONTENT_SLASH_ADVANCED_CODE.key
62
+ ? {
63
+ displayName: t("settings.labSlashAdvancedCode"),
64
+ description: t("settings.labSlashAdvancedCodeDescription"),
65
+ }
66
+ : lab.key === CONTENT_SLASH_LAYOUTS.key
67
+ ? {
68
+ displayName: t("settings.labSlashLayouts"),
69
+ description: t("settings.labSlashLayoutsDescription"),
70
+ }
71
+ : lab.key === CONTENT_SLASH_VISUALS.key
72
+ ? {
73
+ displayName: t("settings.labSlashVisuals"),
74
+ description: t("settings.labSlashVisualsDescription"),
75
+ }
76
+ : lab.key === CONTENT_SLASH_DEVELOPER_DOCS.key
77
+ ? {
78
+ displayName: t("settings.labSlashDeveloperDocs"),
79
+ description: t(
80
+ "settings.labSlashDeveloperDocsDescription",
81
+ ),
82
+ }
83
+ : {}),
57
84
  })),
58
85
  [t],
59
86
  );
@@ -67,7 +67,7 @@ Given a page-owned Artifact has no catalog record, when its owner chooses Save a
67
67
 
68
68
  ## Current evidence
69
69
 
70
- `app/components/editor/extensions/LocalMdxComponentNode.tsx`, `actions/list-local-component-files.ts`, `actions/write-local-component-file.ts`, and `app/blocks/contentBlockRegistry.tsx` are donors, not a hosted catalog.
70
+ `app/components/editor/extensions/LocalMdxComponentNode.tsx`, `actions/list-local-component-files.ts`, `actions/write-local-component-file.ts`, and `app/blocks/contentBlockRegistry.tsx` are donors, not a hosted catalog. `app/components/editor/registrySlashItems.ts` separately curates which built-in renderers appear in slash discovery; registry membership alone does not make a renderer an authoring command. Builder source formats remain registered for existing content but are hidden from insertion until new blocks can receive valid source sidecars.
71
71
 
72
72
  ## Proof plan
73
73
 
@@ -27,6 +27,7 @@ export default createAgentChatPlugin({
27
27
  resolveActionSurface: resolveCommentAiActionSurface,
28
28
  durableBackgroundRuns: true,
29
29
  selectedA2AReceiverOwnsObjective: true,
30
+ frameworkTools: { labs: true },
30
31
  actions: loadActionsFromStaticRegistry(actionsRegistry),
31
32
  initialToolNames: INJECTED_INITIAL_TOOL_NAMES,
32
33
  mcp: {
@@ -7,4 +7,38 @@ export const CONTENT_CREATIVE_CONTEXT = defineLab({
7
7
  keywords: "context creative library reference packs sources",
8
8
  });
9
9
 
10
- export const CONTENT_LABS = defineLabs([CONTENT_CREATIVE_CONTEXT]);
10
+ export const CONTENT_SLASH_ADVANCED_CODE = defineLab({
11
+ key: "content.slash.advanced-code",
12
+ displayName: "Advanced code blocks",
13
+ description: "Add structured code and code-tabs blocks to the slash menu.",
14
+ keywords: "slash commands code tabs",
15
+ });
16
+
17
+ export const CONTENT_SLASH_LAYOUTS = defineLab({
18
+ key: "content.slash.layouts",
19
+ displayName: "Layout blocks",
20
+ description: "Add custom HTML and tabs blocks to the slash menu.",
21
+ keywords: "slash commands html tabs layout",
22
+ });
23
+
24
+ export const CONTENT_SLASH_VISUALS = defineLab({
25
+ key: "content.slash.visuals",
26
+ displayName: "Visual blocks",
27
+ description: "Add diagram, Mermaid, and wireframe blocks to the slash menu.",
28
+ keywords: "slash commands diagram mermaid wireframe",
29
+ });
30
+
31
+ export const CONTENT_SLASH_DEVELOPER_DOCS = defineLab({
32
+ key: "content.slash.developer-docs",
33
+ displayName: "Developer documentation blocks",
34
+ description: "Add API and developer-documentation blocks to the slash menu.",
35
+ keywords: "slash commands api openapi data model diff file tree json",
36
+ });
37
+
38
+ export const CONTENT_LABS = defineLabs([
39
+ CONTENT_CREATIVE_CONTEXT,
40
+ CONTENT_SLASH_ADVANCED_CODE,
41
+ CONTENT_SLASH_LAYOUTS,
42
+ CONTENT_SLASH_VISUALS,
43
+ CONTENT_SLASH_DEVELOPER_DOCS,
44
+ ]);
@@ -3,7 +3,7 @@ import {
3
3
  hydrateBuilderDesignSystemReference,
4
4
  parseBuilderDesignSystemProxyReference,
5
5
  } from "@agent-native/core/server";
6
- import { resolveAccess } from "@agent-native/core/sharing";
6
+ import { accessFilter, resolveAccess } from "@agent-native/core/sharing";
7
7
  import { and, eq } from "drizzle-orm";
8
8
  import { z } from "zod";
9
9
 
@@ -51,7 +51,7 @@ function truncate(value: string, maxChars: number): string {
51
51
  // next call to persist it. Never bumps updatedAt, so a background count
52
52
  // refresh doesn't reorder the list.
53
53
  async function persistBuilderDocCount(
54
- row: { id: string; data: string | null },
54
+ row: { id: string; ownerEmail: string; data: string | null },
55
55
  docCount: number,
56
56
  ): Promise<void> {
57
57
  if (!row.data) return;
@@ -68,7 +68,9 @@ async function persistBuilderDocCount(
68
68
  .set({ data: JSON.stringify({ ...parsed, docCount }) })
69
69
  .where(
70
70
  and(
71
+ accessFilter(schema.designSystems, schema.designSystemShares),
71
72
  eq(schema.designSystems.id, row.id),
73
+ eq(schema.designSystems.ownerEmail, row.ownerEmail),
72
74
  eq(schema.designSystems.data, row.data),
73
75
  ),
74
76
  );