@agent-native/core 0.90.5 → 0.90.8

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 (113) hide show
  1. package/README.md +34 -7
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +20 -0
  4. package/corpus/core/docs/content/agent-native-toolkit.mdx +1 -1
  5. package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
  6. package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
  7. package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
  8. package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
  9. package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
  10. package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
  11. package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
  12. package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
  13. package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
  14. package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
  15. package/corpus/core/docs/content/toolkit-agent-ux.mdx +1 -1
  16. package/corpus/core/docs/content/toolkit-app-adapters.mdx +1 -1
  17. package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +1 -1
  18. package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
  19. package/corpus/core/docs/content/toolkit-command-navigation.mdx +2 -2
  20. package/corpus/core/docs/content/toolkit-comments-review.mdx +1 -1
  21. package/corpus/core/docs/content/toolkit-history.mdx +1 -1
  22. package/corpus/core/docs/content/toolkit-observability.mdx +1 -1
  23. package/corpus/core/docs/content/toolkit-org-team.mdx +1 -1
  24. package/corpus/core/docs/content/toolkit-resources.mdx +1 -1
  25. package/corpus/core/docs/content/toolkit-settings.mdx +1 -1
  26. package/corpus/core/docs/content/toolkit-setup-connections.mdx +1 -1
  27. package/corpus/core/docs/content/toolkit-sharing-ui.mdx +1 -1
  28. package/corpus/core/docs/content/toolkit-sharing.mdx +1 -1
  29. package/corpus/core/docs/content/toolkit-shell-hooks.mdx +1 -1
  30. package/corpus/core/docs/content/toolkit-ui.mdx +1 -1
  31. package/corpus/core/package.json +1 -1
  32. package/corpus/core/src/client/use-db-sync.ts +99 -1
  33. package/corpus/core/src/collab/client.ts +49 -4
  34. package/corpus/core/src/deploy/build.ts +156 -0
  35. package/corpus/core/src/server/core-routes-plugin.ts +137 -8
  36. package/corpus/templates/clips/changelog/2026-07-07-faster-local-whisper-dictation-start.md +6 -0
  37. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +311 -31
  38. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +2 -0
  39. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +9 -0
  40. package/corpus/templates/design/actions/read-source-file.ts +28 -0
  41. package/corpus/templates/design/actions/update-file.ts +76 -7
  42. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +181 -1
  43. package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -0
  44. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +387 -25
  45. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1110 -88
  46. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +368 -16
  47. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +24 -0
  48. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +27 -6
  49. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +18 -0
  50. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +5 -4
  51. package/corpus/templates/design/app/components/design/multi-screen/handle-hit-zones.ts +124 -0
  52. package/corpus/templates/design/app/components/design/multi-screen/types.ts +23 -0
  53. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  54. package/corpus/templates/design/app/i18n-data.ts +19 -0
  55. package/corpus/templates/design/changelog/2026-07-07-dragging-a-free-floating-element-into-a-layout-column-or-row.md +6 -0
  56. package/corpus/templates/design/changelog/2026-07-07-dragging-a-repeated-list-item-like-a-to-do-card-generated-fr.md +6 -0
  57. package/corpus/templates/design/changelog/2026-07-07-dragging-an-element-onto-a-button-or-chip-s-middle-no-longer.md +6 -0
  58. package/corpus/templates/design/changelog/2026-07-07-elements-dropped-into-another-board-shape-now-stay-exactly-w.md +6 -0
  59. package/corpus/templates/design/changelog/2026-07-07-fixed-drag-reordering-elements-within-a-screen-no-longer-sil.md +6 -0
  60. package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-an-element-from-a-screen-onto-the-canvas-now-.md +6 -0
  61. package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-elements-between-screens-into-ai-generated-co.md +6 -0
  62. package/corpus/templates/design/changelog/2026-07-07-fixed-moving-elements-between-screens-no-longer-bakes-intern.md +6 -0
  63. package/corpus/templates/design/changelog/2026-07-07-fixed-the-cross-screen-drop-indicator-now-points-at-the-corr.md +6 -0
  64. package/corpus/templates/design/changelog/2026-07-07-fixed-the-zoom-menu-no-longer-leaves-the-canvas-unresponsive.md +6 -0
  65. package/corpus/templates/design/changelog/2026-07-07-text-drawn-on-the-canvas-is-now-readable-white-instead-of-in.md +6 -0
  66. package/corpus/templates/design/server/source-workspace.ts +27 -3
  67. package/corpus/templates/design/shared/board-file.ts +359 -0
  68. package/corpus/templates/design/shared/breakpoint-media.ts +21 -3
  69. package/corpus/templates/design/shared/code-layer.ts +145 -10
  70. package/dist/client/use-db-sync.d.ts +18 -0
  71. package/dist/client/use-db-sync.d.ts.map +1 -1
  72. package/dist/client/use-db-sync.js +88 -1
  73. package/dist/client/use-db-sync.js.map +1 -1
  74. package/dist/collab/client.d.ts.map +1 -1
  75. package/dist/collab/client.js +43 -4
  76. package/dist/collab/client.js.map +1 -1
  77. package/dist/deploy/build.d.ts +2 -0
  78. package/dist/deploy/build.d.ts.map +1 -1
  79. package/dist/deploy/build.js +96 -0
  80. package/dist/deploy/build.js.map +1 -1
  81. package/dist/observability/routes.d.ts +3 -3
  82. package/dist/server/core-routes-plugin.d.ts +10 -0
  83. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  84. package/dist/server/core-routes-plugin.js +103 -9
  85. package/dist/server/core-routes-plugin.js.map +1 -1
  86. package/docs/content/agent-native-toolkit.mdx +1 -1
  87. package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
  88. package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
  89. package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
  90. package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
  91. package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
  92. package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
  93. package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
  94. package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
  95. package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
  96. package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
  97. package/docs/content/toolkit-agent-ux.mdx +1 -1
  98. package/docs/content/toolkit-app-adapters.mdx +1 -1
  99. package/docs/content/toolkit-collaboration-ui.mdx +1 -1
  100. package/docs/content/toolkit-collaboration.mdx +1 -1
  101. package/docs/content/toolkit-command-navigation.mdx +2 -2
  102. package/docs/content/toolkit-comments-review.mdx +1 -1
  103. package/docs/content/toolkit-history.mdx +1 -1
  104. package/docs/content/toolkit-observability.mdx +1 -1
  105. package/docs/content/toolkit-org-team.mdx +1 -1
  106. package/docs/content/toolkit-resources.mdx +1 -1
  107. package/docs/content/toolkit-settings.mdx +1 -1
  108. package/docs/content/toolkit-setup-connections.mdx +1 -1
  109. package/docs/content/toolkit-sharing-ui.mdx +1 -1
  110. package/docs/content/toolkit-sharing.mdx +1 -1
  111. package/docs/content/toolkit-shell-hooks.mdx +1 -1
  112. package/docs/content/toolkit-ui.mdx +1 -1
  113. package/package.json +1 -1
