@agent-native/core 0.115.4 → 0.117.0
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/agent-native.eject.json +1 -0
- package/corpus/README.md +3 -3
- package/corpus/core/CHANGELOG.md +43 -0
- package/corpus/core/agent-native.eject.json +1 -0
- package/corpus/core/docs/AGENTS.md +1 -1
- package/corpus/core/docs/content/agent-mentions.mdx +2 -2
- package/corpus/core/docs/content/{workspace.mdx → agent-resources.mdx} +1 -7
- package/corpus/core/docs/content/agent-teams.mdx +4 -4
- package/corpus/core/docs/content/dispatch.mdx +10 -10
- package/corpus/core/docs/content/drop-in-agent.mdx +4 -4
- package/corpus/core/docs/content/external-agents.mdx +1 -1
- package/corpus/core/docs/content/frames.mdx +1 -1
- package/corpus/core/docs/content/integrations.mdx +13 -2
- package/corpus/core/docs/content/key-concepts.mdx +2 -2
- package/corpus/core/docs/content/mcp-clients.mdx +7 -1
- package/corpus/core/docs/content/multi-app-workspace.mdx +3 -3
- package/corpus/core/docs/content/real-time-collaboration.mdx +33 -15
- package/corpus/core/docs/content/recurring-jobs.mdx +7 -7
- package/corpus/core/docs/content/skills-guide.mdx +4 -4
- package/corpus/core/docs/content/template-brain.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
- package/corpus/core/docs/content/using-your-agent.mdx +1 -1
- package/corpus/core/docs/content/what-is-agent-native.mdx +4 -4
- package/corpus/core/docs/content/workspace-management.mdx +4 -4
- package/corpus/core/package.json +4 -1
- package/corpus/core/src/agent/production-agent.ts +13 -3
- package/corpus/core/src/cli/doctor.ts +6 -1
- package/corpus/core/src/client/AgentPanel.tsx +79 -41
- package/corpus/core/src/client/AssistantChat.tsx +71 -25
- package/corpus/core/src/client/FeedbackButton.tsx +33 -7
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +6 -8
- package/corpus/core/src/client/agent-chat-adapter.ts +11 -2
- package/corpus/core/src/client/agent-page/AgentTabFrame.tsx +22 -3
- package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +34 -19
- package/corpus/core/src/client/analytics.ts +7 -0
- package/corpus/core/src/client/chat/message-components.tsx +43 -16
- package/corpus/core/src/client/chat/tool-call-display.tsx +30 -18
- package/corpus/core/src/client/chat/tool-render-registry.tsx +2 -0
- package/corpus/core/src/client/resources/McpConnectionSuggestion.tsx +19 -2
- package/corpus/core/src/client/resources/ResourceTree.tsx +7 -10
- package/corpus/core/src/client/resources/ResourcesPanel.tsx +33 -70
- package/corpus/core/src/client/resources/mcp-connection-resume.ts +107 -0
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +65 -12
- package/corpus/core/src/client/resources/mcp-integration-logos.ts +4 -0
- package/corpus/core/src/client/sse-event-processor.ts +50 -14
- package/corpus/core/src/client/use-db-sync.ts +370 -21
- package/corpus/core/src/collab/client.ts +26 -2
- package/corpus/core/src/connections/catalog.ts +7 -1
- package/corpus/core/src/db/ddl-guard.ts +49 -10
- package/corpus/core/src/deploy/build.ts +25 -2
- package/corpus/core/src/eject/remote-mcp-presets.ts +37 -5
- package/corpus/core/src/guards/explicit-collab-access.ts +229 -0
- package/corpus/core/src/guards/index.ts +1 -0
- package/corpus/core/src/localization/default-messages.ts +54 -5
- package/corpus/core/src/mcp/connect-route.ts +34 -51
- package/corpus/core/src/provider-api/index.ts +1 -1
- package/corpus/core/src/realtime-protocol.ts +92 -0
- package/corpus/core/src/server/collab-plugin.ts +137 -18
- package/corpus/core/src/server/core-routes-plugin.ts +7 -0
- package/corpus/core/src/server/index.ts +2 -0
- package/corpus/core/src/server/poll-events.ts +29 -8
- package/corpus/core/src/server/poll.ts +1123 -865
- package/corpus/core/src/server/realtime-token.ts +106 -0
- package/corpus/core/src/server/sentry-config.ts +40 -0
- package/corpus/core/src/server/short-lived-token.ts +156 -0
- package/corpus/core/src/server/sse.ts +15 -0
- package/corpus/core/src/server/ssr-handler.ts +9 -3
- package/corpus/core/src/styles/agent-native.css +40 -0
- package/corpus/core/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/core/src/templates/default/.agents/skills/real-time-collab/SKILL.md +35 -13
- package/corpus/core/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/core/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +35 -13
- package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +12 -8
- package/corpus/templates/analytics/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/analytics/AGENTS.md +7 -2
- package/corpus/templates/analytics/actions/export-dashboard-panel-to-google-sheet.ts +144 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -88
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +34 -37
- package/corpus/templates/analytics/app/global.css +0 -4
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +2 -7
- package/corpus/templates/analytics/app/i18n-data.ts +107 -0
- package/corpus/templates/analytics/app/lib/data-source-status.ts +9 -0
- package/corpus/templates/analytics/app/lib/sql-query.ts +1 -2
- package/corpus/templates/analytics/app/pages/Ask.tsx +2 -7
- package/corpus/templates/analytics/app/pages/DataSources.tsx +48 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +59 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +3 -1
- package/corpus/templates/analytics/changelog/2026-07-22-clear-background-chat-progress.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-22-daily-dashboard-email-query-batches.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-22-export-table-panels-directly-to-google-sheets.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-22-recent-chats-can-expand-in-the-sidebar.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +8 -8
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +31 -0
- package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +3 -5
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +12 -0
- package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +3 -5
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +150 -21
- package/corpus/templates/analytics/server/lib/google-sheets-export.ts +188 -0
- package/corpus/templates/analytics/server/lib/provider-api.ts +7 -2
- package/corpus/templates/analytics/server/plugins/collab.ts +6 -3
- package/corpus/templates/analytics/shared/sql-query-limits.ts +1 -0
- package/corpus/templates/assets/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/assets/.agents/skills/real-time-collab/SKILL.md +35 -13
- package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/assets/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +54 -62
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +2 -6
- package/corpus/templates/assets/app/routes/_index.tsx +8 -5
- package/corpus/templates/assets/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-22-recent-chats-expand-from-a-compact-list.md +6 -0
- package/corpus/templates/brain/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/brain/actions/rotate-source-ingest-token.ts +77 -0
- package/corpus/templates/brain/app/components/layout/Layout.tsx +5 -1
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +38 -51
- package/corpus/templates/brain/app/hooks/use-navigation-state.ts +2 -9
- package/corpus/templates/brain/app/i18n-data.ts +120 -0
- package/corpus/templates/brain/app/routes/sources.tsx +156 -5
- package/corpus/templates/brain/changelog/2026-07-22-chat-runs-always-end-with-an-answer.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-22-clear-background-chat-progress.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-22-long-chat-titles-stay-inside-the-sidebar.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-22-new-clips-and-webhook-sources-now-show-their-secure-connecti.md +6 -0
- package/corpus/templates/brain/jobs/emit-netlify-brain-queue-cron.ts +125 -0
- package/corpus/templates/brain/netlify.toml +5 -1
- package/corpus/templates/brain/scripts/configure-prod-clips-source.ts +50 -0
- package/corpus/templates/brain/server/lib/capture-sanitization.ts +64 -6
- package/corpus/templates/brain/server/plugins/agent-chat.ts +4 -0
- package/corpus/templates/brain/server/plugins/brain-jobs.ts +1 -0
- package/corpus/templates/brain/server/routes/api/brain/queue/run.post.ts +55 -0
- package/corpus/templates/calendar/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +43 -67
- package/corpus/templates/chat/app/hooks/use-navigation-state.ts +2 -7
- package/corpus/templates/chat/app/routes/_index.tsx +8 -5
- package/corpus/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
- package/corpus/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
- package/corpus/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +6 -0
- package/corpus/templates/clips/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/clips/AGENTS.md +9 -1
- package/corpus/templates/clips/actions/export-to-brain.ts +82 -6
- package/corpus/templates/clips/actions/request-transcript.ts +25 -14
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +12 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +1 -0
- package/corpus/templates/clips/changelog/2026-07-22-clips-transcripts-now-retry-brain-export.md +5 -0
- package/corpus/templates/clips/changelog/2026-07-22-german-umlauts-and-other-non-ascii-characters-now-paste-corr.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +26 -3
- package/corpus/templates/clips/jobs/emit-netlify-brain-export-cron.ts +118 -0
- package/corpus/templates/clips/netlify.toml +4 -1
- package/corpus/templates/clips/scripts/configure-prod-brain-export.ts +92 -0
- package/corpus/templates/clips/server/jobs/brain-export.ts +182 -0
- package/corpus/templates/clips/server/lib/brain-export-state.ts +42 -0
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +5 -1
- package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
- package/corpus/templates/clips/server/register-secrets.ts +42 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +10 -1
- package/corpus/templates/clips/server/routes/api/clips/brain-export/run.post.ts +45 -0
- package/corpus/templates/content/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +566 -299
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +14 -0
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -1
- package/corpus/templates/content/app/i18n-data.ts +90 -0
- package/corpus/templates/content/server/plugins/collab.ts +1 -1
- package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
- package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +2 -0
- package/corpus/templates/crm/actions/run.ts +2 -6
- package/corpus/templates/crm/actions/update-crm-record.ts +4 -2
- package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +6 -0
- package/corpus/templates/crm/changelog/2026-07-22-crm-field-validation-now-explains-which-fields-cannot-be-edi.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-22-fixed-hubspot-contact-syncs-missing-records-updated-after-th.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-22-fixed-pipeline-dashboard-panels-so-their-opportunity-data-lo.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
- package/corpus/templates/crm/server/crm/hubspot-adapter.ts +4 -1
- package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/design/server/plugins/collab.ts +11 -8
- package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/forms/app/components/layout/Layout.tsx +1 -0
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/forms/app/hooks/use-navigation-state.ts +2 -9
- package/corpus/templates/forms/app/root.tsx +1 -6
- package/corpus/templates/forms/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
- package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +2 -2
- package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/plan/app/components/layout/Layout.tsx +5 -1
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +57 -72
- package/corpus/templates/plan/app/hooks/use-navigation-state.ts +2 -9
- package/corpus/templates/plan/app/lib/create-plan-routing.ts +2 -2
- package/corpus/templates/plan/app/pages/PlanChatPage.tsx +0 -1
- package/corpus/templates/plan/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-22-recent-chats-and-sidebar-controls-are-more-compact.md +6 -0
- package/corpus/templates/plan/server/plugins/collab.ts +5 -2
- package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/slides/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/slides/server/plugins/collab.ts +11 -8
- package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +5 -22
- package/corpus/templates/tasks/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +7 -0
- package/corpus/toolkit/README.md +12 -3
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/chat-history/ChatHistoryRail.tsx +89 -0
- package/corpus/toolkit/src/chat-history/index.ts +5 -0
- package/corpus/toolkit/src/chat-history.css +59 -0
- package/corpus/toolkit/src/composer/RealtimeVoiceMode.tsx +106 -36
- package/corpus/toolkit/src/composer/VoiceButton.tsx +49 -0
- package/corpus/toolkit/src/composer/realtime-voice-audio-level.ts +2 -1
- package/corpus/toolkit/src/composer/useRealtimeVoiceMode.tsx +5 -3
- package/corpus/toolkit/src/styles.css +35 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +13 -3
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/doctor.d.ts +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +5 -2
- package/dist/cli/doctor.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +1 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +38 -18
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +55 -20
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/FeedbackButton.d.ts +4 -2
- package/dist/client/FeedbackButton.d.ts.map +1 -1
- package/dist/client/FeedbackButton.js +18 -2
- package/dist/client/FeedbackButton.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +2 -3
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +11 -3
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/agent-page/AgentTabFrame.d.ts +3 -1
- package/dist/client/agent-page/AgentTabFrame.d.ts.map +1 -1
- package/dist/client/agent-page/AgentTabFrame.js +3 -2
- package/dist/client/agent-page/AgentTabFrame.js.map +1 -1
- package/dist/client/agent-page/AgentTabsPage.d.ts +2 -0
- package/dist/client/agent-page/AgentTabsPage.d.ts.map +1 -1
- package/dist/client/agent-page/AgentTabsPage.js +26 -15
- package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
- package/dist/client/analytics.d.ts +10 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/chat/message-components.d.ts +6 -1
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +17 -11
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +11 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +16 -17
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/chat/tool-render-registry.d.ts +2 -0
- package/dist/client/chat/tool-render-registry.d.ts.map +1 -1
- package/dist/client/chat/tool-render-registry.js.map +1 -1
- package/dist/client/resources/McpConnectionSuggestion.d.ts.map +1 -1
- package/dist/client/resources/McpConnectionSuggestion.js +15 -1
- package/dist/client/resources/McpConnectionSuggestion.js.map +1 -1
- package/dist/client/resources/ResourceTree.d.ts.map +1 -1
- package/dist/client/resources/ResourceTree.js +3 -3
- package/dist/client/resources/ResourceTree.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts +1 -0
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +34 -20
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/client/resources/mcp-connection-resume.d.ts +11 -0
- package/dist/client/resources/mcp-connection-resume.d.ts.map +1 -0
- package/dist/client/resources/mcp-connection-resume.js +95 -0
- package/dist/client/resources/mcp-connection-resume.js.map +1 -0
- package/dist/client/resources/mcp-integration-catalog.d.ts +1 -0
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +63 -8
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/resources/mcp-integration-logos.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-logos.js +4 -0
- package/dist/client/resources/mcp-integration-logos.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +5 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +39 -13
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-db-sync.d.ts +4 -3
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +332 -21
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +26 -2
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/connections/catalog.d.ts +1 -1
- package/dist/connections/catalog.d.ts.map +1 -1
- package/dist/connections/catalog.js +7 -1
- package/dist/connections/catalog.js.map +1 -1
- package/dist/db/ddl-guard.d.ts +7 -2
- package/dist/db/ddl-guard.d.ts.map +1 -1
- package/dist/db/ddl-guard.js +21 -9
- package/dist/db/ddl-guard.js.map +1 -1
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +25 -2
- package/dist/deploy/build.js.map +1 -1
- package/dist/eject/remote-mcp-presets.d.ts.map +1 -1
- package/dist/eject/remote-mcp-presets.js +33 -3
- package/dist/eject/remote-mcp-presets.js.map +1 -1
- package/dist/guards/explicit-collab-access.d.ts +8 -0
- package/dist/guards/explicit-collab-access.d.ts.map +1 -0
- package/dist/guards/explicit-collab-access.js +214 -0
- package/dist/guards/explicit-collab-access.js.map +1 -0
- package/dist/guards/index.d.ts +1 -0
- package/dist/guards/index.d.ts.map +1 -1
- package/dist/guards/index.js +1 -0
- package/dist/guards/index.js.map +1 -1
- package/dist/localization/default-messages.d.ts +43 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +48 -5
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp/connect-route.d.ts.map +1 -1
- package/dist/mcp/connect-route.js +34 -48
- package/dist/mcp/connect-route.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.js +1 -1
- package/dist/provider-api/index.js.map +1 -1
- package/dist/realtime-protocol.d.ts +40 -0
- package/dist/realtime-protocol.d.ts.map +1 -0
- package/dist/realtime-protocol.js +69 -0
- package/dist/realtime-protocol.js.map +1 -0
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/collab-plugin.d.ts +34 -3
- package/dist/server/collab-plugin.d.ts.map +1 -1
- package/dist/server/collab-plugin.js +65 -12
- package/dist/server/collab-plugin.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +3 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/poll-events.d.ts +7 -1
- package/dist/server/poll-events.d.ts.map +1 -1
- package/dist/server/poll-events.js +24 -7
- package/dist/server/poll-events.js.map +1 -1
- package/dist/server/poll.d.ts +223 -42
- package/dist/server/poll.d.ts.map +1 -1
- package/dist/server/poll.js +849 -737
- package/dist/server/poll.js.map +1 -1
- package/dist/server/realtime-token.d.ts +34 -0
- package/dist/server/realtime-token.d.ts.map +1 -0
- package/dist/server/realtime-token.js +85 -0
- package/dist/server/realtime-token.js.map +1 -0
- package/dist/server/sentry-config.d.ts +12 -0
- package/dist/server/sentry-config.d.ts.map +1 -1
- package/dist/server/sentry-config.js +34 -0
- package/dist/server/sentry-config.js.map +1 -1
- package/dist/server/short-lived-token.d.ts +46 -0
- package/dist/server/short-lived-token.d.ts.map +1 -1
- package/dist/server/short-lived-token.js +104 -0
- package/dist/server/short-lived-token.js.map +1 -1
- package/dist/server/sse.d.ts +1 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +4 -0
- package/dist/server/sse.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +5 -3
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +40 -0
- package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/chat/app/components/layout/Layout.tsx +1 -0
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +43 -67
- package/dist/templates/chat/app/hooks/use-navigation-state.ts +2 -7
- package/dist/templates/chat/app/routes/_index.tsx +8 -5
- package/dist/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
- package/dist/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
- package/dist/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +6 -0
- package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +35 -13
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +35 -13
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +12 -8
- package/docs/AGENTS.md +1 -1
- package/docs/content/agent-mentions.mdx +2 -2
- package/docs/content/{workspace.mdx → agent-resources.mdx} +1 -7
- package/docs/content/agent-teams.mdx +4 -4
- package/docs/content/dispatch.mdx +10 -10
- package/docs/content/drop-in-agent.mdx +4 -4
- package/docs/content/external-agents.mdx +1 -1
- package/docs/content/frames.mdx +1 -1
- package/docs/content/integrations.mdx +13 -2
- package/docs/content/key-concepts.mdx +2 -2
- package/docs/content/mcp-clients.mdx +7 -1
- package/docs/content/multi-app-workspace.mdx +3 -3
- package/docs/content/real-time-collaboration.mdx +33 -15
- package/docs/content/recurring-jobs.mdx +7 -7
- package/docs/content/skills-guide.mdx +4 -4
- package/docs/content/template-brain.mdx +1 -1
- package/docs/content/toolkit-collaboration.mdx +1 -1
- package/docs/content/using-your-agent.mdx +1 -1
- package/docs/content/what-is-agent-native.mdx +4 -4
- package/docs/content/workspace-management.mdx +4 -4
- package/package.json +6 -3
- package/src/agent/production-agent.ts +13 -3
- package/src/cli/doctor.ts +6 -1
- package/src/client/AgentPanel.tsx +79 -41
- package/src/client/AssistantChat.tsx +71 -25
- package/src/client/FeedbackButton.tsx +33 -7
- package/src/client/MultiTabAssistantChat.tsx +6 -8
- package/src/client/agent-chat-adapter.ts +11 -2
- package/src/client/agent-page/AgentTabFrame.tsx +22 -3
- package/src/client/agent-page/AgentTabsPage.tsx +34 -19
- package/src/client/analytics.ts +7 -0
- package/src/client/chat/message-components.tsx +43 -16
- package/src/client/chat/tool-call-display.tsx +30 -18
- package/src/client/chat/tool-render-registry.tsx +2 -0
- package/src/client/resources/McpConnectionSuggestion.tsx +19 -2
- package/src/client/resources/ResourceTree.tsx +7 -10
- package/src/client/resources/ResourcesPanel.tsx +33 -70
- package/src/client/resources/mcp-connection-resume.ts +107 -0
- package/src/client/resources/mcp-integration-catalog.ts +65 -12
- package/src/client/resources/mcp-integration-logos.ts +4 -0
- package/src/client/sse-event-processor.ts +50 -14
- package/src/client/use-db-sync.ts +370 -21
- package/src/collab/client.ts +26 -2
- package/src/connections/catalog.ts +7 -1
- package/src/db/ddl-guard.ts +49 -10
- package/src/deploy/build.ts +25 -2
- package/src/eject/remote-mcp-presets.ts +37 -5
- package/src/guards/explicit-collab-access.ts +229 -0
- package/src/guards/index.ts +1 -0
- package/src/localization/default-messages.ts +54 -5
- package/src/mcp/connect-route.ts +34 -51
- package/src/provider-api/index.ts +1 -1
- package/src/realtime-protocol.ts +92 -0
- package/src/server/collab-plugin.ts +137 -18
- package/src/server/core-routes-plugin.ts +7 -0
- package/src/server/index.ts +2 -0
- package/src/server/poll-events.ts +29 -8
- package/src/server/poll.ts +1123 -865
- package/src/server/realtime-token.ts +106 -0
- package/src/server/sentry-config.ts +40 -0
- package/src/server/short-lived-token.ts +156 -0
- package/src/server/sse.ts +15 -0
- package/src/server/ssr-handler.ts +9 -3
- package/src/styles/agent-native.css +40 -0
- package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/chat/app/components/layout/Layout.tsx +1 -0
- package/src/templates/chat/app/components/layout/Sidebar.tsx +43 -67
- package/src/templates/chat/app/hooks/use-navigation-state.ts +2 -7
- package/src/templates/chat/app/routes/_index.tsx +8 -5
- package/src/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +6 -0
- package/src/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +6 -0
- package/src/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +6 -0
- package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +35 -13
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +4 -1
- package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +35 -13
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +12 -8
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import { ensureDemoModeFetchInterceptor } from "../demo/fetch-interceptor.js";
|
|
4
|
+
import {
|
|
5
|
+
parseHandshakeFrame,
|
|
6
|
+
parseTokenFrame,
|
|
7
|
+
REALTIME_PROTOCOL_VERSION,
|
|
8
|
+
REALTIME_SSE_HANDSHAKE_EVENT,
|
|
9
|
+
REALTIME_SSE_TOKEN_EVENT,
|
|
10
|
+
} from "../realtime-protocol.js";
|
|
4
11
|
import { agentNativePath } from "./api-path.js";
|
|
5
12
|
import { getBrowserTabId } from "./browser-tab-id.js";
|
|
6
13
|
import {
|
|
@@ -33,6 +40,7 @@ const POLL_ABORT_MIN_MS = 10_000;
|
|
|
33
40
|
// four times per minute forever. Focus and active agent work still poll now.
|
|
34
41
|
const SSE_FALLBACK_INTERVAL_MS = 60_000;
|
|
35
42
|
const IDLE_POLL_INTERVAL_MS = 60_000;
|
|
43
|
+
const HIDDEN_POLL_INTERVAL_MS = 10_000;
|
|
36
44
|
const POLL_AUTH_FAILURE_COOLDOWN_MS = 60_000;
|
|
37
45
|
/**
|
|
38
46
|
* Max cadence for SSE/poll-driven query invalidation in `useDbSync`. Events
|
|
@@ -87,6 +95,64 @@ function resolveSseUrl(sseUrl: string | false | undefined): string | false {
|
|
|
87
95
|
return agentNativePath(sseUrl ?? "/_agent-native/events");
|
|
88
96
|
}
|
|
89
97
|
|
|
98
|
+
// --- Hosted Realtime Gateway binding ----------------------------------------
|
|
99
|
+
//
|
|
100
|
+
// When the app is configured for the hosted gateway, the transport connects to
|
|
101
|
+
// the gateway (cross-origin) instead of the Netlify app, carrying a short-lived
|
|
102
|
+
// subscribe token minted from the app's own same-origin endpoint. All of this
|
|
103
|
+
// is gated on a non-null binding — apps without hosted config keep the exact
|
|
104
|
+
// local behavior below.
|
|
105
|
+
|
|
106
|
+
const REALTIME_GATEWAY_SSE_PATH = "/stream";
|
|
107
|
+
const REALTIME_GATEWAY_POLL_PATH = "/poll";
|
|
108
|
+
const REALTIME_TOKEN_MINT_PATH = "/_agent-native/realtime-token";
|
|
109
|
+
/** Consecutive gateway failures before health-gating back to the local app. */
|
|
110
|
+
const HOSTED_UNHEALTHY_THRESHOLD = 3;
|
|
111
|
+
|
|
112
|
+
interface RealtimeGatewayBinding {
|
|
113
|
+
/** Gateway SSE URL (token appended per connect). */
|
|
114
|
+
sseUrl: string;
|
|
115
|
+
/** Gateway poll URL (token appended per request). */
|
|
116
|
+
pollUrl: string;
|
|
117
|
+
/** Same-origin app endpoint that mints the subscribe token. */
|
|
118
|
+
tokenMintUrl: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getRealtimeConfig():
|
|
122
|
+
| { transport?: string; gatewayBaseUrl?: string }
|
|
123
|
+
| undefined {
|
|
124
|
+
if (typeof window === "undefined") return undefined;
|
|
125
|
+
return window.__AGENT_NATIVE_CONFIG__?.realtime;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Resolve the hosted-gateway binding, or null to stay on the local app. Gated
|
|
130
|
+
* on: SSE enabled (the gateway is push-first), not embed auth (needs the
|
|
131
|
+
* same-origin session to mint), and `transport: "hosted"` with a base URL in
|
|
132
|
+
* the impersonal SSR config.
|
|
133
|
+
*/
|
|
134
|
+
function resolveGatewayBinding(
|
|
135
|
+
localSseUrl: string | false,
|
|
136
|
+
): RealtimeGatewayBinding | null {
|
|
137
|
+
if (localSseUrl === false) return null;
|
|
138
|
+
if (isEmbedAuthActive()) return null;
|
|
139
|
+
const config = getRealtimeConfig();
|
|
140
|
+
if (config?.transport !== "hosted") return null;
|
|
141
|
+
const base = config.gatewayBaseUrl?.replace(/\/+$/, "");
|
|
142
|
+
if (!base) return null;
|
|
143
|
+
return {
|
|
144
|
+
sseUrl: `${base}${REALTIME_GATEWAY_SSE_PATH}`,
|
|
145
|
+
pollUrl: `${base}${REALTIME_GATEWAY_POLL_PATH}`,
|
|
146
|
+
tokenMintUrl: agentNativePath(REALTIME_TOKEN_MINT_PATH),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** ±20% jitter so gateway timeout/deploy-driven reconnects don't stampede. */
|
|
151
|
+
function applyReconnectJitter(delay: number): number {
|
|
152
|
+
const jitter = delay * 0.2 * (Math.random() * 2 - 1);
|
|
153
|
+
return Math.max(0, Math.round(delay + jitter));
|
|
154
|
+
}
|
|
155
|
+
|
|
90
156
|
function normalizeEventPayload(payload: unknown): SyncEvent[] {
|
|
91
157
|
if (!payload || typeof payload !== "object") return [];
|
|
92
158
|
const record = payload as { type?: unknown; events?: unknown };
|
|
@@ -157,6 +223,7 @@ async function fetchPollJson<T>(
|
|
|
157
223
|
pollUrl: string,
|
|
158
224
|
since: number,
|
|
159
225
|
interval: number,
|
|
226
|
+
token?: string,
|
|
160
227
|
): Promise<T> {
|
|
161
228
|
const controller =
|
|
162
229
|
typeof AbortController === "undefined" ? null : new AbortController();
|
|
@@ -164,9 +231,16 @@ async function fetchPollJson<T>(
|
|
|
164
231
|
? setTimeout(() => controller.abort(), getPollAbortMs(interval))
|
|
165
232
|
: null;
|
|
166
233
|
|
|
234
|
+
// Local path stays exactly `?since=N`; the hosted gateway also carries the
|
|
235
|
+
// subscribe token on the query string (a cross-origin fetch can't set the
|
|
236
|
+
// Authorization header for the SSE sibling either, so both use the query).
|
|
237
|
+
const url = token
|
|
238
|
+
? `${pollUrl}${pollUrl.includes("?") ? "&" : "?"}since=${since}&token=${encodeURIComponent(token)}`
|
|
239
|
+
: `${pollUrl}?since=${since}`;
|
|
240
|
+
|
|
167
241
|
try {
|
|
168
242
|
const res = await fetch(
|
|
169
|
-
|
|
243
|
+
url,
|
|
170
244
|
controller ? { signal: controller.signal } : undefined,
|
|
171
245
|
);
|
|
172
246
|
if (!res.ok) throw new HttpStatusError(res.status);
|
|
@@ -214,7 +288,10 @@ interface TransportSubscription {
|
|
|
214
288
|
* subscribers with their own fallback loops (e.g. the collab doc poll)
|
|
215
289
|
* relax their cadence while the push path is healthy.
|
|
216
290
|
*/
|
|
217
|
-
onSseStateChange?: (
|
|
291
|
+
onSseStateChange?: (
|
|
292
|
+
connected: boolean,
|
|
293
|
+
capabilities?: readonly string[],
|
|
294
|
+
) => void;
|
|
218
295
|
}
|
|
219
296
|
|
|
220
297
|
class SyncTransport {
|
|
@@ -228,11 +305,148 @@ class SyncTransport {
|
|
|
228
305
|
private authFailureUntil = 0;
|
|
229
306
|
private consecutiveFailures = 0;
|
|
230
307
|
private activeChatIds = new Set<string>();
|
|
308
|
+
// Hosted-gateway state. `mode` starts "hosted" when a binding is present and
|
|
309
|
+
// flips to "local" on health-gate revert; `token` is the current subscribe
|
|
310
|
+
// token (minted from the app, rotated over the stream), never part of any
|
|
311
|
+
// registry key.
|
|
312
|
+
private mode: "hosted" | "local";
|
|
313
|
+
private token: string | null = null;
|
|
314
|
+
private tokenMintInFlight: Promise<boolean> | null = null;
|
|
315
|
+
private gatewayReconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
316
|
+
private capabilities: string[] = [];
|
|
231
317
|
|
|
232
318
|
constructor(
|
|
233
319
|
private readonly pollUrl: string,
|
|
234
320
|
private readonly sseUrl: string | false,
|
|
235
|
-
|
|
321
|
+
private readonly gateway: RealtimeGatewayBinding | null = null,
|
|
322
|
+
) {
|
|
323
|
+
this.mode = gateway ? "hosted" : "local";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Capabilities advertised by the gateway handshake (e.g. `no-awareness`). */
|
|
327
|
+
getCapabilities(): readonly string[] {
|
|
328
|
+
return this.capabilities;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private get activeSseUrl(): string | false {
|
|
332
|
+
if (this.mode === "hosted" && this.gateway) {
|
|
333
|
+
return this.token
|
|
334
|
+
? `${this.gateway.sseUrl}?token=${encodeURIComponent(this.token)}`
|
|
335
|
+
: this.gateway.sseUrl;
|
|
336
|
+
}
|
|
337
|
+
return this.sseUrl;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private get activePollUrl(): string {
|
|
341
|
+
return this.mode === "hosted" && this.gateway
|
|
342
|
+
? this.gateway.pollUrl
|
|
343
|
+
: this.pollUrl;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Mint a subscribe token from the app's same-origin endpoint.
|
|
348
|
+
*
|
|
349
|
+
* Only TERMINAL outcomes health-gate to local: 404 (gateway not provisioned)
|
|
350
|
+
* and 401/403 (not authorized) — retrying those for this tab is pointless.
|
|
351
|
+
* TRANSIENT failures (5xx/429 from a cold Netlify function, network errors)
|
|
352
|
+
* keep the hosted intent and ride the jittered reconnect + unhealthy-threshold
|
|
353
|
+
* path, so a deploy / scale-to-zero blip doesn't permanently abandon the
|
|
354
|
+
* gateway for the tab.
|
|
355
|
+
*/
|
|
356
|
+
private mintToken(): Promise<boolean> {
|
|
357
|
+
if (!this.gateway || this.mode !== "hosted") return Promise.resolve(false);
|
|
358
|
+
if (this.tokenMintInFlight) return this.tokenMintInFlight;
|
|
359
|
+
const mintUrl = this.gateway.tokenMintUrl;
|
|
360
|
+
this.tokenMintInFlight = (async () => {
|
|
361
|
+
// Bound the mint like fetchPollJson bounds polls: a black-holed request
|
|
362
|
+
// must resolve as a transient failure, not hang tokenMintInFlight forever
|
|
363
|
+
// (poll() awaits this while holding inFlight, so a hung mint would stall
|
|
364
|
+
// the whole transport with no timer pending).
|
|
365
|
+
const controller =
|
|
366
|
+
typeof AbortController === "undefined" ? null : new AbortController();
|
|
367
|
+
const timeout = controller
|
|
368
|
+
? setTimeout(() => controller.abort(), POLL_ABORT_MIN_MS)
|
|
369
|
+
: null;
|
|
370
|
+
try {
|
|
371
|
+
const res = await fetch(mintUrl, {
|
|
372
|
+
credentials: "same-origin",
|
|
373
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
374
|
+
});
|
|
375
|
+
if (res.ok) {
|
|
376
|
+
const data = (await res.json()) as { token?: unknown };
|
|
377
|
+
if (typeof data?.token === "string" && data.token) {
|
|
378
|
+
this.token = data.token;
|
|
379
|
+
// Deliberately NOT resetting consecutiveFailures here: minting
|
|
380
|
+
// succeeds via the app origin even when the GATEWAY is down, so a
|
|
381
|
+
// reset would let a mint-ok -> stream-fail loop run forever below
|
|
382
|
+
// the unhealthy threshold. Only real gateway connectivity (stream
|
|
383
|
+
// onopen / poll success) clears the count.
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
// 2xx without a token is a terminal misconfiguration.
|
|
387
|
+
this.revertToLocal();
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
if (res.status === 404 || res.status === 401 || res.status === 403) {
|
|
391
|
+
this.revertToLocal();
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
this.onGatewayTransientFailure();
|
|
395
|
+
return false;
|
|
396
|
+
} catch {
|
|
397
|
+
this.onGatewayTransientFailure();
|
|
398
|
+
return false;
|
|
399
|
+
} finally {
|
|
400
|
+
if (timeout) clearTimeout(timeout);
|
|
401
|
+
this.tokenMintInFlight = null;
|
|
402
|
+
}
|
|
403
|
+
})();
|
|
404
|
+
return this.tokenMintInFlight;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* A transient gateway failure (mint 5xx/429, network). Keep hosted intent but
|
|
409
|
+
* count toward the unhealthy threshold; revert to local only once it trips.
|
|
410
|
+
*/
|
|
411
|
+
private onGatewayTransientFailure(): void {
|
|
412
|
+
this.consecutiveFailures++;
|
|
413
|
+
if (this.consecutiveFailures >= HOSTED_UNHEALTHY_THRESHOLD) {
|
|
414
|
+
this.revertToLocal();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Health-gate back to the app's own /poll + /events with the cursor intact
|
|
420
|
+
* (versionRef is untouched), so delivery stays poll-equivalent — never a
|
|
421
|
+
* silent stall.
|
|
422
|
+
*/
|
|
423
|
+
private revertToLocal(): void {
|
|
424
|
+
if (this.mode === "local") return;
|
|
425
|
+
this.mode = "local";
|
|
426
|
+
this.token = null;
|
|
427
|
+
// The local in-process SSE path sends no handshake, so hosted capabilities
|
|
428
|
+
// (e.g. no-awareness) must not survive the fallback — stale caps would keep
|
|
429
|
+
// collab on its fast presence cadence against the local stream. Subscribers
|
|
430
|
+
// are re-notified via the close/connect cycle below.
|
|
431
|
+
this.capabilities = [];
|
|
432
|
+
if (this.gatewayReconnectTimer) {
|
|
433
|
+
clearTimeout(this.gatewayReconnectTimer);
|
|
434
|
+
this.gatewayReconnectTimer = null;
|
|
435
|
+
}
|
|
436
|
+
this.closeEvents();
|
|
437
|
+
if (!this.stopped) {
|
|
438
|
+
this.connectEvents();
|
|
439
|
+
this.schedulePoll();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private scheduleGatewayReconnect(): void {
|
|
444
|
+
if (this.stopped || this.gatewayReconnectTimer) return;
|
|
445
|
+
this.gatewayReconnectTimer = setTimeout(() => {
|
|
446
|
+
this.gatewayReconnectTimer = null;
|
|
447
|
+
if (!this.stopped && !this.eventSource) this.connectEvents();
|
|
448
|
+
}, applyReconnectJitter(1000));
|
|
449
|
+
}
|
|
236
450
|
|
|
237
451
|
// -------------------------------------------------------------------------
|
|
238
452
|
// Subscriber management
|
|
@@ -252,7 +466,7 @@ class SyncTransport {
|
|
|
252
466
|
} else {
|
|
253
467
|
this.reschedule();
|
|
254
468
|
}
|
|
255
|
-
sub.onSseStateChange?.(this.sseConnected);
|
|
469
|
+
sub.onSseStateChange?.(this.sseConnected, this.capabilities);
|
|
256
470
|
}
|
|
257
471
|
|
|
258
472
|
remove(id: symbol): void {
|
|
@@ -319,8 +533,18 @@ class SyncTransport {
|
|
|
319
533
|
private setSseConnected(connected: boolean): void {
|
|
320
534
|
if (this.sseConnected === connected) return;
|
|
321
535
|
this.sseConnected = connected;
|
|
536
|
+
this.notifySseState();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Notify subscribers of the current SSE state AND the negotiated gateway
|
|
541
|
+
* capabilities. Called on connect/disconnect and again once the handshake
|
|
542
|
+
* arrives, so a consumer (e.g. collab) can decide — for instance — not to
|
|
543
|
+
* relax its presence cadence on a `no-awareness` hosted stream.
|
|
544
|
+
*/
|
|
545
|
+
private notifySseState(): void {
|
|
322
546
|
for (const sub of this.subscribers.values()) {
|
|
323
|
-
sub.onSseStateChange?.(
|
|
547
|
+
sub.onSseStateChange?.(this.sseConnected, this.capabilities);
|
|
324
548
|
}
|
|
325
549
|
}
|
|
326
550
|
|
|
@@ -344,19 +568,26 @@ class SyncTransport {
|
|
|
344
568
|
}, authDelay);
|
|
345
569
|
return;
|
|
346
570
|
}
|
|
347
|
-
const
|
|
571
|
+
const visibleBase = this.isActive
|
|
348
572
|
? this.effectiveInterval
|
|
349
573
|
: this.sseConnected
|
|
350
574
|
? this.effectiveFallbackInterval
|
|
351
575
|
: this.effectiveIdleInterval;
|
|
576
|
+
const base = isDocumentHidden()
|
|
577
|
+
? Math.max(visibleBase, HIDDEN_POLL_INTERVAL_MS)
|
|
578
|
+
: visibleBase;
|
|
352
579
|
// Exponential backoff while polls keep failing (500s during a deploy,
|
|
353
580
|
// DNS blips, a struggling DB). Auth failures have their own cooldown
|
|
354
581
|
// above; this covers everything else so a down server isn't hammered at
|
|
355
582
|
// full cadence. Resets on the first successful poll.
|
|
356
|
-
const
|
|
583
|
+
const backoff =
|
|
357
584
|
this.consecutiveFailures > 0
|
|
358
585
|
? Math.min(base * 2 ** Math.min(this.consecutiveFailures, 5), 300_000)
|
|
359
586
|
: base;
|
|
587
|
+
// Jitter only for gateway-capable transports so reconnect/poll retries
|
|
588
|
+
// don't stampede a gateway deploy; apps with no gateway config keep the
|
|
589
|
+
// exact deterministic cadence.
|
|
590
|
+
const delay = this.gateway ? applyReconnectJitter(backoff) : backoff;
|
|
360
591
|
this.timer = setTimeout(() => {
|
|
361
592
|
this.timer = null;
|
|
362
593
|
void this.poll();
|
|
@@ -383,7 +614,6 @@ class SyncTransport {
|
|
|
383
614
|
private connectEvents(): void {
|
|
384
615
|
if (
|
|
385
616
|
this.stopped ||
|
|
386
|
-
!this.sseUrl ||
|
|
387
617
|
this.eventSource ||
|
|
388
618
|
typeof EventSource === "undefined" ||
|
|
389
619
|
(this.effectivePauseWhenHidden && isDocumentHidden())
|
|
@@ -391,10 +621,38 @@ class SyncTransport {
|
|
|
391
621
|
return;
|
|
392
622
|
}
|
|
393
623
|
|
|
394
|
-
|
|
624
|
+
// Hosted gateway needs a subscribe token before the stream can open.
|
|
625
|
+
// EventSource can't set headers, so the token rides the connect query
|
|
626
|
+
// string (see activeSseUrl). Mint first, then connect.
|
|
627
|
+
if (this.mode === "hosted" && this.gateway && !this.token) {
|
|
628
|
+
void this.mintToken().then((ok) => {
|
|
629
|
+
if (this.stopped) return;
|
|
630
|
+
if (ok && !this.eventSource) {
|
|
631
|
+
this.connectEvents();
|
|
632
|
+
} else if (!ok && this.mode === "hosted") {
|
|
633
|
+
// Transient mint failure (terminal ones already reverted to local,
|
|
634
|
+
// flipping mode). Without a retry timer nothing would ever reopen
|
|
635
|
+
// SSE — connectEvents is only reachable from focus/visibility/run
|
|
636
|
+
// events — leaving the tab poll-only at the idle cadence.
|
|
637
|
+
this.scheduleGatewayReconnect();
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const url = this.activeSseUrl;
|
|
644
|
+
if (!url) return;
|
|
645
|
+
|
|
646
|
+
const source = new EventSource(url);
|
|
395
647
|
this.eventSource = source;
|
|
396
648
|
source.onopen = () => {
|
|
397
649
|
this.setSseConnected(true);
|
|
650
|
+
if (this.mode === "hosted") {
|
|
651
|
+
// A live gateway stream is the real health signal: clear failure
|
|
652
|
+
// counts accumulated by mint/stream retries. Local mode keeps main's
|
|
653
|
+
// semantics (only a successful poll resets the poll backoff).
|
|
654
|
+
this.consecutiveFailures = 0;
|
|
655
|
+
}
|
|
398
656
|
this.schedulePoll();
|
|
399
657
|
};
|
|
400
658
|
source.onerror = () => {
|
|
@@ -406,6 +664,19 @@ class SyncTransport {
|
|
|
406
664
|
// we'd be stuck on polling-only forever.
|
|
407
665
|
if (source.readyState === EventSource.CLOSED) {
|
|
408
666
|
this.eventSource = null;
|
|
667
|
+
if (this.mode === "hosted" && this.gateway) {
|
|
668
|
+
// A closed gateway stream is most likely an expired token or a
|
|
669
|
+
// request-timeout/deploy cycle. Re-mint and reconnect with jitter;
|
|
670
|
+
// this is NOT the poll-401 cooldown path. Each closed stream counts
|
|
671
|
+
// toward the unhealthy threshold so a hard-down gateway (or one
|
|
672
|
+
// rejecting our tokens) health-gates to local instead of looping
|
|
673
|
+
// mint+connect forever; a successful reconnect resets the count in
|
|
674
|
+
// onopen above.
|
|
675
|
+
this.token = null;
|
|
676
|
+
this.onGatewayTransientFailure();
|
|
677
|
+
if (this.mode === "hosted") this.scheduleGatewayReconnect();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
409
680
|
}
|
|
410
681
|
this.schedulePoll();
|
|
411
682
|
};
|
|
@@ -421,6 +692,38 @@ class SyncTransport {
|
|
|
421
692
|
// Ignore malformed SSE frames; polling is the safety net.
|
|
422
693
|
}
|
|
423
694
|
};
|
|
695
|
+
|
|
696
|
+
if (this.mode === "hosted" && this.gateway) {
|
|
697
|
+
// Control frames ride NAMED SSE events so they never reach onmessage /
|
|
698
|
+
// normalizeEventPayload as spurious data events.
|
|
699
|
+
source.addEventListener(REALTIME_SSE_HANDSHAKE_EVENT, (e) => {
|
|
700
|
+
const hs = parseHandshakeFrame((e as MessageEvent).data);
|
|
701
|
+
if (!hs) return;
|
|
702
|
+
if (hs.protocol !== REALTIME_PROTOCOL_VERSION) {
|
|
703
|
+
// Surface an unexpected protocol rather than silently adopting its
|
|
704
|
+
// capabilities; keep the conservative (no advertised capabilities)
|
|
705
|
+
// stance so downstream (collab) does not relax on assumptions.
|
|
706
|
+
console.warn(
|
|
707
|
+
`[agent-native] unsupported realtime protocol ${hs.protocol} (expected ${REALTIME_PROTOCOL_VERSION})`,
|
|
708
|
+
);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
this.capabilities = hs.capabilities;
|
|
712
|
+
// Re-notify subscribers now that capabilities are known — the initial
|
|
713
|
+
// connected notification fired before the handshake arrived.
|
|
714
|
+
this.notifySseState();
|
|
715
|
+
});
|
|
716
|
+
source.addEventListener(REALTIME_SSE_TOKEN_EVENT, (e) => {
|
|
717
|
+
const frame = parseTokenFrame((e as MessageEvent).data);
|
|
718
|
+
if (!frame?.token) return;
|
|
719
|
+
this.token = frame.token;
|
|
720
|
+
// EventSource can't change a live stream's URL, and its auto-reconnect
|
|
721
|
+
// reuses the original (old-token) URL. Close and reconnect (jittered) so
|
|
722
|
+
// the rotated token is actually used on the next connect.
|
|
723
|
+
this.closeEvents();
|
|
724
|
+
this.scheduleGatewayReconnect();
|
|
725
|
+
});
|
|
726
|
+
}
|
|
424
727
|
}
|
|
425
728
|
|
|
426
729
|
/**
|
|
@@ -442,10 +745,17 @@ class SyncTransport {
|
|
|
442
745
|
if (this.stopped || this.inFlight) return;
|
|
443
746
|
this.inFlight = true;
|
|
444
747
|
try {
|
|
748
|
+
if (this.mode === "hosted" && this.gateway && !this.token) {
|
|
749
|
+
// No token yet — mint before polling the gateway. A failed mint has
|
|
750
|
+
// already reverted us to local; a scheduled poll will pick it up.
|
|
751
|
+
const ok = await this.mintToken();
|
|
752
|
+
if (!ok || this.stopped) return;
|
|
753
|
+
}
|
|
445
754
|
const data = await fetchPollJson<PollResponse>(
|
|
446
|
-
this.
|
|
755
|
+
this.activePollUrl,
|
|
447
756
|
this.versionRef,
|
|
448
757
|
this.effectiveInterval,
|
|
758
|
+
this.mode === "hosted" ? (this.token ?? undefined) : undefined,
|
|
449
759
|
);
|
|
450
760
|
if (this.stopped) return;
|
|
451
761
|
this.consecutiveFailures = 0;
|
|
@@ -455,7 +765,18 @@ class SyncTransport {
|
|
|
455
765
|
} catch (err) {
|
|
456
766
|
if (this.stopped) return;
|
|
457
767
|
this.consecutiveFailures++;
|
|
458
|
-
if (
|
|
768
|
+
if (this.mode === "hosted" && this.gateway) {
|
|
769
|
+
// Gateway auth failure → re-mint (expired/rotated token), WITHOUT
|
|
770
|
+
// tripping the poll-401 cooldown. Persistent failures of any kind
|
|
771
|
+
// health-gate back to the local app.
|
|
772
|
+
if (isAuthFailure(err)) {
|
|
773
|
+
this.token = null;
|
|
774
|
+
void this.mintToken();
|
|
775
|
+
}
|
|
776
|
+
if (this.consecutiveFailures >= HOSTED_UNHEALTHY_THRESHOLD) {
|
|
777
|
+
this.revertToLocal();
|
|
778
|
+
}
|
|
779
|
+
} else if (isAuthFailure(err)) {
|
|
459
780
|
this.authFailureUntil = Date.now() + POLL_AUTH_FAILURE_COOLDOWN_MS;
|
|
460
781
|
this.closeEvents();
|
|
461
782
|
}
|
|
@@ -490,6 +811,11 @@ class SyncTransport {
|
|
|
490
811
|
clearTimeout(this.timer);
|
|
491
812
|
this.timer = null;
|
|
492
813
|
}
|
|
814
|
+
} else {
|
|
815
|
+
// Keep push connected and the polling safety net alive in backgrounded
|
|
816
|
+
// tabs, but relax an active chat's cadence so hidden work stays current
|
|
817
|
+
// without polling as aggressively as the visible surface.
|
|
818
|
+
this.reschedule();
|
|
493
819
|
}
|
|
494
820
|
};
|
|
495
821
|
|
|
@@ -554,6 +880,10 @@ class SyncTransport {
|
|
|
554
880
|
clearTimeout(this.timer);
|
|
555
881
|
this.timer = null;
|
|
556
882
|
}
|
|
883
|
+
if (this.gatewayReconnectTimer) {
|
|
884
|
+
clearTimeout(this.gatewayReconnectTimer);
|
|
885
|
+
this.gatewayReconnectTimer = null;
|
|
886
|
+
}
|
|
557
887
|
window.removeEventListener("focus", this.handleFocus);
|
|
558
888
|
window.removeEventListener(
|
|
559
889
|
"agentNative.chatRunning",
|
|
@@ -576,11 +906,14 @@ const transportRegistry = new Map<string, SyncTransport>();
|
|
|
576
906
|
function getOrCreateTransport(
|
|
577
907
|
pollUrl: string,
|
|
578
908
|
sseUrl: string | false,
|
|
909
|
+
gateway: RealtimeGatewayBinding | null = null,
|
|
579
910
|
): SyncTransport {
|
|
911
|
+
// Key on the LOCAL urls only — a transport may flip hosted→local at runtime,
|
|
912
|
+
// and the token must never fragment the registry, so neither is in the key.
|
|
580
913
|
const key = `${pollUrl}\0${String(sseUrl)}`;
|
|
581
914
|
let transport = transportRegistry.get(key);
|
|
582
915
|
if (!transport) {
|
|
583
|
-
transport = new SyncTransport(pollUrl, sseUrl);
|
|
916
|
+
transport = new SyncTransport(pollUrl, sseUrl, gateway);
|
|
584
917
|
transportRegistry.set(key, transport);
|
|
585
918
|
}
|
|
586
919
|
return transport;
|
|
@@ -607,7 +940,10 @@ export interface SubscribeSyncEventsOptions {
|
|
|
607
940
|
/** Receives every batch of change events (SSE push or poll). */
|
|
608
941
|
onEvents: (events: SyncEvent[], version: number | undefined) => void;
|
|
609
942
|
/** Notified when the shared SSE connection opens/closes (and once on join). */
|
|
610
|
-
onSseStateChange?: (
|
|
943
|
+
onSseStateChange?: (
|
|
944
|
+
connected: boolean,
|
|
945
|
+
capabilities?: readonly string[],
|
|
946
|
+
) => void;
|
|
611
947
|
pollUrl?: string;
|
|
612
948
|
sseUrl?: string | false;
|
|
613
949
|
pauseWhenHidden?: boolean;
|
|
@@ -636,12 +972,16 @@ export function subscribeSyncEvents(
|
|
|
636
972
|
): () => void {
|
|
637
973
|
const pollUrl = agentNativePath(options.pollUrl ?? "/_agent-native/poll");
|
|
638
974
|
const sseUrl = resolveSseUrl(options.sseUrl);
|
|
639
|
-
const transport = getOrCreateTransport(
|
|
975
|
+
const transport = getOrCreateTransport(
|
|
976
|
+
pollUrl,
|
|
977
|
+
sseUrl,
|
|
978
|
+
resolveGatewayBinding(sseUrl),
|
|
979
|
+
);
|
|
640
980
|
const id = Symbol("subscribeSyncEvents");
|
|
641
981
|
transport.add(id, {
|
|
642
982
|
onEvents: options.onEvents,
|
|
643
983
|
onSseStateChange: options.onSseStateChange,
|
|
644
|
-
pauseWhenHidden: options.pauseWhenHidden ??
|
|
984
|
+
pauseWhenHidden: options.pauseWhenHidden ?? false,
|
|
645
985
|
interval: options.interval ?? 60_000,
|
|
646
986
|
idleInterval: options.interval ?? 60_000,
|
|
647
987
|
fallbackInterval: options.fallbackInterval ?? 60_000,
|
|
@@ -675,8 +1015,9 @@ export function subscribeSyncEvents(
|
|
|
675
1015
|
* @param options.interval - Poll interval in ms. Default: 2000
|
|
676
1016
|
* @param options.fallbackInterval - Poll interval while SSE is connected.
|
|
677
1017
|
* Default: 60000
|
|
678
|
-
* @param options.pauseWhenHidden - Pause
|
|
679
|
-
* Default:
|
|
1018
|
+
* @param options.pauseWhenHidden - Pause sync while the tab is hidden.
|
|
1019
|
+
* Default: false. Hidden tabs keep SSE connected and poll no faster than
|
|
1020
|
+
* every 10 seconds while active; idle polling remains at 60 seconds.
|
|
680
1021
|
* @param options.ignoreSource - Skip events whose `requestSource` matches this
|
|
681
1022
|
* value. Use a per-tab ID so the UI ignores its own writes while still
|
|
682
1023
|
* picking up changes from other tabs, agents, and scripts.
|
|
@@ -714,7 +1055,7 @@ export function useDbSync(
|
|
|
714
1055
|
options.fallbackInterval ?? SSE_FALLBACK_INTERVAL_MS,
|
|
715
1056
|
interval,
|
|
716
1057
|
),
|
|
717
|
-
pauseWhenHidden =
|
|
1058
|
+
pauseWhenHidden = false,
|
|
718
1059
|
} = options;
|
|
719
1060
|
const idleInterval =
|
|
720
1061
|
options.interval === undefined ? IDLE_POLL_INTERVAL_MS : interval;
|
|
@@ -978,7 +1319,11 @@ export function useDbSync(
|
|
|
978
1319
|
);
|
|
979
1320
|
}
|
|
980
1321
|
|
|
981
|
-
const transport = getOrCreateTransport(
|
|
1322
|
+
const transport = getOrCreateTransport(
|
|
1323
|
+
pollUrl,
|
|
1324
|
+
sseUrl,
|
|
1325
|
+
resolveGatewayBinding(sseUrl),
|
|
1326
|
+
);
|
|
982
1327
|
transport.add(id, {
|
|
983
1328
|
onEvents,
|
|
984
1329
|
pauseWhenHidden,
|
|
@@ -1053,7 +1398,7 @@ export function useScreenRefreshKey(
|
|
|
1053
1398
|
options.fallbackInterval ?? SSE_FALLBACK_INTERVAL_MS,
|
|
1054
1399
|
interval,
|
|
1055
1400
|
),
|
|
1056
|
-
pauseWhenHidden =
|
|
1401
|
+
pauseWhenHidden = false,
|
|
1057
1402
|
} = options;
|
|
1058
1403
|
const idleInterval =
|
|
1059
1404
|
options.interval === undefined ? IDLE_POLL_INTERVAL_MS : interval;
|
|
@@ -1084,7 +1429,11 @@ export function useScreenRefreshKey(
|
|
|
1084
1429
|
);
|
|
1085
1430
|
}
|
|
1086
1431
|
|
|
1087
|
-
const transport = getOrCreateTransport(
|
|
1432
|
+
const transport = getOrCreateTransport(
|
|
1433
|
+
pollUrl,
|
|
1434
|
+
sseUrl,
|
|
1435
|
+
resolveGatewayBinding(sseUrl),
|
|
1436
|
+
);
|
|
1088
1437
|
transport.add(id, {
|
|
1089
1438
|
onEvents,
|
|
1090
1439
|
pauseWhenHidden,
|
package/src/collab/client.ts
CHANGED
|
@@ -45,6 +45,7 @@ import * as Y from "yjs";
|
|
|
45
45
|
|
|
46
46
|
import { agentNativePath } from "../client/api-path.js";
|
|
47
47
|
import { subscribeSyncEvents, type SyncEvent } from "../client/use-db-sync.js";
|
|
48
|
+
import { REALTIME_CAP_NO_AWARENESS } from "../realtime-protocol.js";
|
|
48
49
|
export {
|
|
49
50
|
dedupeCollabUsersByEmail,
|
|
50
51
|
emailToColor,
|
|
@@ -326,6 +327,12 @@ class CollabDocConnection {
|
|
|
326
327
|
private pollVersion = 0;
|
|
327
328
|
private lastPolledVersion = 0;
|
|
328
329
|
private sseActive = false;
|
|
330
|
+
// Whether the active SSE stream actually forwards awareness. The hosted
|
|
331
|
+
// Realtime Gateway advertises `no-awareness` (it can't see the in-process
|
|
332
|
+
// awareness emitter), so on that transport we must NOT relax the presence
|
|
333
|
+
// poll cadence — otherwise remote cursors go stale. In-process SSE forwards
|
|
334
|
+
// awareness and sends no handshake, so this stays true there.
|
|
335
|
+
private sseAwarenessCovered = false;
|
|
329
336
|
private sseSubscribedWithPause: boolean | null = null;
|
|
330
337
|
private unsubscribeCollabEvents: (() => void) | null = null;
|
|
331
338
|
private unsubscribeAwarenessEvents: (() => void) | null = null;
|
|
@@ -760,8 +767,22 @@ class CollabDocConnection {
|
|
|
760
767
|
if (this.disposed || !this.syncActive) return;
|
|
761
768
|
for (const change of events) this.handleSharedEvent(change);
|
|
762
769
|
},
|
|
763
|
-
onSseStateChange: (connected) => {
|
|
770
|
+
onSseStateChange: (connected, capabilities) => {
|
|
771
|
+
const wasActive = this.sseActive;
|
|
764
772
|
this.sseActive = connected;
|
|
773
|
+
// Only treat SSE as covering awareness when it's connected AND does not
|
|
774
|
+
// advertise `no-awareness` (the hosted gateway does). Drives whether we
|
|
775
|
+
// relax the presence poll — see getActivePollInterval.
|
|
776
|
+
const awarenessCovered =
|
|
777
|
+
connected && !capabilities?.includes(REALTIME_CAP_NO_AWARENESS);
|
|
778
|
+
const coverageFlipped = awarenessCovered !== this.sseAwarenessCovered;
|
|
779
|
+
this.sseAwarenessCovered = awarenessCovered;
|
|
780
|
+
// The gateway handshake lands AFTER onopen, so a relaxed poll timer
|
|
781
|
+
// scheduled at connect can already be pending when `no-awareness`
|
|
782
|
+
// arrives. Reschedule only for that mid-connection capability flip so
|
|
783
|
+
// the fast presence cadence applies immediately; connect/disconnect
|
|
784
|
+
// transitions keep the pre-existing next-natural-tick behavior.
|
|
785
|
+
if (coverageFlipped && connected === wasActive) this.reschedulePoll();
|
|
765
786
|
if (connected) this.consecutiveErrors = 0;
|
|
766
787
|
},
|
|
767
788
|
pauseWhenHidden,
|
|
@@ -811,7 +832,10 @@ class CollabDocConnection {
|
|
|
811
832
|
}
|
|
812
833
|
|
|
813
834
|
private getActivePollInterval(): number {
|
|
814
|
-
|
|
835
|
+
// Relax to the slow cadence only when SSE is genuinely carrying awareness;
|
|
836
|
+
// on a `no-awareness` hosted stream keep the fast cadence so presence/
|
|
837
|
+
// cursor state doesn't go stale (the gateway doesn't forward awareness).
|
|
838
|
+
return this.sseActive && this.sseAwarenessCovered
|
|
815
839
|
? this.effectivePollIntervalWithSse
|
|
816
840
|
: this.effectivePollInterval;
|
|
817
841
|
}
|
|
@@ -194,7 +194,13 @@ export const WORKSPACE_CONNECTION_PROVIDERS = [
|
|
|
194
194
|
},
|
|
195
195
|
],
|
|
196
196
|
capabilities: ["search", "import", "docs"],
|
|
197
|
-
recommendedTemplateUses: [
|
|
197
|
+
recommendedTemplateUses: [
|
|
198
|
+
"brain",
|
|
199
|
+
"content",
|
|
200
|
+
"slides",
|
|
201
|
+
"dispatch",
|
|
202
|
+
"analytics",
|
|
203
|
+
],
|
|
198
204
|
oauth: {
|
|
199
205
|
provider: "google",
|
|
200
206
|
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|