@agent-native/core 0.148.1 → 0.149.1
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/analytics/changelog/2026-08-10-wide-tables-in-analytics-chat-now-scroll-horizontally-within.md +6 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +13 -53
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -70
- package/corpus/templates/calendar/app/lib/event-layout.ts +122 -0
- package/corpus/templates/calendar/app/routes/_app.availability.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-08-10-opening-availability-settings-from-the-agent-lands-on-the-av.md +6 -0
- package/corpus/templates/calendar/vitest.config.ts +6 -1
- package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +3 -1
- package/corpus/templates/clips/app/components/player/video-player.tsx +44 -4
- package/corpus/templates/clips/changelog/2026-08-10-fixed-transcription-failures-for-recordings-without-captured.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-10-mobile-clips-can-now-enter-fullscreen-on-iphone-safari.md +6 -0
- package/corpus/templates/content/actions/_property-utils.ts +11 -1
- package/corpus/templates/content/actions/view-screen.ts +62 -12
- package/corpus/templates/content/server/plugins/agent-chat.ts +2 -0
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +22 -0
- package/corpus/templates/design/AGENTS.md +3 -0
- package/corpus/templates/design/actions/create-design-from-template.ts +19 -0
- package/corpus/templates/design/actions/get-design-snapshot.ts +18 -0
- package/corpus/templates/design/actions/get-design-template.ts +178 -0
- package/corpus/templates/design/actions/list-designs.ts +3 -1
- package/corpus/templates/design/actions/view-screen.ts +31 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +225 -214
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
- package/corpus/templates/design/app/i18n-data.ts +78 -0
- package/corpus/templates/design/app/lib/design-authors.ts +64 -0
- package/corpus/templates/design/app/pages/Index.tsx +114 -4
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +1 -0
- package/corpus/templates/design/changelog/2026-08-07-alt-dragging-an-element-to-copy-it-now-keeps-the-copy-visibl.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-design-cards-now-show-who-created-each-design-with-a-filter-.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-drawing-a-shape-on-the-overview-canvas-now-keeps-the-shape-s.md +6 -0
- package/corpus/templates/design/changelog/2026-08-07-hold-option-alt-while-dragging-a-frame-on-the-overview-canva.md +6 -0
- package/corpus/templates/design/server/lib/design-template-data.ts +143 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/forms/AGENTS.md +2 -2
- package/corpus/templates/forms/actions/create-form.ts +1 -1
- package/corpus/templates/forms/actions/update-form.ts +5 -5
- package/corpus/templates/forms/changelog/2026-08-10-forms-now-keep-agent-applied-field-changes-visible-after-an-.md +6 -0
- package/corpus/templates/forms/changelog/2026-08-10-published-form-links-always-use-the-working-public-form-url.md +6 -0
- package/corpus/templates/forms/server/plugins/agent-chat.ts +2 -2
- package/corpus/templates/slides/AGENTS.md +4 -3
- package/corpus/templates/slides/actions/import-google-slides-reference.ts +56 -10
- package/corpus/templates/slides/actions/patch-deck.ts +18 -2
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +46 -0
- package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +194 -3
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +22 -75
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +5 -3
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +5 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +15 -4
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +116 -55
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +27 -4
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +45 -0
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +143 -18
- package/corpus/templates/slides/app/i18n/en-US.ts +6 -0
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +84 -25
- package/corpus/templates/slides/app/lib/generation-state.ts +7 -1
- package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +11 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -14
- package/corpus/templates/slides/app/pages/Index.tsx +124 -135
- package/corpus/templates/slides/changelog/2026-08-08-google-slides-export-now-tells-you-why-a-deck-fell-back-to-a.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-delete-key-now-removes-a-selected-image.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-editing-a-slide-in-an-imported-deck-no-longer-fails-to-save.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-fixed-pdf-and-custom-size-powerpoint-imports-rendering-with-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-images-from-sites-without-cors-no-longer-export-blank.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-imported-pdf-headings-no-longer-lose-the-space-between-color.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-imported-slide-text-no-longer-renders-too-large-for-its-box.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-large-decks-no-longer-render-slowly-or-glitch-in-the-editor.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-revise-highlighted-slide-text-with-ai.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-09-the-deck-editor-no-longer-stays-dimmed-after-the-window-narro.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-imported-reference-decks-now-show-clear-success-and-stay-sel.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-10-pasted-google-slides-links-import-after-reconnecting-google-.md +6 -0
- package/corpus/templates/slides/server/handlers/google-docs-auth.ts +4 -0
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +115 -22
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +11 -2
- package/corpus/templates/slides/server/lib/fetch-remote-image.ts +192 -0
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +13 -0
- package/corpus/templates/slides/server/lib/image-proxy-url.ts +192 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/server/routes/api/image-proxy.get.ts +65 -0
- package/dist/agent/engine/registry.d.ts +8 -2
- package/dist/agent/engine/registry.js +63 -29
- package/dist/agent/production-agent.js +7 -0
- package/dist/agent/run-manager.d.ts +5 -4
- package/dist/agent/run-manager.js +6 -5
- package/dist/cli/claude-code-participant.d.ts +2 -1
- package/dist/cli/claude-code-participant.js +19 -0
- package/dist/cli/code-agent-executor.js +223 -0
- package/dist/cli/create-workspace.js +3 -3
- package/dist/cli/create.d.ts +3 -2
- package/dist/cli/create.js +20 -24
- package/dist/client/AgentPanel.js +8 -4
- package/dist/client/AssistantChat.js +34 -1
- package/dist/client/agent-chat-adapter.d.ts +1 -0
- package/dist/client/agent-chat-adapter.js +10 -2
- package/dist/client/chat/markdown-renderer.d.ts +1 -0
- package/dist/client/chat/markdown-renderer.js +4 -0
- package/dist/client/chat/message-components.d.ts +1 -0
- package/dist/client/chat/message-components.js +9 -1
- package/dist/client/chat/tool-call-display.d.ts +8 -0
- package/dist/client/chat/tool-call-display.js +30 -21
- package/dist/client/chat-first/app-pane.js +1 -1
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +11 -11
- package/dist/client/chat-first/chat-history.d.ts +1 -1
- package/dist/client/chat-first/chat-history.js +2 -0
- package/dist/client/chat-first/primary-nav.js +1 -1
- package/dist/client/progress/RunsTray.js +14 -1
- package/dist/client/resources/McpConnectionSuggestion.js +2 -2
- package/dist/client/settings/SettingsTabsPage.d.ts +3 -1
- package/dist/client/settings/SettingsTabsPage.js +2 -2
- package/dist/client/sse-event-processor.d.ts +8 -0
- package/dist/client/sse-event-processor.js +13 -6
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +5 -7
- package/dist/deploy/build.js +19 -12
- package/dist/localization/default-messages.js +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/scripts/call-agent.js +3 -3
- package/dist/scripts/describe-workspace-apps.js +1 -1
- package/dist/server/agent-capabilities.js +0 -0
- package/dist/server/agent-chat/context-tools.js +1 -1
- package/dist/server/agent-chat/prompt-resources.js +1 -1
- package/dist/server/agent-chat-plugin.js +1 -1
- package/dist/server/auth.js +190 -42
- package/dist/server/better-auth-migrations.js +53 -0
- package/dist/server/http-response-telemetry.js +7 -1
- package/dist/server/onboarding-html.js +119 -33
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/password-policy.d.ts +2 -1
- package/dist/shared/password-policy.js +2 -1
- package/dist/styles/agent-conversation.css +20 -3
- package/dist/styles/agent-native.css +14 -2
- package/dist/templates/default/app/i18n/ar-SA.ts +1 -1
- package/dist/templates/default/app/i18n/de-DE.ts +1 -1
- package/dist/templates/default/app/i18n/en-US.ts +1 -1
- package/dist/templates/default/app/i18n/es-ES.ts +1 -1
- package/dist/templates/default/app/i18n/fr-FR.ts +1 -1
- package/dist/templates/default/app/i18n/hi-IN.ts +1 -1
- package/dist/templates/default/app/i18n/ja-JP.ts +1 -1
- package/dist/templates/default/app/i18n/ko-KR.ts +1 -1
- package/dist/templates/default/app/i18n/pt-BR.ts +1 -1
- package/dist/templates/default/app/i18n/zh-CN.ts +1 -1
- package/dist/templates/default/app/i18n/zh-TW.ts +1 -1
- package/dist/templates/default/pnpm-workspace.yaml +1 -0
- package/dist/templates/workspace-root/pnpm-workspace.yaml +2 -0
- package/docs/content/cloneable-saas.mdx +6 -6
- package/docs/content/deployment.mdx +10 -8
- package/docs/content/environment-variables.mdx +4 -3
- package/package.json +2 -2
- package/src/templates/default/app/i18n/ar-SA.ts +1 -1
- package/src/templates/default/app/i18n/de-DE.ts +1 -1
- package/src/templates/default/app/i18n/en-US.ts +1 -1
- package/src/templates/default/app/i18n/es-ES.ts +1 -1
- package/src/templates/default/app/i18n/fr-FR.ts +1 -1
- package/src/templates/default/app/i18n/hi-IN.ts +1 -1
- package/src/templates/default/app/i18n/ja-JP.ts +1 -1
- package/src/templates/default/app/i18n/ko-KR.ts +1 -1
- package/src/templates/default/app/i18n/pt-BR.ts +1 -1
- package/src/templates/default/app/i18n/zh-CN.ts +1 -1
- package/src/templates/default/app/i18n/zh-TW.ts +1 -1
- package/src/templates/default/pnpm-workspace.yaml +1 -0
- package/src/templates/workspace-root/pnpm-workspace.yaml +2 -0
|
@@ -1353,6 +1353,9 @@ const enUS = {
|
|
|
1353
1353
|
newDesign: "New Design",
|
|
1354
1354
|
newDesignLower: "New design",
|
|
1355
1355
|
createDesignProject: "Create a design project",
|
|
1356
|
+
createdBy: "Created by",
|
|
1357
|
+
allAuthors: "All authors",
|
|
1358
|
+
me: "Me",
|
|
1356
1359
|
openingDesign: "Opening design...",
|
|
1357
1360
|
skipToEditor: "Skip to editor",
|
|
1358
1361
|
failedToCreateDesign: "Failed to create design",
|
|
@@ -15375,6 +15378,72 @@ const designComponentSourceOverrides = {
|
|
|
15375
15378
|
},
|
|
15376
15379
|
} satisfies Record<Exclude<LocaleCode, "en-US" | "zh-TW">, PartialMessages>;
|
|
15377
15380
|
|
|
15381
|
+
const designAuthorFilterOverrides = {
|
|
15382
|
+
"zh-CN": {
|
|
15383
|
+
home: {
|
|
15384
|
+
createdBy: "创建者",
|
|
15385
|
+
allAuthors: "所有作者",
|
|
15386
|
+
me: "我",
|
|
15387
|
+
},
|
|
15388
|
+
},
|
|
15389
|
+
"es-ES": {
|
|
15390
|
+
home: {
|
|
15391
|
+
createdBy: "Creado por",
|
|
15392
|
+
allAuthors: "Todos los autores",
|
|
15393
|
+
me: "Yo",
|
|
15394
|
+
},
|
|
15395
|
+
},
|
|
15396
|
+
"fr-FR": {
|
|
15397
|
+
home: {
|
|
15398
|
+
createdBy: "Créé par",
|
|
15399
|
+
allAuthors: "Tous les auteurs",
|
|
15400
|
+
me: "Moi",
|
|
15401
|
+
},
|
|
15402
|
+
},
|
|
15403
|
+
"de-DE": {
|
|
15404
|
+
home: {
|
|
15405
|
+
createdBy: "Erstellt von",
|
|
15406
|
+
allAuthors: "Alle Autoren",
|
|
15407
|
+
me: "Ich",
|
|
15408
|
+
},
|
|
15409
|
+
},
|
|
15410
|
+
"ja-JP": {
|
|
15411
|
+
home: {
|
|
15412
|
+
createdBy: "作成者",
|
|
15413
|
+
allAuthors: "すべての作成者",
|
|
15414
|
+
me: "自分",
|
|
15415
|
+
},
|
|
15416
|
+
},
|
|
15417
|
+
"ko-KR": {
|
|
15418
|
+
home: {
|
|
15419
|
+
createdBy: "만든 사람",
|
|
15420
|
+
allAuthors: "모든 작성자",
|
|
15421
|
+
me: "나",
|
|
15422
|
+
},
|
|
15423
|
+
},
|
|
15424
|
+
"pt-BR": {
|
|
15425
|
+
home: {
|
|
15426
|
+
createdBy: "Criado por",
|
|
15427
|
+
allAuthors: "Todos os autores",
|
|
15428
|
+
me: "Eu",
|
|
15429
|
+
},
|
|
15430
|
+
},
|
|
15431
|
+
"hi-IN": {
|
|
15432
|
+
home: {
|
|
15433
|
+
createdBy: "द्वारा बनाया गया",
|
|
15434
|
+
allAuthors: "सभी लेखक",
|
|
15435
|
+
me: "मैं",
|
|
15436
|
+
},
|
|
15437
|
+
},
|
|
15438
|
+
"ar-SA": {
|
|
15439
|
+
home: {
|
|
15440
|
+
createdBy: "أنشأه",
|
|
15441
|
+
allAuthors: "جميع المؤلفين",
|
|
15442
|
+
me: "أنا",
|
|
15443
|
+
},
|
|
15444
|
+
},
|
|
15445
|
+
} satisfies Record<Exclude<LocaleCode, "en-US" | "zh-TW">, PartialMessages>;
|
|
15446
|
+
|
|
15378
15447
|
export const messagesByLocale = {
|
|
15379
15448
|
"en-US": enUS,
|
|
15380
15449
|
"zh-TW": mergeLocalizedMessages(
|
|
@@ -15429,6 +15498,7 @@ export const messagesByLocale = {
|
|
|
15429
15498
|
designRuntimeIdentityAndBridgeOverrides["zh-CN"],
|
|
15430
15499
|
designComponentInstanceOverrides["zh-CN"],
|
|
15431
15500
|
designComponentSourceOverrides["zh-CN"],
|
|
15501
|
+
designAuthorFilterOverrides["zh-CN"],
|
|
15432
15502
|
{
|
|
15433
15503
|
root: {
|
|
15434
15504
|
commandActions: "操作",
|
|
@@ -15508,6 +15578,7 @@ export const messagesByLocale = {
|
|
|
15508
15578
|
designRuntimeIdentityAndBridgeOverrides["es-ES"],
|
|
15509
15579
|
designComponentInstanceOverrides["es-ES"],
|
|
15510
15580
|
designComponentSourceOverrides["es-ES"],
|
|
15581
|
+
designAuthorFilterOverrides["es-ES"],
|
|
15511
15582
|
{
|
|
15512
15583
|
root: {
|
|
15513
15584
|
commandActions: "Acciones",
|
|
@@ -15589,6 +15660,7 @@ export const messagesByLocale = {
|
|
|
15589
15660
|
designRuntimeIdentityAndBridgeOverrides["fr-FR"],
|
|
15590
15661
|
designComponentInstanceOverrides["fr-FR"],
|
|
15591
15662
|
designComponentSourceOverrides["fr-FR"],
|
|
15663
|
+
designAuthorFilterOverrides["fr-FR"],
|
|
15592
15664
|
{
|
|
15593
15665
|
root: {
|
|
15594
15666
|
commandActions: "Actions",
|
|
@@ -15670,6 +15742,7 @@ export const messagesByLocale = {
|
|
|
15670
15742
|
designRuntimeIdentityAndBridgeOverrides["de-DE"],
|
|
15671
15743
|
designComponentInstanceOverrides["de-DE"],
|
|
15672
15744
|
designComponentSourceOverrides["de-DE"],
|
|
15745
|
+
designAuthorFilterOverrides["de-DE"],
|
|
15673
15746
|
{
|
|
15674
15747
|
root: {
|
|
15675
15748
|
commandActions: "Aktionen",
|
|
@@ -15751,6 +15824,7 @@ export const messagesByLocale = {
|
|
|
15751
15824
|
designRuntimeIdentityAndBridgeOverrides["ja-JP"],
|
|
15752
15825
|
designComponentInstanceOverrides["ja-JP"],
|
|
15753
15826
|
designComponentSourceOverrides["ja-JP"],
|
|
15827
|
+
designAuthorFilterOverrides["ja-JP"],
|
|
15754
15828
|
{
|
|
15755
15829
|
root: {
|
|
15756
15830
|
commandActions: "操作",
|
|
@@ -15833,6 +15907,7 @@ export const messagesByLocale = {
|
|
|
15833
15907
|
designRuntimeIdentityAndBridgeOverrides["ko-KR"],
|
|
15834
15908
|
designComponentInstanceOverrides["ko-KR"],
|
|
15835
15909
|
designComponentSourceOverrides["ko-KR"],
|
|
15910
|
+
designAuthorFilterOverrides["ko-KR"],
|
|
15836
15911
|
{
|
|
15837
15912
|
root: {
|
|
15838
15913
|
commandActions: "작업",
|
|
@@ -15913,6 +15988,7 @@ export const messagesByLocale = {
|
|
|
15913
15988
|
designRuntimeIdentityAndBridgeOverrides["pt-BR"],
|
|
15914
15989
|
designComponentInstanceOverrides["pt-BR"],
|
|
15915
15990
|
designComponentSourceOverrides["pt-BR"],
|
|
15991
|
+
designAuthorFilterOverrides["pt-BR"],
|
|
15916
15992
|
{
|
|
15917
15993
|
root: {
|
|
15918
15994
|
commandActions: "Ações",
|
|
@@ -15994,6 +16070,7 @@ export const messagesByLocale = {
|
|
|
15994
16070
|
designRuntimeIdentityAndBridgeOverrides["hi-IN"],
|
|
15995
16071
|
designComponentInstanceOverrides["hi-IN"],
|
|
15996
16072
|
designComponentSourceOverrides["hi-IN"],
|
|
16073
|
+
designAuthorFilterOverrides["hi-IN"],
|
|
15997
16074
|
{
|
|
15998
16075
|
root: {
|
|
15999
16076
|
commandActions: "क्रियाएं",
|
|
@@ -16075,6 +16152,7 @@ export const messagesByLocale = {
|
|
|
16075
16152
|
designRuntimeIdentityAndBridgeOverrides["ar-SA"],
|
|
16076
16153
|
designComponentInstanceOverrides["ar-SA"],
|
|
16077
16154
|
designComponentSourceOverrides["ar-SA"],
|
|
16155
|
+
designAuthorFilterOverrides["ar-SA"],
|
|
16078
16156
|
{
|
|
16079
16157
|
root: {
|
|
16080
16158
|
commandActions: "الإجراءات",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { emailToName } from "@agent-native/core/client/collab";
|
|
2
|
+
|
|
3
|
+
/** Author-filter values that cannot collide with a real owner email. */
|
|
4
|
+
export const ALL_AUTHORS = "all";
|
|
5
|
+
export const MY_DESIGNS = "me";
|
|
6
|
+
|
|
7
|
+
export interface AuthoredDesign {
|
|
8
|
+
ownerEmail?: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeAuthorEmail(
|
|
12
|
+
email: string | null | undefined,
|
|
13
|
+
): string | null {
|
|
14
|
+
const trimmed = email?.trim();
|
|
15
|
+
return trimmed ? trimmed.toLowerCase() : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Distinct owner emails across the whole library, ordered by display name. */
|
|
19
|
+
export function collectAuthorEmails(designs: AuthoredDesign[]): string[] {
|
|
20
|
+
const byNormalized = new Map<string, string>();
|
|
21
|
+
for (const design of designs) {
|
|
22
|
+
const email = design.ownerEmail?.trim();
|
|
23
|
+
if (!email) continue;
|
|
24
|
+
const normalized = email.toLowerCase();
|
|
25
|
+
if (!byNormalized.has(normalized)) byNormalized.set(normalized, email);
|
|
26
|
+
}
|
|
27
|
+
return Array.from(byNormalized.values()).sort((a, b) =>
|
|
28
|
+
emailToName(a).localeCompare(emailToName(b)),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether author bylines are worth showing on cards. A solo workspace would
|
|
34
|
+
* only ever repeat the viewer's own name, so bylines stay hidden until a
|
|
35
|
+
* second person is involved — either as an org member or as the owner of a
|
|
36
|
+
* design the viewer can see.
|
|
37
|
+
*/
|
|
38
|
+
export function shouldShowAuthors(input: {
|
|
39
|
+
orgMemberCount: number | undefined;
|
|
40
|
+
authorEmails: string[];
|
|
41
|
+
}): boolean {
|
|
42
|
+
return (input.orgMemberCount ?? 0) > 1 || input.authorEmails.length > 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function filterDesignsByAuthor<T extends AuthoredDesign>(
|
|
46
|
+
designs: T[],
|
|
47
|
+
author: string,
|
|
48
|
+
viewerEmail: string | null | undefined,
|
|
49
|
+
): T[] {
|
|
50
|
+
if (author === ALL_AUTHORS) return designs;
|
|
51
|
+
const normalizedViewer = normalizeAuthorEmail(viewerEmail);
|
|
52
|
+
// An unknown viewer cannot own anything, so "mine" is empty rather than
|
|
53
|
+
// silently falling back to every design.
|
|
54
|
+
if (author === MY_DESIGNS) {
|
|
55
|
+
if (!normalizedViewer) return [];
|
|
56
|
+
return designs.filter(
|
|
57
|
+
(design) => normalizeAuthorEmail(design.ownerEmail) === normalizedViewer,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const normalizedAuthor = normalizeAuthorEmail(author);
|
|
61
|
+
return designs.filter(
|
|
62
|
+
(design) => normalizeAuthorEmail(design.ownerEmail) === normalizedAuthor,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { emailToColor, emailToName } from "@agent-native/core/client/collab";
|
|
1
2
|
import { type PromptComposerSubmitOptions } from "@agent-native/core/client/composer";
|
|
2
3
|
import { useFeatureFlag } from "@agent-native/core/client/feature-flags";
|
|
3
4
|
import {
|
|
4
5
|
useActionQuery,
|
|
5
6
|
useActionMutation,
|
|
7
|
+
useAvatarUrl,
|
|
8
|
+
useSession,
|
|
6
9
|
} from "@agent-native/core/client/hooks";
|
|
7
10
|
import {
|
|
8
11
|
injectSessionReplayIframeBootstrap,
|
|
9
12
|
SESSION_REPLAY_IFRAME_ATTRIBUTE,
|
|
10
13
|
} from "@agent-native/core/client/host";
|
|
11
14
|
import { useT } from "@agent-native/core/client/i18n";
|
|
15
|
+
import { useOrgMembers } from "@agent-native/core/client/org";
|
|
12
16
|
import { CreativeContextShareSheet } from "@agent-native/creative-context/client";
|
|
13
17
|
import {
|
|
14
18
|
useSetHeaderActions,
|
|
@@ -49,6 +53,7 @@ import {
|
|
|
49
53
|
AlertDialogHeader,
|
|
50
54
|
AlertDialogTitle,
|
|
51
55
|
} from "@/components/ui/alert-dialog";
|
|
56
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
52
57
|
import { Button } from "@/components/ui/button";
|
|
53
58
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
54
59
|
import {
|
|
@@ -58,6 +63,13 @@ import {
|
|
|
58
63
|
DropdownMenuTrigger,
|
|
59
64
|
} from "@/components/ui/dropdown-menu";
|
|
60
65
|
import { Input } from "@/components/ui/input";
|
|
66
|
+
import {
|
|
67
|
+
Select,
|
|
68
|
+
SelectContent,
|
|
69
|
+
SelectItem,
|
|
70
|
+
SelectTrigger,
|
|
71
|
+
SelectValue,
|
|
72
|
+
} from "@/components/ui/select";
|
|
61
73
|
import { Spinner } from "@/components/ui/spinner";
|
|
62
74
|
import {
|
|
63
75
|
Tooltip,
|
|
@@ -66,6 +78,14 @@ import {
|
|
|
66
78
|
} from "@/components/ui/tooltip";
|
|
67
79
|
import { useDesignSystems } from "@/hooks/use-design-systems";
|
|
68
80
|
import { sendToDesignAgentChat } from "@/lib/agent-chat";
|
|
81
|
+
import {
|
|
82
|
+
ALL_AUTHORS,
|
|
83
|
+
collectAuthorEmails,
|
|
84
|
+
filterDesignsByAuthor,
|
|
85
|
+
MY_DESIGNS,
|
|
86
|
+
normalizeAuthorEmail,
|
|
87
|
+
shouldShowAuthors,
|
|
88
|
+
} from "@/lib/design-authors";
|
|
69
89
|
import {
|
|
70
90
|
clearPendingGeneration,
|
|
71
91
|
writePendingGeneration,
|
|
@@ -80,6 +100,7 @@ interface Design {
|
|
|
80
100
|
description?: string;
|
|
81
101
|
projectType: ProjectType;
|
|
82
102
|
designSystemId?: string | null;
|
|
103
|
+
ownerEmail?: string | null;
|
|
83
104
|
createdAt?: string;
|
|
84
105
|
updatedAt?: string;
|
|
85
106
|
/** Preview HTML for the thumbnail. Only present when the list query asks
|
|
@@ -93,6 +114,7 @@ export default function Index() {
|
|
|
93
114
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
94
115
|
const queryClient = useQueryClient();
|
|
95
116
|
const [search, setSearch] = useState("");
|
|
117
|
+
const [author, setAuthor] = useState<string>(ALL_AUTHORS);
|
|
96
118
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
|
97
119
|
const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false);
|
|
98
120
|
const [selectedDesignIds, setSelectedDesignIds] = useState<Set<string>>(
|
|
@@ -134,6 +156,8 @@ export default function Index() {
|
|
|
134
156
|
"list-design-templates",
|
|
135
157
|
{ includePreview: "true" },
|
|
136
158
|
);
|
|
159
|
+
const { session } = useSession();
|
|
160
|
+
const { data: orgMembersPage } = useOrgMembers();
|
|
137
161
|
|
|
138
162
|
const createMutation = useActionMutation("create-design");
|
|
139
163
|
const createFromTemplateMutation = useActionMutation(
|
|
@@ -174,11 +198,27 @@ export default function Index() {
|
|
|
174
198
|
const selectedTemplate =
|
|
175
199
|
templateOptions.find((template) => template.id === newTemplateId) ?? null;
|
|
176
200
|
|
|
201
|
+
const viewerEmail = session?.email ?? null;
|
|
202
|
+
const authorEmails = useMemo(() => collectAuthorEmails(designs), [designs]);
|
|
203
|
+
const showAuthors = shouldShowAuthors({
|
|
204
|
+
orgMemberCount: orgMembersPage?.totalCount,
|
|
205
|
+
authorEmails,
|
|
206
|
+
});
|
|
207
|
+
const normalizedViewerEmail = normalizeAuthorEmail(viewerEmail);
|
|
208
|
+
const viewerHasDesigns = authorEmails.some(
|
|
209
|
+
(email) => normalizeAuthorEmail(email) === normalizedViewerEmail,
|
|
210
|
+
);
|
|
211
|
+
// One condition for both the control and the filtering it drives — a hidden
|
|
212
|
+
// control with a live filter leaves an empty grid the user cannot reset.
|
|
213
|
+
const canFilterByAuthor = authorEmails.length > 1;
|
|
214
|
+
const byAuthor = canFilterByAuthor
|
|
215
|
+
? filterDesignsByAuthor(designs, author, viewerEmail)
|
|
216
|
+
: designs;
|
|
177
217
|
const filtered = search
|
|
178
|
-
?
|
|
218
|
+
? byAuthor.filter((d) =>
|
|
179
219
|
d.title.toLowerCase().includes(search.toLowerCase()),
|
|
180
220
|
)
|
|
181
|
-
:
|
|
221
|
+
: byAuthor;
|
|
182
222
|
const selectedDesignCount = selectedDesignIds.size;
|
|
183
223
|
const isSelectingDesigns = selectedDesignCount > 0;
|
|
184
224
|
const allVisibleSelected =
|
|
@@ -313,6 +353,15 @@ export default function Index() {
|
|
|
313
353
|
);
|
|
314
354
|
}, []);
|
|
315
355
|
|
|
356
|
+
// Bulk actions operate on the visible set, so narrowing the visible set has
|
|
357
|
+
// to drop selections the user can no longer see.
|
|
358
|
+
const handleAuthorChange = useCallback((next: string) => {
|
|
359
|
+
setAuthor(next);
|
|
360
|
+
setSelectedDesignIds((current) =>
|
|
361
|
+
current.size === 0 ? current : new Set(),
|
|
362
|
+
);
|
|
363
|
+
}, []);
|
|
364
|
+
|
|
316
365
|
const clearSelection = useCallback(() => {
|
|
317
366
|
setSelectedDesignIds(new Set());
|
|
318
367
|
}, []);
|
|
@@ -723,6 +772,37 @@ export default function Index() {
|
|
|
723
772
|
useSetHeaderActions(
|
|
724
773
|
designs.length > 0 ? (
|
|
725
774
|
<div className="flex items-center gap-3">
|
|
775
|
+
{canFilterByAuthor ? (
|
|
776
|
+
<Select value={author} onValueChange={handleAuthorChange}>
|
|
777
|
+
<SelectTrigger
|
|
778
|
+
aria-label={t("home.createdBy")}
|
|
779
|
+
className="h-8 w-40 bg-accent/50 border-border text-sm text-foreground/90"
|
|
780
|
+
>
|
|
781
|
+
<SelectValue placeholder={t("home.createdBy")} />
|
|
782
|
+
</SelectTrigger>
|
|
783
|
+
<SelectContent>
|
|
784
|
+
<SelectItem value={ALL_AUTHORS}>
|
|
785
|
+
{t("home.allAuthors")}
|
|
786
|
+
</SelectItem>
|
|
787
|
+
{viewerHasDesigns ? (
|
|
788
|
+
<SelectItem value={MY_DESIGNS}>{t("home.me")}</SelectItem>
|
|
789
|
+
) : null}
|
|
790
|
+
{authorEmails
|
|
791
|
+
.filter(
|
|
792
|
+
(email) =>
|
|
793
|
+
!(
|
|
794
|
+
viewerHasDesigns &&
|
|
795
|
+
normalizeAuthorEmail(email) === normalizedViewerEmail
|
|
796
|
+
),
|
|
797
|
+
)
|
|
798
|
+
.map((email) => (
|
|
799
|
+
<SelectItem key={email} value={email}>
|
|
800
|
+
{emailToName(email)}
|
|
801
|
+
</SelectItem>
|
|
802
|
+
))}
|
|
803
|
+
</SelectContent>
|
|
804
|
+
</Select>
|
|
805
|
+
) : null}
|
|
726
806
|
<div className="relative">
|
|
727
807
|
<IconSearch className="absolute start-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground/70" />
|
|
728
808
|
<Input
|
|
@@ -881,8 +961,16 @@ export default function Index() {
|
|
|
881
961
|
{design.title}
|
|
882
962
|
</h3>
|
|
883
963
|
</div>
|
|
884
|
-
<div className="text-xs text-muted-foreground/70">
|
|
885
|
-
|
|
964
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground/70">
|
|
965
|
+
<span className="shrink-0">
|
|
966
|
+
{formatDate(design.updatedAt || design.createdAt)}
|
|
967
|
+
</span>
|
|
968
|
+
{showAuthors && design.ownerEmail ? (
|
|
969
|
+
<>
|
|
970
|
+
<span aria-hidden>·</span>
|
|
971
|
+
<DesignAuthorByline email={design.ownerEmail} />
|
|
972
|
+
</>
|
|
973
|
+
) : null}
|
|
886
974
|
</div>
|
|
887
975
|
</div>
|
|
888
976
|
</>
|
|
@@ -1133,6 +1221,28 @@ export default function Index() {
|
|
|
1133
1221
|
);
|
|
1134
1222
|
}
|
|
1135
1223
|
|
|
1224
|
+
/** Who created a design, shown on its library card in shared workspaces. */
|
|
1225
|
+
function DesignAuthorByline({ email }: { email: string }) {
|
|
1226
|
+
const name = emailToName(email);
|
|
1227
|
+
const avatarUrl = useAvatarUrl(email);
|
|
1228
|
+
|
|
1229
|
+
return (
|
|
1230
|
+
<span className="flex min-w-0 items-center gap-1.5" title={email}>
|
|
1231
|
+
<Avatar className="size-4 shrink-0">
|
|
1232
|
+
{avatarUrl ? <AvatarImage src={avatarUrl} alt="" /> : null}
|
|
1233
|
+
<AvatarFallback
|
|
1234
|
+
/* guard:allow-raw-color — emailToColor ignores the theme, so the initial stays white in both. */
|
|
1235
|
+
className="text-[8px] font-semibold text-white"
|
|
1236
|
+
style={{ backgroundColor: emailToColor(email) }}
|
|
1237
|
+
>
|
|
1238
|
+
{name.trim().charAt(0).toUpperCase() || "?"}
|
|
1239
|
+
</AvatarFallback>
|
|
1240
|
+
</Avatar>
|
|
1241
|
+
<span className="truncate">{name}</span>
|
|
1242
|
+
</span>
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1136
1246
|
/**
|
|
1137
1247
|
* Render the design's index.html as a non-interactive thumbnail. The iframe
|
|
1138
1248
|
* renders at a fixed natural size (so designs that assume a desktop viewport
|
|
@@ -182,6 +182,7 @@ export function designTemplateRefinementDirectives(
|
|
|
182
182
|
`This design was copied from template "${templateId}". Its files, canvas dimensions, defaults, and locked layers already exist.`,
|
|
183
183
|
...designSystemTemplateEditDirectives(designSystemId),
|
|
184
184
|
`Call \`get-design-snapshot --designId="${designId}"\` exactly once before editing.`,
|
|
185
|
+
`The copied screens are edited in place, so they stop showing the template once this run saves. \`view-screen\` reports the template's authoritative dimensions and fonts as \`design.createdFromTemplate\` on every turn — keep them unchanged. Call \`get-design-template --designId="${designId}"\` when you need the template's original markup or locked layers.`,
|
|
185
186
|
"Refine the existing template with `edit-design`; do not call `generate-design`, `delete-file`, or create a replacement screen.",
|
|
186
187
|
'Layers marked `data-agent-native-locked="true"` and everything inside them must remain byte-for-byte unchanged. The server rejects changes to locked backgrounds, logos, and other fixed template layers.',
|
|
187
188
|
"Preserve canvasFrames and the template's width and height. Change only the unlocked content needed for the user's request.",
|
|
@@ -23,6 +23,128 @@ export function parseDesignTemplateData(
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface DesignTemplateSourceFile {
|
|
27
|
+
designFileId: string;
|
|
28
|
+
templateFileId: string;
|
|
29
|
+
filename: string | null;
|
|
30
|
+
width: number | null;
|
|
31
|
+
height: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DesignTemplateSource {
|
|
35
|
+
templateId: string;
|
|
36
|
+
title: string | null;
|
|
37
|
+
category: string | null;
|
|
38
|
+
instantiatedAt: string | null;
|
|
39
|
+
appliedDesignSystemId: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Captured at copy time, not re-read from the template. Dimensions and fonts
|
|
42
|
+
* are small enough to restate on every turn, which is what stops a follow-up
|
|
43
|
+
* request from resizing the artboard or swapping the typeface. The full
|
|
44
|
+
* template markup stays behind `get-design-template`.
|
|
45
|
+
*/
|
|
46
|
+
files: DesignTemplateSourceFile[];
|
|
47
|
+
fonts: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const MAX_TRACKED_FONTS = 12;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Font drift is invisible in a layout diff and is the most common way a
|
|
54
|
+
* refined template stops looking like its template. Capture the declared
|
|
55
|
+
* families once so later turns can restate them without re-parsing markup.
|
|
56
|
+
*/
|
|
57
|
+
export function extractTemplateFonts(html: string): string[] {
|
|
58
|
+
const fonts = new Set<string>();
|
|
59
|
+
|
|
60
|
+
for (const match of html.matchAll(/font-family\s*:\s*([^;}]+)/gi)) {
|
|
61
|
+
const family = match[1]
|
|
62
|
+
?.split(",")[0]
|
|
63
|
+
?.trim()
|
|
64
|
+
.replace(/^["']|["']$/g, "");
|
|
65
|
+
if (family && !/^(inherit|initial|unset|var\()/i.test(family)) {
|
|
66
|
+
fonts.add(family);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const link of html.matchAll(/fonts\.googleapis\.com\/[^"'\s>]+/gi)) {
|
|
70
|
+
for (const family of link[0].matchAll(/family=([^&:"']+)/gi)) {
|
|
71
|
+
let name: string;
|
|
72
|
+
try {
|
|
73
|
+
name = decodeURIComponent(family[1]!).replace(/\+/g, " ").trim();
|
|
74
|
+
} catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (name) fonts.add(name);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return [...fonts].slice(0, MAX_TRACKED_FONTS);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function finiteNumber(value: unknown): number | null {
|
|
85
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
86
|
+
? Math.round(value)
|
|
87
|
+
: null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reads the template a design was created from. Returns null when the design
|
|
92
|
+
* was not created from a template; throws when the design claims a template
|
|
93
|
+
* but the record cannot be read, because silently treating that as "no
|
|
94
|
+
* template" would drop the template constraints the design is bound to.
|
|
95
|
+
*/
|
|
96
|
+
export function readDesignTemplateSource(
|
|
97
|
+
data: Record<string, unknown>,
|
|
98
|
+
): DesignTemplateSource | null {
|
|
99
|
+
const raw = data.templateSource;
|
|
100
|
+
if (raw === undefined || raw === null) return null;
|
|
101
|
+
|
|
102
|
+
const source = record(raw);
|
|
103
|
+
const templateId = source.templateId;
|
|
104
|
+
if (typeof templateId !== "string" || !templateId.trim()) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Design records a templateSource without a readable templateId",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
templateId,
|
|
112
|
+
title: typeof source.title === "string" ? source.title : null,
|
|
113
|
+
category: typeof source.category === "string" ? source.category : null,
|
|
114
|
+
instantiatedAt:
|
|
115
|
+
typeof source.instantiatedAt === "string" ? source.instantiatedAt : null,
|
|
116
|
+
appliedDesignSystemId:
|
|
117
|
+
typeof source.appliedDesignSystemId === "string"
|
|
118
|
+
? source.appliedDesignSystemId
|
|
119
|
+
: null,
|
|
120
|
+
files: (Array.isArray(source.files) ? source.files : []).flatMap(
|
|
121
|
+
(entry) => {
|
|
122
|
+
const file = record(entry);
|
|
123
|
+
const designFileId = file.designFileId;
|
|
124
|
+
const templateFileId = file.templateFileId;
|
|
125
|
+
if (
|
|
126
|
+
typeof designFileId !== "string" ||
|
|
127
|
+
typeof templateFileId !== "string"
|
|
128
|
+
) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
designFileId,
|
|
134
|
+
templateFileId,
|
|
135
|
+
filename: typeof file.filename === "string" ? file.filename : null,
|
|
136
|
+
width: finiteNumber(file.width),
|
|
137
|
+
height: finiteNumber(file.height),
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
},
|
|
141
|
+
),
|
|
142
|
+
fonts: (Array.isArray(source.fonts) ? source.fonts : []).filter(
|
|
143
|
+
(font): font is string => typeof font === "string",
|
|
144
|
+
),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
26
148
|
/**
|
|
27
149
|
* Templates are portable/shareable snapshots, so they must never retain
|
|
28
150
|
* localhost bridge credentials. Reuse the same viewer-safe redaction applied
|
|
@@ -68,6 +190,27 @@ export function remapTemplateFileIds(
|
|
|
68
190
|
return next;
|
|
69
191
|
}
|
|
70
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Exact frame lookup for one file. Unlike `firstTemplateDimensions` this never
|
|
195
|
+
* falls back to another screen's frame: a screen with no recorded frame must
|
|
196
|
+
* report "unknown", not a neighbour's dimensions.
|
|
197
|
+
*/
|
|
198
|
+
export function templateFileDimensions(
|
|
199
|
+
data: Record<string, unknown>,
|
|
200
|
+
fileId: string,
|
|
201
|
+
): { width: number | null; height: number | null } {
|
|
202
|
+
const frame = record(record(data.canvasFrames)[fileId]) as CanvasFrame;
|
|
203
|
+
const width =
|
|
204
|
+
typeof frame.width === "number" && Number.isFinite(frame.width)
|
|
205
|
+
? Math.round(frame.width)
|
|
206
|
+
: null;
|
|
207
|
+
const height =
|
|
208
|
+
typeof frame.height === "number" && Number.isFinite(frame.height)
|
|
209
|
+
? Math.round(frame.height)
|
|
210
|
+
: null;
|
|
211
|
+
return { width, height };
|
|
212
|
+
}
|
|
213
|
+
|
|
71
214
|
export function firstTemplateDimensions(
|
|
72
215
|
data: Record<string, unknown>,
|
|
73
216
|
preferredFileId?: string,
|
|
@@ -35,8 +35,8 @@ ladder.
|
|
|
35
35
|
native inline summary/table and an "Open editor" expansion path.
|
|
36
36
|
- For an anonymous feedback form or survey, create all fields in one
|
|
37
37
|
`create-form` call with `status: "published"`, verify the persisted form, and
|
|
38
|
-
|
|
39
|
-
the
|
|
38
|
+
copy the returned `publicUrl` verbatim. Never derive the link from `slug` or
|
|
39
|
+
omit the public `/f/<slug>` route segment.
|
|
40
40
|
- To email the form owner when someone submits a response, set
|
|
41
41
|
`settings.emailOnNewResponses: true` through `create-form` or `update-form`.
|
|
42
42
|
Delivery uses the configured framework email provider (`RESEND_API_KEY` or
|
|
@@ -39,7 +39,7 @@ function formDeepLink(formId: string): string {
|
|
|
39
39
|
|
|
40
40
|
export default defineAction({
|
|
41
41
|
description:
|
|
42
|
-
"Create a draft or published form. Set settings.anonymous=true to suppress submitter IP, identity, and source metadata, or settings.emailOnNewResponses=true to email the form owner when responses arrive. Published
|
|
42
|
+
"Create a draft or published form. Set settings.anonymous=true to suppress submitter IP, identity, and source metadata, or settings.emailOnNewResponses=true to email the form owner when responses arrive. Published results include a canonical publicUrl; copy it verbatim (it includes /f/<slug>) instead of deriving a URL from slug. Drafts return an editor URL.",
|
|
43
43
|
schema: z.object({
|
|
44
44
|
title: z.string().optional().describe("Form title"),
|
|
45
45
|
description: z.string().optional().describe("Form description"),
|
|
@@ -142,11 +142,11 @@ export default defineAction({
|
|
|
142
142
|
.set(updates)
|
|
143
143
|
.where(eq(schema.forms.id, args.id));
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
// Do not re-read after the write. On pooled Postgres a follow-up SELECT
|
|
146
|
+
// can land on a lagging replica and return the pre-update field array,
|
|
147
|
+
// which makes the agent report stale state and can overwrite a later edit
|
|
148
|
+
// with that stale snapshot. The values written above are already validated.
|
|
149
|
+
const row = { ...existing, ...updates } as typeof existing;
|
|
150
150
|
|
|
151
151
|
invalidatePublicFormCache(existing, row);
|
|
152
152
|
|