@@ -13,7 +13,7 @@ that operators use to understand what happened.
13
13
  <Screen
14
14
  surface="desktop"
15
15
  html={
16
- "<div style='min-height:560px;box-sizing:border-box;padding:26px;display:grid;grid-template-columns:1fr 300px;gap:16px'><main class='wf-card' style='display:flex;flex-direction:column;gap:12px'><h2 style='margin:0'>Observability</h2><div style='display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px'><div class='wf-box'>Runs 482</div><div class='wf-box'>Spend $42</div><div class='wf-box'>Errors 3</div></div><div class='wf-box' style='height:260px'>Replay timeline and traces</div></main><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><strong>Diagnostics</strong><div class='wf-box'>Run failed: missing key</div><div class='wf-box'>User feedback: thumbs down</div><div class='wf-box'>Slow tool: provider-api-request</div><button class='primary'>Open run</button></aside></div>"
16
+ "<div style='min-height:540px;box-sizing:border-box;padding:24px;display:grid;grid-template-columns:1fr 300px;gap:16px'><main class='wf-card' style='display:flex;flex-direction:column;gap:14px'><h2 style='margin:0'>Observability</h2><div style='display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px'><div class='wf-box'><small class='wf-muted'>Runs</small><br/><strong style='font-size:20px'>482</strong></div><div class='wf-box'><small class='wf-muted'>Spend</small><br/><strong style='font-size:20px'>$42</strong></div><div class='wf-box'><small class='wf-muted'>Errors</small><br/><strong style='font-size:20px'>3</strong></div></div><div class='wf-box' style='display:flex;flex-direction:column;gap:10px'><small class='wf-muted'>Runs per day</small><div style='display:flex;align-items:flex-end;gap:6px;height:150px'><div style='flex:1;height:40%;background:var(--wf-accent-soft);border-radius:3px'></div><div style='flex:1;height:60%;background:var(--wf-accent-soft);border-radius:3px'></div><div style='flex:1;height:50%;background:var(--wf-accent-soft);border-radius:3px'></div><div style='flex:1;height:75%;background:var(--wf-accent-soft);border-radius:3px'></div><div style='flex:1;height:65%;background:var(--wf-accent-soft);border-radius:3px'></div><div style='flex:1;height:90%;background:var(--wf-accent);border-radius:3px'></div><div style='flex:1;height:80%;background:var(--wf-accent-soft);border-radius:3px'></div></div></div></main><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><strong>Diagnostics</strong><div class='wf-box' style='display:flex;align-items:center;gap:8px'><span data-icon='x'></span><div style='flex:1'>Run failed: missing key</div></div><div class='wf-box' style='display:flex;align-items:center;gap:8px'><span data-icon='bell'></span><div style='flex:1'>Feedback: thumbs down</div></div><div class='wf-box' style='display:flex;align-items:center;gap:8px'><span data-icon='calendar'></span><div style='flex:1'>Slow tool: provider-api</div></div><div style='flex:1'></div><button class='primary'>Open run</button></aside></div>"
17
17
  }
18
18
  />
19
19
  </WireframeBlock>
@@ -14,7 +14,7 @@ connections, service tokens, and app switching.
14
14
  <Screen
15
15
  surface="desktop"
16
16
  html={
17
- "<div style='min-height:540px;box-sizing:border-box;padding:28px;display:grid;grid-template-columns:240px 1fr;gap:18px'><aside class='wf-card' style='display:flex;flex-direction:column;gap:12px'><strong>Workspace</strong><div class='wf-box'>OrgSwitcher: Acme</div><div class='wf-pill'>Analytics</div><div class='wf-pill'>Content</div><div class='wf-pill'>Slides</div></aside><main class='wf-card' style='display:flex;flex-direction:column;gap:14px'><div style='display:flex;align-items:center;gap:10px'><h2 style='margin:0'>Team</h2><div style='flex:1'></div><button class='primary'>Invite</button></div><div class='wf-box'>Allowed domain: acme.com</div><div class='wf-box'>Steve - owner</div><div class='wf-box'>Maya - admin</div><div class='wf-box'>Kai - member</div><div class='wf-box'>Pending invites: 3</div></main></div>"
17
+ "<div style='min-height:540px;box-sizing:border-box;padding:26px;display:grid;grid-template-columns:210px 1fr;gap:18px'><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><small class='wf-muted'>Organization</small><div class='wf-box' style='display:flex;align-items:center;gap:8px;padding:8px 10px'><div style='width:24px;height:24px;border-radius:7px;background:var(--wf-accent-soft);display:flex;align-items:center;justify-content:center;font-size:11px;flex:none'>A</div><strong style='flex:1'>Acme</strong><span data-icon='chevronDown'></span></div><div style='display:flex;flex-direction:column;gap:4px;margin-top:4px'><div style='padding:6px 9px;border-radius:8px;background:var(--wf-accent-soft)'>Analytics</div><div style='padding:6px 9px;border-radius:8px'>Content</div><div style='padding:6px 9px;border-radius:8px'>Slides</div></div></aside><main class='wf-card' style='display:flex;flex-direction:column;gap:12px'><div style='display:flex;align-items:center;gap:10px'><h2 style='margin:0'>Team</h2><span class='wf-pill'>8 members</span><div style='flex:1'></div><button class='primary'><span data-icon='plus'></span> Invite</button></div><div style='display:flex;align-items:center;gap:10px'><div style='width:28px;height:28px;border-radius:999px;background:var(--wf-accent-soft);flex:none'></div><div style='flex:1;min-width:0'><strong>Steve Ray</strong><br/><small class='wf-muted'>steve@acme.com</small></div><span class='wf-pill'>Owner</span></div><div style='display:flex;align-items:center;gap:10px'><div style='width:28px;height:28px;border-radius:999px;background:var(--wf-accent-soft);flex:none'></div><div style='flex:1;min-width:0'><strong>Maya Chen</strong><br/><small class='wf-muted'>maya@acme.com</small></div><span class='wf-pill'>Admin <span data-icon='chevronDown'></span></span></div><div style='display:flex;align-items:center;gap:10px'><div style='width:28px;height:28px;border-radius:999px;background:var(--wf-accent-soft);flex:none'></div><div style='flex:1;min-width:0'><strong>Kai Wong</strong><br/><small class='wf-muted'>kai@acme.com</small></div><span class='wf-pill'>Member <span data-icon='chevronDown'></span></span></div><hr/><div style='display:flex;align-items:center;gap:8px'><span data-icon='mail'></span><div style='flex:1'>Anyone with an acme.com email can join</div></div><div style='display:flex;align-items:center;gap:8px'><span data-icon='user'></span><div style='flex:1'>Pending invites</div><span class='wf-pill'>3</span></div></main></div>"
18
18
  }
