@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/calendar/actions/delete-event.ts +6 -4
- package/corpus/templates/calendar/actions/delete-events.ts +10 -11
- package/corpus/templates/calendar/actions/event-action-helpers.ts +53 -0
- package/corpus/templates/calendar/actions/get-event.ts +59 -14
- package/corpus/templates/calendar/actions/rsvp-event.ts +5 -3
- package/corpus/templates/calendar/actions/update-event.ts +15 -7
- package/corpus/templates/calendar/actions/update-events.ts +29 -11
- package/corpus/templates/calendar/changelog/2026-09-21-events-stay-editable-when-their-calendar-is-connected-throug.md +6 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +70 -18
- package/corpus/templates/calendar/shared/google-calendar-sources.ts +47 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +35 -1
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +5 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +18 -1
- package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -2
- package/corpus/templates/clips/app/routes/record.tsx +1 -0
- package/corpus/templates/clips/changelog/2026-09-23-long-recording-menus-scroll-within-the-visible-space-so-ever.md +6 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -4
- package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +9 -1
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +16 -2
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +61 -40
- package/corpus/templates/content/app/i18n-data.ts +12 -0
- package/corpus/templates/content/app/routes/_app.settings.tsx +36 -9
- package/corpus/templates/content/docs/product/capabilities/content.renderer.custom-block.md +1 -1
- package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/content/shared/labs.ts +35 -1
- package/corpus/templates/slides/actions/get-design-system.ts +4 -2
- package/dist/agent/engine/openai-compatible-endpoint.d.ts +7 -0
- package/dist/agent/engine/openai-compatible-endpoint.js +9 -0
- package/dist/agent/engine/provider-endpoint-validation.d.ts +8 -1
- package/dist/agent/engine/provider-endpoint-validation.js +47 -10
- package/dist/agent/engine/registry.js +8 -5
- package/dist/automation/index.d.ts +1 -1
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.js +45 -9
- package/dist/client/agent-engine-key.d.ts +7 -0
- package/dist/client/agent-engine-key.js +37 -0
- package/dist/client/agent-provider-catalog.js +1 -1
- package/dist/client/settings/ProviderSetupForm.js +80 -7
- package/dist/client/settings/SettingsPanel.js +149 -11
- package/dist/client/use-chat-models.js +31 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/labs/actions/set-lab.js +1 -0
- package/dist/labs/registry.js +6 -1
- package/dist/localization/default-messages.d.ts +7 -1
- package/dist/localization/default-messages.js +7 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/index.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +3 -3
- package/dist/scripts/agent-engines/test-agent-engine.js +9 -7
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.js +8 -5
- package/dist/server/agent-engine-ollama-models-route.d.ts +15 -0
- package/dist/server/agent-engine-ollama-models-route.js +105 -0
- package/dist/server/core-routes-plugin.js +5 -0
- package/dist/server/credential-provider.d.ts +13 -0
- package/dist/server/credential-provider.js +19 -0
- 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({
|
|
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={
|
|
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
|
-
|
|
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"
|
|
@@ -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: {
|
|
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),
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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,
|
|
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
|
|
80
|
-
*
|
|
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: {
|
|
130
|
+
options: {
|
|
131
|
+
notionCompatibleOnly?: boolean;
|
|
132
|
+
policy?: ContentRegistrySlashPolicy;
|
|
133
|
+
} = {},
|
|
86
134
|
): RegistrySlashItem[] {
|
|
87
|
-
//
|
|
88
|
-
//
|
|
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 {
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
);
|