19
19
  />
20
20
  </WireframeBlock>
@@ -14,7 +14,7 @@ for repo-backed content.
14
14
  <Screen
15
15
  surface="desktop"
16
16
  html={
17
- "<div style='min-height:560px;box-sizing:border-box;padding:26px;display:grid;grid-template-columns:260px 1fr 240px;gap:16px'><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><strong>Resources</strong><div class='wf-box'>docs/</div><div class='wf-box'>blog/</div><div class='wf-box'>assets/</div><div class='wf-box'>Q3 forecast.md</div></aside><main class='wf-card' style='display:flex;flex-direction:column;gap:12px'><h2 style='margin:0'>Q3 forecast.md</h2><div class='wf-box' style='height:330px'>ResourceEditor</div><button class='primary'>Save</button></main><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><strong>Attachments</strong><div class='wf-box'>Drop file</div><div class='wf-box'>Search resources</div><div class='wf-box'>local file mode: on</div></aside></div>"
17
+ "<div style='min-height:540px;box-sizing:border-box;padding:24px;display:grid;grid-template-columns:230px 1fr 220px;gap:14px'><aside class='wf-card' style='display:flex;flex-direction:column;gap:4px'><div style='display:flex;align-items:center;gap:8px;margin-bottom:6px'><strong style='flex:1'>Resources</strong><span data-icon='search'></span><span data-icon='plus'></span></div><div style='display:flex;align-items:center;gap:8px;padding:6px 8px'><span data-icon='chevronDown'></span> docs</div><div style='display:flex;align-items:center;gap:8px;padding:6px 8px 6px 22px;border-radius:8px;background:var(--wf-accent-soft)'><span data-icon='edit'></span> Q3 forecast.md</div><div style='display:flex;align-items:center;gap:8px;padding:6px 8px 6px 22px'><span data-icon='edit'></span> roadmap.md</div><div style='display:flex;align-items:center;gap:8px;padding:6px 8px'><span data-icon='chevronRight'></span> blog</div><div style='display:flex;align-items:center;gap:8px;padding:6px 8px'><span data-icon='chevronRight'></span> assets</div></aside><main class='wf-card' style='display:flex;flex-direction:column;gap:12px'><div style='display:flex;align-items:center;gap:10px'><h2 style='margin:0'>Q3 forecast.md</h2><div style='flex:1'></div><button class='primary'>Save</button></div><div style='display:flex;flex-direction:column;gap:8px'><div style='height:11px;width:60%;background:var(--wf-line);border-radius:4px'></div><div style='height:11px;width:92%;background:var(--wf-line);border-radius:4px'></div><div style='height:11px;width:85%;background:var(--wf-line);border-radius:4px'></div><div style='height:11px;width:95%;background:var(--wf-line);border-radius:4px'></div><div style='height:11px;width:70%;background:var(--wf-line);border-radius:4px'></div><div style='height:11px;width:88%;background:var(--wf-line);border-radius:4px'></div></div></main><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><strong>Attachments</strong><div style='border:1.6px dashed var(--wf-line);border-radius:var(--wf-radius);padding:16px;text-align:center'><span data-icon='plus'></span><br/><small class='wf-muted'>Drop files</small></div><div class='wf-box' style='display:flex;align-items:center;gap:8px'><span data-icon='search'></span><span class='wf-muted'>Search resources</span></div><div style='flex:1'></div><div style='display:flex;align-items:center;gap:8px'><span data-icon='check'></span><small>Local file mode on</small></div></aside></div>"
18
18
  }
19
19
  />
20
20
  </WireframeBlock>
@@ -14,7 +14,7 @@ page; framework tabs can be mounted inside it.
14
14
  <Screen
15
15
  surface="desktop"
16
16
  html={
17
- "<div style='min-height:540px;box-sizing:border-box;padding:28px;display:grid;grid-template-columns:220px 1fr;gap:18px'><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><strong>Settings</strong><div class='wf-pill'>General</div><div class='wf-pill'>AI and models</div><div class='wf-pill'>Connections</div><div class='wf-pill'>Team</div><div class='wf-pill'>Developer</div></aside><main class='wf-card' style='display:flex;flex-direction:column;gap:14px'><h2 style='margin:0'>AI and models</h2><div class='wf-box'>Default model: GPT-5</div><div class='wf-box'>LLM keys: OpenAI configured, Anthropic missing</div><div class='wf-box'>Provider readiness: Builder connected</div><div style='display:flex;gap:10px'><button>Open agent settings</button><button class='primary'>Save changes</button></div></main></div>"
17
+ "<div style='min-height:520px;box-sizing:border-box;padding:26px;display:grid;grid-template-columns:200px 1fr;gap:18px'><aside class='wf-card' style='display:flex;flex-direction:column;gap:6px'><strong style='margin-bottom:4px'>Settings</strong><div style='padding:7px 9px;border-radius:8px'>General</div><div style='padding:7px 9px;border-radius:8px;background:var(--wf-accent-soft);display:flex;align-items:center;gap:8px'><span data-icon='settings'></span> AI and models</div><div style='padding:7px 9px;border-radius:8px'>Connections</div><div style='padding:7px 9px;border-radius:8px'>Team</div><div style='padding:7px 9px;border-radius:8px'>Developer</div></aside><main class='wf-card' style='display:flex;flex-direction:column;gap:14px'><h2 style='margin:0'>AI and models</h2><div style='display:flex;flex-direction:column;gap:6px'><small class='wf-muted'>Default model</small><div class='wf-box' style='display:flex;align-items:center;gap:8px'><div style='flex:1'>GPT-5</div><span data-icon='chevronDown'></span></div></div><div style='display:flex;flex-direction:column;gap:8px'><small class='wf-muted'>Provider keys</small><div class='wf-box' style='display:flex;align-items:center;gap:10px'><span data-icon='check'></span><div style='flex:1'>OpenAI</div><span class='wf-pill'>Connected</span></div><div class='wf-box' style='display:flex;align-items:center;gap:10px'><span data-icon='lock'></span><div style='flex:1'>Anthropic</div><button>Add key</button></div><div class='wf-box' style='display:flex;align-items:center;gap:10px'><span data-icon='check'></span><div style='flex:1'>Builder</div><span class='wf-pill'>Connected</span></div></div><div style='flex:1'></div><div style='display:flex;gap:10px'><button>Open agent settings</button><div style='flex:1'></div><button class='primary'>Save changes</button></div></main></div>"
18
18
  }
19
19
  />
20
20
  </WireframeBlock>
@@ -14,7 +14,7 @@ what is missing and link users to the exact fix.
14
14
  <Screen
15
15
  surface="desktop"
16
16
  html={
17
- "<div style='min-height:540px;box-sizing:border-box;padding:28px;display:grid;grid-template-columns:1fr 330px;gap:18px'><main class='wf-card' style='display:flex;flex-direction:column;gap:14px'><h2 style='margin:0'>Setup checklist</h2><div class='wf-box'>1. Connect Builder - complete</div><div class='wf-box'>2. Add OpenAI key - needs attention</div><div class='wf-box'>3. Connect Slack workspace - optional</div><div class='wf-box'>4. Enable GitHub OAuth - ready</div></main><aside class='wf-card' style='display:flex;flex-direction:column;gap:12px'><strong>Provider readiness</strong><div class='wf-box'>Builder: connected</div><div class='wf-box'>LLM: missing key</div><div class='wf-box'>GitHub: OAuth ready</div><button class='primary'>Open Settings</button></aside></div>"
17
+ "<div style='min-height:520px;box-sizing:border-box;padding:26px;display:grid;grid-template-columns:1fr 300px;gap:18px'><main class='wf-card' style='display:flex;flex-direction:column;gap:12px'><h2 style='margin:0'>Finish setup</h2><small class='wf-muted'>2 of 4 steps complete</small><div class='wf-box' style='display:flex;align-items:center;gap:10px'><span data-icon='check'></span><div style='flex:1'>Connect Builder</div><span class='wf-pill'>Done</span></div><div class='wf-box' style='display:flex;align-items:center;gap:10px;border-color:var(--wf-accent)'><span data-icon='lock'></span><div style='flex:1'><strong>Add OpenAI key</strong><br/><small class='wf-muted'>Needed for the agent to run</small></div><button class='primary'>Fix</button></div><div class='wf-box' style='display:flex;align-items:center;gap:10px'><span data-icon='plus'></span><div style='flex:1'>Connect Slack</div><span class='wf-muted'>Optional</span></div><div class='wf-box' style='display:flex;align-items:center;gap:10px'><span data-icon='check'></span><div style='flex:1'>Enable GitHub OAuth</div><span class='wf-pill'>Ready</span></div></main><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><strong>Provider readiness</strong><div style='display:flex;align-items:center;gap:8px'><span data-icon='check'></span><div style='flex:1'>Builder</div><span class='wf-pill'>Connected</span></div><div style='display:flex;align-items:center;gap:8px'><span data-icon='x'></span><div style='flex:1'>LLM key</div><span class='wf-pill'>Missing</span></div><div style='display:flex;align-items:center;gap:8px'><span data-icon='check'></span><div style='flex:1'>GitHub</div><span class='wf-pill'>OAuth ready</span></div><div style='flex:1'></div><button class='primary'>Open settings</button></aside></div>"
18
18
  }
19
19
  />
20
20
  </WireframeBlock>
@@ -13,7 +13,7 @@ mutations; core and app wrappers inject data and callbacks.
13
13
  <Screen
14
14
  surface="desktop"
15
15
  html={
16
- "<div style='min-height:480px;box-sizing:border-box;padding:28px;display:grid;grid-template-columns:1fr 300px;gap:16px'><main class='wf-card' style='display:flex;flex-direction:column;gap:12px'><div style='display:flex;align-items:center;gap:10px'><h2 style='margin:0'>Resource header</h2><span class='wf-pill'>Org visible</span><div style='flex:1'></div><button>Share</button></div><div class='wf-box' style='height:120px'>Share list row</div><div class='wf-box' style='height:180px'>Shared resource preview</div></main><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><strong>Presentational pieces</strong><div class='wf-box'>VisibilityBadge</div><div class='wf-box'>Grant row</div><div class='wf-box'>Role selector</div><div class='wf-box'>Policy hint</div></aside></div>"
16
+ "<div style='min-height:480px;box-sizing:border-box;padding:26px;display:grid;grid-template-columns:1fr 300px;gap:16px'><main class='wf-card' style='display:flex;flex-direction:column;gap:14px'><div style='display:flex;align-items:center;gap:10px'><h2 style='margin:0'>Roadmap</h2><span class='wf-pill accent'>Org visible</span><div style='flex:1'></div><button><span data-icon='user'></span> Share</button></div><small class='wf-muted'>Visibility badges</small><div style='display:flex;gap:6px'><span class='wf-pill'><span data-icon='lock'></span> Private</span><span class='wf-pill'>Shared</span><span class='wf-pill accent'>Org</span><span class='wf-pill'>Public</span></div><div style='display:flex;flex-direction:column;gap:8px'><div style='height:11px;width:88%;background:var(--wf-line);border-radius:4px'></div><div style='height:11px;width:72%;background:var(--wf-line);border-radius:4px'></div></div></main><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><strong>Share</strong><div style='display:flex;align-items:center;gap:10px'><div style='width:28px;height:28px;border-radius:999px;background:var(--wf-accent-soft);flex:none'></div><div style='flex:1;min-width:0'><strong>Maya Chen</strong><br/><small class='wf-muted'>maya@acme.com</small></div><span class='wf-pill'>Editor <span data-icon='chevronDown'></span></span></div><div class='wf-card' style='display:flex;flex-direction:column;gap:2px;padding:6px'><div style='padding:6px 8px;border-radius:6px;background:var(--wf-accent-soft);display:flex;align-items:center;gap:8px'><span data-icon='check'></span> Editor</div><div style='padding:6px 8px'>Viewer</div><div style='padding:6px 8px'>Remove</div></div><div style='display:flex;align-items:center;gap:8px'><span data-icon='lock'></span><small class='wf-muted'>Public links disabled by policy</small></div></aside></div>"
17
17
  }
18
18
  />
19
19
  </WireframeBlock>
@@ -14,7 +14,7 @@ links.
14
14
  <Screen
15
15
  surface="desktop"
16
16
  html={
17
- "<div style='min-height:540px;box-sizing:border-box;padding:28px;display:grid;grid-template-columns:1fr 320px;gap:18px'><main class='wf-card' style='display:flex;flex-direction:column;gap:14px'><div style='display:flex;align-items:center;gap:10px'><h2 style='margin:0'>Q3 Forecast</h2><span class='wf-pill'>Org visible</span><div style='flex:1'></div><button>Share</button></div><div class='wf-box' style='height:92px'>Dashboard preview</div><div class='wf-box' style='height:260px'>Resource content</div></main><aside class='wf-card' style='display:flex;flex-direction:column;gap:12px'><strong>Share resource</strong><div class='wf-box'>Visibility: Organization</div><div class='wf-box'>Invite teammate@example.com</div><div class='wf-box'>Role: Editor</div><button class='primary'>Send invite</button><div class='wf-box'>Public link disabled by policy</div></aside></div>"
17
+ "<div style='min-height:540px;box-sizing:border-box;padding:26px;display:grid;grid-template-columns:1fr 320px;gap:18px'><main class='wf-card' style='display:flex;flex-direction:column;gap:14px'><div style='display:flex;align-items:center;gap:10px'><h2 style='margin:0'>Q3 Forecast</h2><span class='wf-pill accent'>Org visible</span><div style='flex:1'></div><button><span data-icon='user'></span> Share</button></div><div class='wf-box' style='display:flex;flex-direction:column;gap:10px'><small class='wf-muted'>Revenue by month</small><div style='display:flex;align-items:flex-end;gap:10px;height:120px'><div style='flex:1;height:50%;background:var(--wf-accent-soft);border-radius:4px'></div><div style='flex:1;height:75%;background:var(--wf-accent-soft);border-radius:4px'></div><div style='flex:1;height:60%;background:var(--wf-accent-soft);border-radius:4px'></div><div style='flex:1;height:92%;background:var(--wf-accent);border-radius:4px'></div></div></div><div style='display:flex;flex-direction:column;gap:8px'><div style='height:11px;width:70%;background:var(--wf-line);border-radius:4px'></div><div style='height:11px;width:90%;background:var(--wf-line);border-radius:4px'></div></div></main><aside class='wf-card' style='display:flex;flex-direction:column;gap:12px'><strong>Share Q3 Forecast</strong><div style='display:flex;align-items:center;gap:8px'><div class='wf-box' style='flex:1;display:flex;align-items:center;gap:8px;padding:8px 10px'><span data-icon='mail'></span><span class='wf-muted'>Add people or teams</span></div><button class='primary'>Invite</button></div><div style='display:flex;align-items:center;gap:10px'><div style='width:28px;height:28px;border-radius:999px;background:var(--wf-accent-soft);flex:none'></div><div style='flex:1;min-width:0'><strong>Steve Ray</strong><br/><small class='wf-muted'>steve@acme.com</small></div><small class='wf-muted'>Owner</small></div><div style='display:flex;align-items:center;gap:10px'><div style='width:28px;height:28px;border-radius:999px;background:var(--wf-accent-soft);flex:none'></div><div style='flex:1;min-width:0'><strong>Maya Chen</strong><br/><small class='wf-muted'>maya@acme.com</small></div><span class='wf-pill'>Editor <span data-icon='chevronDown'></span></span></div><hr/><div style='display:flex;align-items:center;gap:8px'><span data-icon='user'></span><div style='flex:1'>Anyone at Acme</div><span class='wf-pill'>Can view <span data-icon='chevronDown'></span></span></div><div style='display:flex;align-items:center;gap:8px'><span data-icon='lock'></span><div style='flex:1'>Public link</div><span class='wf-muted'>Off</span></div></aside></div>"
18
18
  }
19
19
  />
20
20
  </WireframeBlock>
@@ -11,7 +11,7 @@ Toolkit includes small shell helpers and hooks that are useful across apps:
11
11
  <Screen
12
12
  surface="desktop"
13
13
  html={
14
- "<div style='min-height:480px;box-sizing:border-box;padding:28px;display:grid;grid-template-columns:230px 1fr;gap:16px'><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><strong>Shell</strong><div class='wf-pill'>Header actions</div><div class='wf-pill'>Mobile state</div><div class='wf-pill'>Toasts</div></aside><main class='wf-card' style='display:flex;flex-direction:column;gap:14px'><div style='display:flex;align-items:center;gap:10px'><h2 style='margin:0'>Page title</h2><div style='flex:1'></div><button>Export</button><button class='primary'>Create</button></div><div class='wf-box' style='height:250px'>App content using shared shell hooks</div><div class='wf-box'>Toast: Saved changes</div></main></div>"
14
+ "<div style='min-height:460px;box-sizing:border-box;padding:24px;display:grid;grid-template-columns:200px 1fr;gap:16px'><aside class='wf-card' style='display:flex;flex-direction:column;gap:6px'><strong style='margin-bottom:4px'>Analytics</strong><div style='padding:6px 9px;border-radius:8px;background:var(--wf-accent-soft)'>Dashboards</div><div style='padding:6px 9px;border-radius:8px'>Reports</div><div style='padding:6px 9px;border-radius:8px'>Settings</div></aside><main style='position:relative;display:flex;flex-direction:column;gap:14px'><div class='wf-card' style='display:flex;align-items:center;gap:10px'><h2 style='margin:0'>Dashboards</h2><div style='flex:1'></div><button><span data-icon='send'></span> Export</button><button class='primary'><span data-icon='plus'></span> Create</button></div><div class='wf-card' style='flex:1;display:flex;flex-direction:column;gap:8px'><div style='height:11px;width:70%;background:var(--wf-line);border-radius:4px'></div><div style='height:11px;width:90%;background:var(--wf-line);border-radius:4px'></div><div style='height:11px;width:80%;background:var(--wf-line);border-radius:4px'></div></div><div class='wf-card' style='display:flex;align-items:center;gap:8px;border-color:var(--wf-accent);position:absolute;right:0;bottom:0;width:230px'><span data-icon='check'></span><div style='flex:1'>Saved changes</div><span data-icon='x'></span></div></main></div>"
15
15
  }
16
16
  />
17
17
  </WireframeBlock>
@@ -12,7 +12,7 @@ shared shadcn-style building blocks used by templates and reusable kits.
12
12
  <Screen
13
13
  surface="desktop"
14
14
  html={
15
- "<div style='min-height:480px;box-sizing:border-box;padding:28px;display:grid;grid-template-columns:220px 1fr;gap:16px'><aside class='wf-card' style='display:flex;flex-direction:column;gap:10px'><strong>Primitives</strong><div class='wf-pill'>Button</div><div class='wf-pill'>Dialog</div><div class='wf-pill'>Popover</div><div class='wf-pill'>Tooltip</div></aside><main class='wf-card' style='display:flex;flex-direction:column;gap:14px'><h2 style='margin:0'>Reusable kit surface</h2><div style='display:flex;gap:10px'><button>Cancel</button><button class='primary'>Save</button></div><div class='wf-box' style='height:96px'>Dialog content</div><div class='wf-box' style='height:150px'>Popover or menu content</div></main></div>"
15
+ "<div style='min-height:460px;box-sizing:border-box;padding:26px;display:flex;flex-direction:column;gap:16px'><h2 style='margin:0'>UI primitives</h2><div style='display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px'><div class='wf-card' style='display:flex;flex-direction:column;gap:10px'><small class='wf-muted'>Buttons</small><div style='display:flex;gap:8px'><button>Cancel</button><button class='primary'>Save</button></div></div><div class='wf-card' style='display:flex;flex-direction:column;gap:10px'><small class='wf-muted'>Input</small><div class='wf-box' style='display:flex;align-items:center;gap:8px'><span data-icon='search'></span><span class='wf-muted'>Search...</span></div></div><div class='wf-card' style='display:flex;flex-direction:column;gap:10px'><small class='wf-muted'>Select</small><div class='wf-box' style='display:flex;align-items:center;gap:8px'><div style='flex:1'>Editor</div><span data-icon='chevronDown'></span></div></div><div class='wf-card' style='display:flex;flex-direction:column;gap:10px'><small class='wf-muted'>Badges</small><div style='display:flex;gap:6px'><span class='wf-pill accent'>Active</span><span class='wf-pill'>Draft</span></div></div><div class='wf-card' style='display:flex;flex-direction:column;gap:8px'><small class='wf-muted'>Dialog</small><strong>Delete file?</strong><small class='wf-muted'>This cannot be undone.</small><div style='display:flex;gap:8px;justify-content:flex-end'><button>Cancel</button><button class='primary'>Delete</button></div></div><div class='wf-card' style='display:flex;flex-direction:column;gap:6px'><small class='wf-muted'>Menu</small><div style='display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:8px;background:var(--wf-accent-soft)'><span data-icon='edit'></span> Rename</div><div style='display:flex;align-items:center;gap:8px;padding:6px 8px'><span data-icon='user'></span> Share</div></div></div></div>"
16
16
  }
17
17
  />
18
18
  </WireframeBlock>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.90.5",
3
+ "version": "0.90.8",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -22,6 +22,13 @@ interface QueryClient {
22
22
  const POLL_ABORT_MIN_MS = 10_000;
23
23
  const SSE_FALLBACK_INTERVAL_MS = 15_000;
24
24
  const POLL_AUTH_FAILURE_COOLDOWN_MS = 60_000;
25
+ /**
26
+ * Max cadence for SSE/poll-driven query invalidation in `useDbSync`. Events
27
+ * that arrive within this window of the first one in a burst are merged into
28
+ * a single `invalidateForEvents` call instead of one call per event — see the
29
+ * `queueInvalidateBatch` comment at the call site.
30
+ */
31
+ const INVALIDATE_COALESCE_MS = 250;
25
32
 
26
33
  class HttpStatusError extends Error {
27
34
  status: number;
@@ -94,6 +101,46 @@ function isAuthFailure(error: unknown): boolean {
94
101
  );
95
102
  }
96
103
 
104
+ /**
105
+ * App-state keys that drive immediate UI navigation/interaction and must
106
+ * never sit behind the invalidation coalesce window (see
107
+ * `isInteractionCriticalSyncEvent`).
108
+ */
109
+ const INTERACTION_CRITICAL_APP_STATE_KEYS = [
110
+ "navigate",
111
+ "show-questions",
112
+ "__set_url__",
113
+ ];
114
+
115
+ /**
116
+ * True for sync events that drive immediate, agent-initiated UI navigation
117
+ * or interaction rather than passive data invalidation — app-state writes in
118
+ * general (they back `["app-state"]` queries directly), and specifically the
119
+ * `navigate` / `show-questions` / `__set_url__` app-state keys that
120
+ * `invalidateForEvents` special-cases into their own query keys below.
121
+ *
122
+ * `useDbSync` batches ordinary invalidation-driving events (action/collab/db
123
+ * change events) into one flush per `INVALIDATE_COALESCE_MS` so a chatty doc
124
+ * doesn't refetch on every keystroke. That trade-off is wrong for these
125
+ * events: agent-driven navigation, `set-url`, and guided-questions prompts
126
+ * must land as close to instantly as possible, so any batch containing one
127
+ * of these bypasses the coalesce window and flushes immediately instead.
128
+ *
129
+ * Exported as a small pure predicate so this classification is unit-testable
130
+ * independent of the transport/timer plumbing around it.
131
+ */
132
+ export function isInteractionCriticalSyncEvent(event: SyncEvent): boolean {
133
+ return (
134
+ event.source === "app-state" &&
135
+ (event.key === "*" ||
136
+ INTERACTION_CRITICAL_APP_STATE_KEYS.some(
137
+ (key) =>
138
+ event.key === key ||
139
+ (typeof event.key === "string" && event.key.startsWith(`${key}:`)),
140
+ ))
141
+ );
142
+ }
143
+
97
144
  async function fetchPollJson<T>(
98
145
  pollUrl: string,
99
146
  since: number,
@@ -615,6 +662,51 @@ export function useDbSync(
615
662
  // processed by THIS subscriber so stale poll re-deliveries are ignored.
616
663
  let subscriberVersion = 0;
617
664
 
665
+ // Coalesce bursts of SSE-driven invalidation into at most one flush per
666
+ // INVALIDATE_COALESCE_MS. A chatty doc (many small agent edits, several
667
+ // peers editing at once) can otherwise deliver a handful of `action`/
668
+ // `collab` events within a few hundred ms, each independently calling
669
+ // `queryClient.invalidateQueries` and firing `onEvent` — every one of
670
+ // those touches whatever query subscribers are mounted (e.g. a
671
+ // full-page editor) even though the end state only needs to be
672
+ // refreshed once. Version bookkeeping stays synchronous (below) so
673
+ // freshness filtering for the NEXT batch is unaffected by the delay.
674
+ //
675
+ // This coalesce window is wrong for interaction-critical events (agent
676
+ // navigation, `set-url`, guided questions — see
677
+ // `isInteractionCriticalSyncEvent`): those must reach the UI immediately,
678
+ // not up to INVALIDATE_COALESCE_MS late. So a fresh batch containing one
679
+ // of those flushes synchronously (queued + new events together,
680
+ // canceling any pending timer) instead of joining the coalesce window.
681
+ // Pure invalidation bursts with no interaction-critical members keep the
682
+ // coalesced behavior.
683
+ let pendingInvalidateEvents: SyncEvent[] = [];
684
+ let invalidateTimer: ReturnType<typeof setTimeout> | null = null;
685
+
686
+ function flushInvalidateBatch() {
687
+ if (invalidateTimer) {
688
+ clearTimeout(invalidateTimer);
689
+ invalidateTimer = null;
690
+ }
691
+ if (pendingInvalidateEvents.length === 0) return;
692
+ const batch = pendingInvalidateEvents;
693
+ pendingInvalidateEvents = [];
694
+ invalidateForEvents(batch);
695
+ }
696
+
697
+ function queueInvalidateBatch(events: SyncEvent[]) {
698
+ pendingInvalidateEvents.push(...events);
699
+ if (events.some(isInteractionCriticalSyncEvent)) {
700
+ flushInvalidateBatch();
701
+ return;
702
+ }
703
+ if (invalidateTimer) return;
704
+ invalidateTimer = setTimeout(
705
+ flushInvalidateBatch,
706
+ INVALIDATE_COALESCE_MS,
707
+ );
708
+ }
709
+
618
710
  function hasAppStateEvent(events: SyncEvent[], key: string): boolean {
619
711
  return events.some(
620
712
  (event) =>
@@ -722,7 +814,7 @@ export function useDbSync(
722
814
  });
723
815
 
724
816
  if (freshEvents.length > 0) {
725
- invalidateForEvents(freshEvents);
817
+ queueInvalidateBatch(freshEvents);
726
818
  }
727
819
 
728
820
  const maxEventVersion = freshEvents.reduce(
@@ -746,6 +838,12 @@ export function useDbSync(
746
838
  });
747
839
 
748
840
  return () => {
841
+ if (invalidateTimer) {
842
+ clearTimeout(invalidateTimer);
843
+ // Flush synchronously on unmount so a pending batch isn't silently
844
+ // dropped (e.g. a route change right after an agent edit lands).
845
+ flushInvalidateBatch();
846
+ }
749
847
  transport.remove(id);
750
848
  // If the registry still holds this transport, and the transport is now
751
849
  // empty, evict it so the next mount gets a fresh instance rather than a
@@ -94,6 +94,31 @@ function isDocumentHidden(): boolean {
94
94
  );
95
95
  }
96
96
 
97
+ /**
98
+ * Content equality for deduped active-user lists. `dedupeCollabUsersByEmail`
99
+ * always allocates a fresh array (and fresh per-user objects), so callers
100
+ * that only want to know "did the actual user set change" need this instead
101
+ * of an identity check. Order-sensitive: the dedupe helper iterates awareness
102
+ * states in a stable Map insertion order, so a reordering here does reflect
103
+ * a real change (a client joined/left and re-joined).
104
+ */
105
+ function collabUsersEqual(a: CollabUser[], b: CollabUser[]): boolean {
106
+ if (a === b) return true;
107
+ if (a.length !== b.length) return false;
108
+ for (let i = 0; i < a.length; i++) {
109
+ const left = a[i]!;
110
+ const right = b[i]!;
111
+ if (
112
+ left.email !== right.email ||
113
+ left.name !== right.name ||
114
+ left.color !== right.color
115
+ ) {
116
+ return false;
117
+ }
118
+ }
119
+ return true;
120
+ }
121
+
97
122
  /**
98
123
  * Leader election for applying authoritative external snapshots into a shared
99
124
  * collaborative document.
@@ -566,10 +591,30 @@ class CollabDocConnection {
566
591
  }
567
592
  }
568
593
  });
569
- this.setSnapshot({
570
- activeUsers: dedupeCollabUsersByEmail(users),
571
- agentPresent: hasAgent,
572
- });
594
+ const nextActiveUsers = dedupeCollabUsersByEmail(users);
595
+ // Awareness "change" fires for every remote broadcast — including cursor
596
+ // jiggles, re-published-but-unchanged heartbeat state, and edits to
597
+ // presence fields (recentEdits, selection) that don't affect who's
598
+ // active. dedupeCollabUsersByEmail always returns a fresh array, so
599
+ // without this comparison every one of those events would hand
600
+ // `activeUsers` a new identity and force every consumer (e.g. a
601
+ // full-page editor keying a useMemo/effect off it) to re-render even
602
+ // though the actual user list is unchanged. Reuse the previous
603
+ // reference when the deduped content is the same, matching the
604
+ // stable-ref discipline `usePresence`'s shallowEqualOthers already
605
+ // applies to `others`.
606
+ const activeUsers = collabUsersEqual(
607
+ this.snapshot.activeUsers,
608
+ nextActiveUsers,
609
+ )
610
+ ? this.snapshot.activeUsers
611
+ : nextActiveUsers;
612
+ if (
613
+ activeUsers !== this.snapshot.activeUsers ||
614
+ hasAgent !== this.snapshot.agentPresent
615
+ ) {
616
+ this.setSnapshot({ activeUsers, agentPresent: hasAgent });
617
+ }
573
618
 
574
619
  // Fast awareness push: whenever awareness changes (e.g. cursor moves,
575
620
  // setPresence() calls), schedule a throttled POST so peers receive updates
@@ -2427,6 +2427,157 @@ export const config = {
2427
2427
  );
2428
2428
  }
2429
2429
 
2430
+ export function assertSingleTemplateNetlifyBuildOutput(
2431
+ projectCwd: string,
2432
+ ): void {
2433
+ const failures: string[] = [];
2434
+ const publishDir = path.join(projectCwd, "dist");
2435
+ const redirectsPath = path.join(publishDir, "_redirects");
2436
+ const internalDir = path.join(projectCwd, ".netlify", "functions-internal");
2437
+ const serverDir = path.join(internalDir, "server");
2438
+ const serverEntryPath = path.join(serverDir, "server.mjs");
2439
+ const serverMainPath = path.join(serverDir, "main.mjs");
2440
+
2441
+ if (!fs.existsSync(publishDir)) {
2442
+ failures.push("missing publish directory: dist");
2443
+ } else if (!fs.existsSync(redirectsPath)) {
2444
+ failures.push("missing Netlify fallback redirects file: dist/_redirects");
2445
+ } else {
2446
+ const redirects = fs.readFileSync(redirectsPath, "utf-8");
2447
+ if (
2448
+ !redirects
2449
+ .split(/\r?\n/)
2450
+ .some(
2451
+ (line) =>
2452
+ line.trim().replace(/\s+/g, " ") ===
2453
+ "/* /.netlify/functions/server 200",
2454
+ )
2455
+ ) {
2456
+ failures.push(
2457
+ 'dist/_redirects is missing "/* /.netlify/functions/server 200"',
2458
+ );
2459
+ }
2460
+ }
2461
+
2462
+ if (!fs.existsSync(serverDir)) {
2463
+ failures.push(
2464
+ "missing scanned Netlify server function: .netlify/functions-internal/server",
2465
+ );
2466
+ }
2467
+
2468
+ if (!fs.existsSync(serverMainPath)) {
2469
+ failures.push(
2470
+ "missing Netlify server bundle: .netlify/functions-internal/server/main.mjs",
2471
+ );
2472
+ }
2473
+
2474
+ if (!fs.existsSync(serverEntryPath)) {
2475
+ failures.push(
2476
+ "missing Netlify server entry: .netlify/functions-internal/server/server.mjs",
2477
+ );
2478
+ } else {
2479
+ const serverEntry = fs.readFileSync(serverEntryPath, "utf-8");
2480
+ if (!/\bpath\s*:\s*["']\/\*["']/.test(serverEntry)) {
2481
+ failures.push(
2482
+ 'Netlify server entry is missing the "/*" catch-all function path',
2483
+ );
2484
+ }
2485
+ if (!serverEntry.includes('"/.netlify/*"')) {
2486
+ failures.push(
2487
+ 'Netlify server catch-all is missing the "/.netlify/*" exclusion',
2488
+ );
2489
+ }
2490
+ if (!serverEntry.includes("./main.mjs")) {
2491
+ failures.push(
2492
+ "Netlify server entry does not reference the generated main.mjs bundle",
2493
+ );
2494
+ }
2495
+ }
2496
+
2497
+ if (isDurableBackgroundDeployEnabled()) {
2498
+ const backgroundDir = path.join(
2499
+ internalDir,
2500
+ AGENT_BACKGROUND_FUNCTION_NAME,
2501
+ );
2502
+ const backgroundEntryPath = path.join(
2503
+ backgroundDir,
2504
+ `${AGENT_BACKGROUND_FUNCTION_NAME}.mjs`,
2505
+ );
2506
+ if (!fs.existsSync(backgroundEntryPath)) {
2507
+ failures.push(
2508
+ `durable background is enabled but ${path.relative(
2509
+ projectCwd,
2510
+ backgroundEntryPath,
2511
+ )} was not emitted`,
2512
+ );
2513
+ } else {
2514
+ const backgroundEntry = fs.readFileSync(backgroundEntryPath, "utf-8");
2515
+ if (!/\bbackground\s*:\s*true\b/.test(backgroundEntry)) {
2516
+ failures.push(
2517
+ `durable background entry ${path.relative(
2518
+ projectCwd,
2519
+ backgroundEntryPath,
2520
+ )} is missing background: true`,
2521
+ );
2522
+ }
2523
+ if (/^\s*path\s*:/m.test(backgroundEntry)) {
2524
+ failures.push(
2525
+ `durable background entry ${path.relative(
2526
+ projectCwd,
2527
+ backgroundEntryPath,
2528
+ )} must not declare a custom path`,
2529
+ );
2530
+ }
2531
+ }
2532
+ }
2533
+
2534
+ if (failures.length > 0) {
2535
+ throw new Error(
2536
+ "[deploy] Netlify deploy guard failed; refusing to publish an output " +
2537
+ "that would likely serve Netlify 404s:\n" +
2538
+ failures.map((failure) => `- ${failure}`).join("\n"),
2539
+ );
2540
+ }
2541
+
2542
+ console.log(
2543
+ "[deploy] Netlify deploy guard passed: publish dir and catch-all server function are present.",
2544
+ );
2545
+ }
2546
+
2547
+ export function writeSingleTemplateNetlifyRedirects(projectCwd: string): void {
2548
+ const publishDir = path.join(projectCwd, "dist");
2549
+ if (!fs.existsSync(publishDir)) return;
2550
+
2551
+ const redirectsPath = path.join(publishDir, "_redirects");
2552
+ const existing = fs.existsSync(redirectsPath)
2553
+ ? fs.readFileSync(redirectsPath, "utf-8").trimEnd()
2554
+ : "";
2555
+ const lines = existing ? existing.split(/\r?\n/) : [];
2556
+
2557
+ function hasRedirect(line: string): boolean {
2558
+ return lines.some(
2559
+ (existingLine) => existingLine.trim().replace(/\s+/g, " ") === line,
2560
+ );
2561
+ }
2562
+
2563
+ const fallbackRules = [
2564
+ "# Generated by agent-native build for Netlify single-template deploys",
2565
+ "# Static files are served first; dynamic routes fall through to the server function.",
2566
+ "/* /.netlify/functions/server 200",
2567
+ ];
2568
+
2569
+ for (const line of fallbackRules) {
2570
+ if (line.startsWith("#") || !hasRedirect(line)) {
2571
+ lines.push(line);
2572
+ }
2573
+ }
2574
+
2575
+ fs.writeFileSync(redirectsPath, lines.join("\n") + "\n");
2576
+ console.log(
2577
+ "[deploy] Wrote Netlify fallback redirect /* -> /.netlify/functions/server.",
2578
+ );
2579
+ }
2580
+
2430
2581
  function copyInstalledLibsqlNativePackages(serverDir: string | undefined) {
2431
2582
  if (!serverDir || !fs.existsSync(serverDir)) return;
2432
2583
  const nodeModulesRoots = nodeModulesAncestors(cwd);
@@ -2976,6 +3127,11 @@ export default bundle;
2976
3127
  }
2977
3128
  }
2978
3129
 
3130
+ if (preset === "netlify") {
3131
+ writeSingleTemplateNetlifyRedirects(cwd);
3132
+ assertSingleTemplateNetlifyBuildOutput(cwd);
3133
+ }
3134
+
2979
3135
  // Resolve remaining bare npm imports by bundling them into _libs/.
2980
3136
  // Nitro sometimes leaves small packages as externals even with noExternals.
2981
3137
  if (preset.startsWith("cloudflare") || preset.startsWith("deno")) {