@agent-native/core 0.169.0 → 0.170.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 +4 -1
- package/bin/agent-native.js +23 -5
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/analytics/actions/bigquery-table-info.ts +3 -2
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
- package/corpus/templates/analytics/actions/search-bigquery-schema.ts +130 -4
- package/corpus/templates/analytics/agent-native.config.ts +1 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +36 -23
- package/corpus/templates/analytics/app/i18n-data.ts +28 -0
- package/corpus/templates/analytics/app/lib/dashboard-visibility.ts +41 -0
- package/corpus/templates/analytics/app/pages/Agents.tsx +25 -1
- package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +34 -9
- package/corpus/templates/analytics/app/routes/$.tsx +10 -0
- package/corpus/templates/analytics/app/routes.ts +3 -1
- package/corpus/templates/analytics/changelog/2026-08-21-dashboard-filters-keep-other-users-dashboards-out-of-mine.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-22-fixed-the-agent-native-templates-first-party-dashboard-panel.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-22-org-admin-panels-no-longer-report-a-database-error-as-a-per.md +9 -0
- package/corpus/templates/analytics/changelog/2026-08-22-slack-requests-report-invalid-inputs.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-22-unknown-analytics-links-now-return-a-real-not-found-status.md +6 -0
- package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +2 -1
- package/corpus/templates/analytics/docs/schemas/tracked-events.md +2 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +29 -7
- package/corpus/templates/analytics/server/handlers/slack.ts +59 -5
- package/corpus/templates/analytics/server/lib/db-admin-connections.ts +11 -6
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +132 -2
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +14 -3
- package/corpus/templates/analytics/server/lib/slack.ts +4 -2
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +33 -3
- package/corpus/templates/analytics/vitest.config.ts +6 -1
- package/corpus/templates/assets/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/assets/app/routes/library.tsx +5 -3
- package/corpus/templates/assets/app/routes.ts +3 -1
- package/corpus/templates/assets/changelog/2026-08-22-library-page-loads-again-instead-of-crashing-on-open.md +6 -0
- package/corpus/templates/assets/vitest.config.ts +6 -1
- package/corpus/templates/brain/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/brain/app/components/ui/pointer-lock.ts +1 -0
- package/corpus/templates/brain/app/routes/sources.tsx +8 -3
- package/corpus/templates/brain/changelog/2026-08-22-tuning-or-adding-a-source-no-longer-leaves-the-sources-page.md +6 -0
- package/corpus/templates/brain/server/lib/brain.ts +27 -2
- package/corpus/templates/brain/vitest.config.ts +1 -0
- package/corpus/templates/calendar/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/calendar/actions/list-booking-links.ts +83 -4
- package/corpus/templates/calendar/actions/update-booking-link.ts +4 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +11 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +1 -1
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +103 -44
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +28 -24
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +9 -2
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +47 -58
- package/corpus/templates/calendar/app/hooks/use-booking-links.ts +5 -1
- package/corpus/templates/calendar/app/lib/event-layout.ts +52 -26
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +126 -98
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +1 -0
- package/corpus/templates/calendar/app/pages/Settings.tsx +88 -81
- package/corpus/templates/calendar/app/routes.ts +3 -1
- package/corpus/templates/calendar/changelog/2026-08-22-booking-link-visibility-respects-shared-access.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-22-clicking-an-out-of-office-event-s-marker-no-longer-creates-a.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-22-event-detail-views-now-show-your-calendar-timezone.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-22-google-calendar-can-now-connect-with-the-shared-google-oauth.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-22-overlapping-events-now-show-each-others-boundaries.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-22-public-booking-pages-now-offer-time-slots-from-the-host-s-we.md +6 -0
- package/corpus/templates/calendar/server/handlers/bookings.ts +5 -7
- package/corpus/templates/calendar/server/handlers/google-auth.ts +10 -2
- package/corpus/templates/calendar/server/lib/google-calendar.ts +139 -14
- package/corpus/templates/calendar/server/onboarding.ts +85 -0
- package/corpus/templates/calendar/server/plugins/agent-chat.ts +2 -0
- package/corpus/templates/calendar/shared/api.ts +9 -1
- package/corpus/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/chat/app/routes.ts +3 -1
- package/corpus/templates/chat/vitest.config.ts +8 -0
- package/corpus/templates/clips/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/clips/actions/get-meeting.ts +9 -4
- package/corpus/templates/clips/actions/list-recordings.ts +16 -3
- package/corpus/templates/clips/actions/search-meetings.ts +1 -0
- package/corpus/templates/clips/actions/stop-meeting-recording.ts +87 -6
- package/corpus/templates/clips/actions/view-screen.ts +8 -1
- package/corpus/templates/clips/app/components/library/search-bar.tsx +1 -1
- package/corpus/templates/clips/app/components/meetings/meeting-history-row.tsx +32 -1
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +1 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +29 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +5 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +33 -4
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +1 -0
- package/corpus/templates/clips/app/routes/access-request.approve.tsx +16 -9
- package/corpus/templates/clips/app/routes/embed.$shareId.tsx +17 -7
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +17 -7
- package/corpus/templates/clips/changelog/2026-08-21-camera-bubble-black-circle.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-22-screen-recordings-respect-the-microphone-toggle.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-22-the-recordings-library-no-longer-fails-right-after-server-start.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-23-the-agent-can-see-your-library-right-after-server-start.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +1 -2
- package/corpus/templates/clips/chrome-extension/src/native-transcription.ts +39 -7
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +21 -25
- package/corpus/templates/clips/desktop/src/app.tsx +53 -24
- package/corpus/templates/clips/desktop/src/lib/bubble-playback.ts +68 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +3 -0
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +72 -14
- package/corpus/templates/clips/desktop/src/lib/updater.ts +8 -3
- package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +162 -20
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +43 -6
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +150 -13
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +12 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +28 -3
- package/corpus/templates/clips/server/lib/emails.ts +17 -33
- package/corpus/templates/content/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +140 -15
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +12 -14
- package/corpus/templates/content/app/components/ui/pointer-lock.ts +1 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +18 -2
- package/corpus/templates/content/app/hooks/use-local-storage.ts +38 -22
- package/corpus/templates/content/app/i18n-data.ts +25 -0
- package/corpus/templates/content/app/routes.ts +3 -1
- package/corpus/templates/content/changelog/2026-08-22-code-blocks-inserted-from-the-slash-menu-no-longer-get-stuck.md +6 -0
- package/corpus/templates/content/changelog/2026-08-22-the-sidebar-page-tree-no-longer-flashes-empty-after-creating-a-page.md +6 -0
- package/corpus/templates/content/docs/solutions/2026-08-21-content-mermaid-rendering-shape.md +233 -0
- package/corpus/templates/content/server/plugins/agent-chat.ts +9 -2
- package/corpus/templates/content/vitest.config.ts +7 -1
- package/corpus/templates/crm/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/design/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1386 -92
- package/corpus/templates/design/actions/generate-design.ts +98 -47
- package/corpus/templates/design/actions/get-design-system.ts +6 -1
- package/corpus/templates/design/actions/refresh-design-system-with-builder.ts +146 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +70 -26
- package/corpus/templates/design/app/components/design/EditPanel.tsx +15 -47
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +284 -136
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +2060 -144
- package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +6 -2
- package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +4 -4
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +10 -8
- package/corpus/templates/design/app/components/design/multi-screen/alt-hover-measurement.ts +19 -2
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +9 -2
- package/corpus/templates/design/app/components/layout/Header.tsx +15 -0
- package/corpus/templates/design/app/components/layout/Layout.tsx +7 -2
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +13 -8
- package/corpus/templates/design/app/i18n-data.ts +11 -10
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +12 -0
- package/corpus/templates/design/app/lib/design-system-data.ts +68 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +99 -57
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +39 -8
- package/corpus/templates/design/app/pages/DesignSystems.tsx +137 -43
- package/corpus/templates/design/app/pages/Index.tsx +3 -1
- package/corpus/templates/design/app/pages/Templates.tsx +0 -10
- package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +10 -0
- package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +50 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +10 -8
- package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +6 -6
- package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +18 -7
- package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +4 -90
- package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +2 -1
- package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +4 -3
- package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +12 -2
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +9 -2
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +11 -1
- package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +11 -19
- package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +6 -6
- package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +1 -1
- package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +12 -15
- package/corpus/templates/design/app/pages/design-editor/preview-navigation.ts +25 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -59
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/types.ts +3 -3
- package/corpus/templates/design/app/routes.ts +3 -1
- package/corpus/templates/design/changelog/2026-08-20-canvas-edits-patch-the-frame-instead-of-rebuilding-it.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-builder-design-system-imports-reconcile-after-indexing.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-draw-mode-needs-shift-y.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-eyedropper-shortcut.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-figma-style-alignment-guides-and-snapping.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-group-transforms-keep-the-selection.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-inspector-fields-keep-focus-on-enter.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-pen-alt-keeps-the-incoming-handle.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-pen-on-the-board-creates-one-vector.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-scale-a-multi-selection-from-the-group-handles.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-scale-resizes-text-with-the-object.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-selecting-another-object-keeps-the-scale-tool-armed.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-the-shape-tool-remembers-the-shape-you-picked.md +6 -0
- package/corpus/templates/design/changelog/2026-08-21-vectors-drawn-in-a-frame-land-where-you-drew-them.md +6 -0
- package/corpus/templates/design/changelog/2026-08-22-multi-screen-generation-no-longer-loses-a-saved-screens-layo.md +6 -0
- package/corpus/templates/design/changelog/2026-08-22-rename-and-search-fields-are-labeled-for-screen-readers.md +6 -0
- package/corpus/templates/design/package.json +2 -0
- package/corpus/templates/design/server/lib/builder-design-system-proxy.ts +311 -0
- package/corpus/templates/design/shared/canvas-math.ts +612 -206
- package/corpus/templates/design/shared/pen-path.ts +37 -0
- package/corpus/templates/design/vitest.config.ts +7 -1
- package/corpus/templates/dispatch/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/dispatch/actions/list-workspace-connections.ts +34 -3
- package/corpus/templates/dispatch/app/routes/integrations.tsx +55 -21
- package/corpus/templates/dispatch/app/routes.ts +3 -1
- package/corpus/templates/factory/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/factory/.env.example +2 -7
- package/corpus/templates/factory/AGENTS.md +3 -0
- package/corpus/templates/factory/README.md +5 -3
- package/corpus/templates/factory/actions/create-factory.ts +16 -3
- package/corpus/templates/factory/actions/get-slack-feedback-context.ts +2 -0
- package/corpus/templates/factory/actions/get-triage-item.ts +2 -0
- package/corpus/templates/factory/actions/list-factory-audit.ts +116 -36
- package/corpus/templates/factory/actions/list-factory-automations.ts +2 -1
- package/corpus/templates/factory/actions/list-triage-items.ts +22 -19
- package/corpus/templates/factory/actions/run-factory-automation.ts +7 -3
- package/corpus/templates/factory/actions/save-factory-automation.ts +5 -2
- package/corpus/templates/factory/actions/save-triage-config.ts +2 -8
- package/corpus/templates/factory/actions/start-builder-for-item.ts +21 -0
- package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +229 -297
- package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +397 -247
- package/corpus/templates/factory/app/components/factory/FactorySourceSettingsGroup.tsx +33 -0
- package/corpus/templates/factory/app/components/factory/NewFactoryForm.tsx +202 -162
- package/corpus/templates/factory/app/i18n/en-US.ts +32 -4
- package/corpus/templates/factory/app/routes/factory.tsx +84 -11
- package/corpus/templates/factory/app/routes.ts +3 -1
- package/corpus/templates/factory/changelog/2026-08-21-activity-runs-use-automation-display-names.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-21-create-and-settings-source-cards.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-21-keep-automations-visible-while-running.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-21-nested-factory-automations-can-poll.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-21-settings-sticky-unsaved-bar.md +6 -0
- package/corpus/templates/factory/server/connectors/credentials.ts +10 -5
- package/corpus/templates/factory/server/lib/factory-audit-report.ts +412 -0
- package/corpus/templates/factory/server/lib/factory-config-reconcile.ts +321 -0
- package/corpus/templates/factory/server/lib/factory-scope.ts +25 -2
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +13 -4
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +148 -4
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +4 -6
- package/corpus/templates/factory/vitest.config.ts +17 -2
- package/corpus/templates/forms/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/forms/app/routes/_app.settings.tsx +4 -1
- package/corpus/templates/forms/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
- package/corpus/templates/forms/server/lib/public-form-ssr.ts +11 -1
- package/corpus/templates/forms/vitest.config.ts +1 -0
- package/corpus/templates/macros/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/macros/app/routes.ts +3 -1
- package/corpus/templates/macros/vitest.config.ts +6 -1
- package/corpus/templates/mail/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/mail/actions/manage-gmail-filters.ts +10 -2
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -37
- package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +14 -2
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +66 -36
- package/corpus/templates/mail/app/hooks/use-emails.ts +13 -3
- package/corpus/templates/mail/app/hooks/use-google-auth.ts +2 -0
- package/corpus/templates/mail/app/lib/google-oauth-setup.ts +17 -0
- package/corpus/templates/mail/app/pages/InboxPage.tsx +69 -9
- package/corpus/templates/mail/app/routes/$.tsx +9 -0
- package/corpus/templates/mail/app/routes/$view.$threadId.tsx +23 -1
- package/corpus/templates/mail/app/routes/$view.tsx +47 -1
- package/corpus/templates/mail/app/routes.ts +3 -1
- package/corpus/templates/mail/changelog/2026-08-22-gmail-can-now-connect-with-the-shared-google-oauth.md +6 -0
- package/corpus/templates/mail/changelog/2026-08-22-gmail-filters-now-shows-the-real-error.md +6 -0
- package/corpus/templates/mail/changelog/2026-08-22-unknown-mail-urls-now-show-the-404-page.md +6 -0
- package/corpus/templates/mail/server/handlers/google-auth.ts +11 -2
- package/corpus/templates/mail/server/lib/google-auth.ts +92 -12
- package/corpus/templates/mail/server/onboarding.ts +66 -18
- package/corpus/templates/mail/vitest.config.ts +6 -1
- package/corpus/templates/plan/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/plan/app/routes/settings.tsx +4 -1
- package/corpus/templates/plan/app/routes.ts +3 -1
- package/corpus/templates/plan/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
- package/corpus/templates/plan/netlify.toml +1 -1
- package/corpus/templates/slides/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/slides/actions/patch-deck.ts +79 -10
- package/corpus/templates/slides/actions/update-slide.ts +11 -1
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +14 -4
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +10 -52
- package/corpus/templates/slides/app/components/editor/GoogleDocImportHint.tsx +8 -53
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +9 -75
- package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +8 -60
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +10 -2
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +19 -7
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +49 -36
- package/corpus/templates/slides/app/routes/access-request.approve.tsx +16 -9
- package/corpus/templates/slides/app/routes.ts +3 -1
- package/corpus/templates/slides/changelog/2026-08-22-already-uploaded-chat-images-are-no-longer-dropped.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-22-google-drive-and-slides-can-now-connect-with-the-shared-google-oauth.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-22-multi-slide-repairs-now-report-slides-that-still-overflow.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-22-update-slide-now-reports-which-batched-edit-was-skipped.md +6 -0
- package/corpus/templates/slides/server/handlers/google-docs-auth.ts +52 -1
- package/corpus/templates/slides/server/lib/chat-attachments.ts +29 -3
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +66 -16
- package/corpus/templates/slides/server/lib/slide-content-patch.ts +13 -2
- package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -2
- package/corpus/templates/slides/tsconfig.json +5 -0
- package/corpus/templates/slides/vitest.config.ts +6 -1
- package/corpus/templates/tasks/.agents/skills/secrets/SKILL.md +11 -4
- package/corpus/templates/tasks/app/routes/settings.tsx +4 -1
- package/corpus/templates/tasks/app/routes.ts +3 -1
- package/corpus/templates/tasks/changelog/2026-08-22-the-extensions-link-now-opens-the-extensions-tab-instead-o.md +6 -0
- package/corpus/templates/tasks/vitest.config.ts +7 -1
- package/dist/action.d.ts +91 -0
- package/dist/action.js +50 -0
- package/dist/agent/abort-reasons.d.ts +38 -0
- package/dist/agent/abort-reasons.js +49 -0
- package/dist/agent/engine/ai-sdk-engine.js +8 -5
- package/dist/agent/engine/anthropic-engine.js +7 -5
- package/dist/agent/engine/credential-errors.d.ts +1 -1
- package/dist/agent/engine/credential-errors.js +2 -2
- package/dist/agent/engine/first-event-timeout.d.ts +42 -17
- package/dist/agent/engine/first-event-timeout.js +67 -25
- package/dist/agent/production-agent.d.ts +23 -0
- package/dist/agent/production-agent.js +107 -102
- package/dist/agent/run-loop-with-resume.d.ts +2 -28
- package/dist/agent/run-loop-with-resume.js +25 -40
- package/dist/agent/run-manager.js +5 -4
- package/dist/agent/run-store.d.ts +55 -0
- package/dist/agent/run-store.js +124 -1
- package/dist/agent/tool-search.js +2 -1
- package/dist/agent/types.d.ts +9 -4
- package/dist/app-config/configuration-error.d.ts +11 -0
- package/dist/app-config/configuration-error.js +14 -0
- package/dist/app-config/index.d.ts +1 -0
- package/dist/app-config/index.js +1 -0
- package/dist/app-config/integrations.d.ts +1 -0
- package/dist/app-config/integrations.js +10 -0
- package/dist/app-config/plugins.d.ts +34 -0
- package/dist/app-config/plugins.js +39 -0
- package/dist/app-config/run-lifecycle-invariants.d.ts +18 -9
- package/dist/app-config/run-lifecycle-invariants.js +18 -25
- package/dist/app-config/schema.d.ts +16 -0
- package/dist/app-config/schema.js +2 -0
- package/dist/chat-threads/schema-migrations.js +20 -0
- package/dist/chat-threads/store.js +21 -4
- package/dist/cli/native-dependencies.d.ts +4 -0
- package/dist/cli/native-dependencies.js +15 -0
- package/dist/client/AssistantChat.js +47 -15
- package/dist/client/ConnectBuilderCard.d.ts +3 -1
- package/dist/client/ConnectBuilderCard.js +6 -3
- package/dist/client/EnvironmentBadge.d.ts +1 -1
- package/dist/client/EnvironmentBadge.js +40 -5
- package/dist/client/MultiTabAssistantChat.d.ts +1 -1
- package/dist/client/MultiTabAssistantChat.js +35 -14
- package/dist/client/agent-chat/index.d.ts +1 -1
- package/dist/client/agent-chat/index.js +1 -1
- package/dist/client/agent-page/use-jobs.d.ts +2 -1
- package/dist/client/agent-page/use-jobs.js +3 -5
- package/dist/client/agent-sidebar-state.js +3 -1
- package/dist/client/blocks/library/code-tabs.js +5 -0
- package/dist/client/blocks/library/code.js +1 -0
- package/dist/client/chat/message-components.d.ts +7 -0
- package/dist/client/chat/message-components.js +1 -1
- package/dist/client/chat/run-recovery.js +5 -2
- package/dist/client/chat/tool-call-display.js +9 -3
- package/dist/client/chat-first/apps-rail.js +2 -2
- package/dist/client/chat-model-groups.d.ts +12 -0
- package/dist/client/chat-model-groups.js +3 -2
- package/dist/client/error-format.js +12 -0
- package/dist/client/integrations/GoogleProductLogo.d.ts +5 -0
- package/dist/client/integrations/GoogleProductLogo.js +45 -0
- package/dist/client/integrations/index.d.ts +2 -0
- package/dist/client/integrations/index.js +2 -0
- package/dist/client/integrations/workspace-provider-oauth.d.ts +8 -0
- package/dist/client/integrations/workspace-provider-oauth.js +13 -0
- package/dist/client/navigation/index.d.ts +1 -0
- package/dist/client/navigation/index.js +1 -0
- package/dist/client/resources/mcp-integration-catalog.js +0 -29
- package/dist/client/route-state.js +6 -0
- package/dist/client/settings/SettingsTabsPage.js +10 -6
- package/dist/client/sharing/ShareButton.js +11 -2
- package/dist/client/use-chat-models.d.ts +6 -0
- package/dist/client/use-chat-models.js +10 -0
- package/dist/client/use-db-sync.js +27 -9
- package/dist/client/workspace-app-navigation.d.ts +10 -0
- package/dist/client/workspace-app-navigation.js +38 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/connections/catalog.d.ts +111 -9
- package/dist/connections/catalog.js +86 -30
- package/dist/db/create-get-db.js +18 -0
- package/dist/db/ddl-guard.d.ts +0 -4
- package/dist/db/ddl-guard.js +46 -0
- package/dist/deploy/build.d.ts +32 -0
- package/dist/deploy/build.js +241 -16
- package/dist/deploy/function-bundle.d.ts +45 -0
- package/dist/deploy/function-bundle.js +337 -0
- package/dist/deploy/workspace-deploy.js +19 -1
- package/dist/eject/workspace-connections.js +4 -0
- package/dist/email-catalog/registry.d.ts +4 -0
- package/dist/email-catalog/registry.js +79 -14
- package/dist/extensions/content-patch.js +14 -2
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/plugin.d.ts +10 -0
- package/dist/integrations/plugin.js +40 -6
- package/dist/jobs/actions/run-automation-now.d.ts +2 -1
- package/dist/jobs/actions/run-automation-now.js +11 -5
- package/dist/jobs/frontmatter.js +53 -2
- package/dist/jobs/run-now.d.ts +11 -1
- package/dist/jobs/run-now.js +42 -7
- package/dist/jobs/scheduler.d.ts +1 -0
- package/dist/jobs/scheduler.js +2 -1
- package/dist/localization/core-messages/ar-SA.js +4 -1
- package/dist/localization/core-messages/de-DE.js +4 -1
- package/dist/localization/core-messages/en-US.d.ts +4 -1
- package/dist/localization/core-messages/en-US.js +4 -1
- package/dist/localization/core-messages/es-ES.js +4 -1
- package/dist/localization/core-messages/fr-FR.js +4 -1
- package/dist/localization/core-messages/hi-IN.js +4 -1
- package/dist/localization/core-messages/ja-JP.js +4 -1
- package/dist/localization/core-messages/ko-KR.js +4 -1
- package/dist/localization/core-messages/pt-BR.js +4 -1
- package/dist/localization/core-messages/zh-CN.js +4 -1
- package/dist/localization/core-messages/zh-TW.js +4 -1
- package/dist/localization/core-messages.d.ts +4 -1
- package/dist/localization/default-messages.d.ts +2 -5
- package/dist/localization/default-messages.js +2 -5
- package/dist/mcp/build-server.d.ts +9 -0
- package/dist/mcp/build-server.js +37 -8
- package/dist/mcp-client/oauth-client.d.ts +7 -0
- package/dist/mcp-client/oauth-client.js +12 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/oauth-tokens/index.d.ts +1 -1
- package/dist/oauth-tokens/index.js +1 -1
- package/dist/oauth-tokens/lifecycle.d.ts +11 -0
- package/dist/oauth-tokens/lifecycle.js +17 -0
- package/dist/oauth-tokens/store.js +7 -0
- package/dist/observability/posthog-ai.d.ts +15 -1
- package/dist/observability/posthog-ai.js +24 -12
- package/dist/observability/traces.js +158 -28
- package/dist/onboarding/app-profile.js +2 -2
- package/dist/onboarding/plugin.js +10 -2
- package/dist/onboarding/types.d.ts +2 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.js +2 -0
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/resources/store.js +13 -4
- package/dist/scripts/agent-engines/list-agent-engines.js +29 -3
- package/dist/scripts/db/check-scoping.js +9 -6
- package/dist/secrets/register-framework-secrets.js +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/agent-chat/action-filters-a2a.d.ts +27 -3
- package/dist/server/agent-chat/action-filters-a2a.js +48 -9
- package/dist/server/agent-chat-plugin.js +36 -9
- package/dist/server/agent-discovery.js +43 -20
- package/dist/server/auth.js +18 -16
- package/dist/server/beta-opt-out-html.js +45 -4
- package/dist/server/better-auth-instance.d.ts +2 -1
- package/dist/server/better-auth-instance.js +23 -7
- package/dist/server/builder-browser.d.ts +4 -0
- package/dist/server/builder-browser.js +23 -1
- package/dist/server/builder-design-systems.d.ts +4 -1
- package/dist/server/builder-design-systems.js +87 -6
- package/dist/server/builder-oauth.d.ts +1 -7
- package/dist/server/builder-oauth.js +80 -163
- package/dist/server/core-routes-plugin.d.ts +29 -0
- package/dist/server/core-routes-plugin.js +113 -21
- package/dist/server/default-plugins.d.ts +18 -0
- package/dist/server/default-plugins.js +22 -0
- package/dist/server/edge.d.ts +1 -0
- package/dist/server/edge.js +1 -0
- package/dist/server/framework-request-handler.js +31 -3
- package/dist/server/google-oauth.d.ts +3 -0
- package/dist/server/google-oauth.js +3 -0
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +3 -3
- package/dist/server/onboarding-html.js +37 -3
- package/dist/server/workspace-provider-oauth.d.ts +6 -1
- package/dist/server/workspace-provider-oauth.js +72 -20
- package/dist/shared/agent-sidebar-url.d.ts +1 -0
- package/dist/shared/agent-sidebar-url.js +1 -0
- package/dist/shared/environment-lanes.d.ts +2 -0
- package/dist/shared/environment-lanes.js +3 -1
- package/dist/shared/index.d.ts +3 -1
- package/dist/shared/index.js +3 -1
- package/dist/shared/surface-visibility.d.ts +31 -0
- package/dist/shared/surface-visibility.js +57 -0
- package/dist/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
- package/dist/templates/chat/app/routes.ts +3 -1
- package/dist/templates/chat/vitest.config.ts +8 -0
- package/dist/templates/default/.agents/skills/secrets/SKILL.md +11 -4
- package/dist/templates/default/app/routes.ts +3 -1
- package/dist/templates/headless/.agents/skills/secrets/SKILL.md +11 -4
- package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +11 -4
- package/dist/tracking/registry.d.ts +11 -0
- package/dist/tracking/registry.js +5 -2
- package/dist/triggers/actions.js +10 -3
- package/dist/usage/store.js +23 -3
- package/dist/user-profile/actions/get-auth-methods.js +18 -5
- package/dist/vite/client.js +24 -5
- package/docs/content/actions-access-control.mdx +43 -4
- package/docs/content/actions-defining.mdx +16 -0
- package/docs/content/template-clips-capture-everywhere.mdx +29 -30
- package/docs/content/template-clips-developers.mdx +4 -4
- package/docs/content/template-clips.mdx +3 -1
- package/package.json +3 -3
- package/src/eject/workspace-connections.ts +4 -0
- package/src/templates/chat/.agents/skills/secrets/SKILL.md +11 -4
- package/src/templates/chat/app/routes.ts +3 -1
- package/src/templates/chat/vitest.config.ts +8 -0
- package/src/templates/default/.agents/skills/secrets/SKILL.md +11 -4
- package/src/templates/default/app/routes.ts +3 -1
- package/src/templates/headless/.agents/skills/secrets/SKILL.md +11 -4
- package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +11 -4
- /package/corpus/templates/dispatch/app/routes/{apps.$appId.tsx → apps.$appId.$.tsx} +0 -0
|
@@ -404,11 +404,10 @@ const CAM_ON_KEY = "clips:camera-on";
|
|
|
404
404
|
const MIC_ON_KEY = "clips:mic-on";
|
|
405
405
|
const SYSTEM_AUDIO_KEY = "clips:system-audio";
|
|
406
406
|
const READINESS_REVIEWED_KEY = "clips:readiness-reviewed";
|
|
407
|
-
// The tray's rolling buffer,
|
|
408
|
-
//
|
|
409
|
-
// third-party rewind.ai app, which this app does not integrate with at all.
|
|
407
|
+
// The docs section for the tray's rolling buffer, published under the same
|
|
408
|
+
// Rewind name the settings tab uses.
|
|
410
409
|
const REWIND_DOCS_URL =
|
|
411
|
-
"https://www.agent-native.com/docs/template-clips-capture-everywhere#
|
|
410
|
+
"https://www.agent-native.com/docs/template-clips-capture-everywhere#rewind";
|
|
412
411
|
|
|
413
412
|
// Sensible defaults so the user never has to type a URL on first launch.
|
|
414
413
|
// Dev builds point at the local dev server; production builds point at the
|
|
@@ -2697,6 +2696,7 @@ export function App({
|
|
|
2697
2696
|
let stopPump: (() => void) | null = null;
|
|
2698
2697
|
let fellBackToPump = false;
|
|
2699
2698
|
let stream: MediaStream | null = null;
|
|
2699
|
+
let unlistenUnrendered: (() => void) | null = null;
|
|
2700
2700
|
|
|
2701
2701
|
const startPump = (reason: string) => {
|
|
2702
2702
|
if (cancelled || stopPump || !stream) return;
|
|
@@ -2757,11 +2757,30 @@ export function App({
|
|
|
2757
2757
|
webrtcHandle = null;
|
|
2758
2758
|
startPump(reason);
|
|
2759
2759
|
};
|
|
2760
|
+
// ICE reaching `connected` proves the transport works, nothing more.
|
|
2761
|
+
// WKWebView can refuse to play the received track (no user gesture in
|
|
2762
|
+
// the bubble page, or its window briefly had no on-screen area), and
|
|
2763
|
+
// that failure is invisible from here — so the bubble reports it and
|
|
2764
|
+
// we fall back to the pump. Without this the safety net below only
|
|
2765
|
+
// ever fired on ICE failure, which is not how this breaks in practice.
|
|
2766
|
+
listen("clips:bubble-webrtc-unrendered", (ev) => {
|
|
2767
|
+
startCanvasFallback(
|
|
2768
|
+
`bubble reported no rendered frames ${JSON.stringify(ev.payload)}`,
|
|
2769
|
+
);
|
|
2770
|
+
})
|
|
2771
|
+
.then((u) => {
|
|
2772
|
+
if (cancelled) {
|
|
2773
|
+
u();
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
unlistenUnrendered = u;
|
|
2777
|
+
})
|
|
2778
|
+
.catch(() => {});
|
|
2760
2779
|
webrtcHandle = startBubbleWebrtc({
|
|
2761
2780
|
stream: s,
|
|
2762
2781
|
onConnected: () => {
|
|
2763
2782
|
console.log(
|
|
2764
|
-
"[clips-popover] bubble WebRTC connected —
|
|
2783
|
+
"[clips-popover] bubble WebRTC transport connected — waiting for the bubble to confirm playback",
|
|
2765
2784
|
);
|
|
2766
2785
|
},
|
|
2767
2786
|
onFailure: startCanvasFallback,
|
|
@@ -2806,6 +2825,10 @@ export function App({
|
|
|
2806
2825
|
!!webrtcHandle,
|
|
2807
2826
|
!!stopPump,
|
|
2808
2827
|
);
|
|
2828
|
+
if (unlistenUnrendered) {
|
|
2829
|
+
unlistenUnrendered();
|
|
2830
|
+
unlistenUnrendered = null;
|
|
2831
|
+
}
|
|
2809
2832
|
if (webrtcHandle) {
|
|
2810
2833
|
webrtcHandle.stop();
|
|
2811
2834
|
webrtcHandle = null;
|
|
@@ -3293,11 +3316,14 @@ export function App({
|
|
|
3293
3316
|
// A restart hands off the already-live display stream from the take
|
|
3294
3317
|
// it's replacing (see `discardForRestart`/`preAcquiredDisplayStream`
|
|
3295
3318
|
// below) — it must keep recording the same screen, not re-prompt.
|
|
3296
|
-
if (
|
|
3319
|
+
if (
|
|
3320
|
+
(source === "full-screen" || source === "region") &&
|
|
3321
|
+
!options?.resumeCapture
|
|
3322
|
+
) {
|
|
3297
3323
|
try {
|
|
3298
3324
|
// Must resolve before `recordingFlowActive` flips the toolbar on
|
|
3299
|
-
// below — the toolbar
|
|
3300
|
-
// chosen screen the first time
|
|
3325
|
+
// below — the toolbar and region selector read the pick to place
|
|
3326
|
+
// themselves on the chosen screen the first time they are shown.
|
|
3301
3327
|
await pickFullscreenRecordingDisplay();
|
|
3302
3328
|
} catch (err) {
|
|
3303
3329
|
recordingFlowGateRef.current = false;
|
|
@@ -3835,22 +3861,25 @@ export function App({
|
|
|
3835
3861
|
const showCameraRow = mode !== "screen"; // screen-only has no camera
|
|
3836
3862
|
const showSourceRow = mode !== "camera"; // camera-only has no screen source
|
|
3837
3863
|
|
|
3838
|
-
const pendingUploadBanner =
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3864
|
+
const pendingUploadBanner =
|
|
3865
|
+
authStatus === "authed" ? (
|
|
3866
|
+
recordingStopFinalizing ? (
|
|
3867
|
+
<FinalizingUploadBanner />
|
|
3868
|
+
) : pendingUploads.length > 0 ? (
|
|
3869
|
+
<PendingUploadBanner
|
|
3870
|
+
uploads={pendingUploads}
|
|
3871
|
+
retryingUploadId={retryingUploadId}
|
|
3872
|
+
retryingUploadStatus={retryingUploadStatus}
|
|
3873
|
+
exportingUploadId={exportingUploadId}
|
|
3874
|
+
dismissingUploadId={dismissingUploadId}
|
|
3875
|
+
onExport={exportPendingUpload}
|
|
3876
|
+
onRetry={retryPendingUpload}
|
|
3877
|
+
onDismiss={dismissPendingUpload}
|
|
3878
|
+
onOpenFolder={openPendingUploadFolder}
|
|
3879
|
+
onConnectStorage={(upload) => openVideoStorageSetup(upload.serverUrl)}
|
|
3880
|
+
/>
|
|
3881
|
+
) : null
|
|
3882
|
+
) : null;
|
|
3854
3883
|
|
|
3855
3884
|
async function copyRewindAgentPrompt() {
|
|
3856
3885
|
try {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When the camera bubble may declare its WebRTC video dead.
|
|
3
|
+
*
|
|
4
|
+
* WKWebView refuses to start a MediaStream-backed `<video>` that has had no
|
|
5
|
+
* user gesture in its page, and pauses one again whenever the owning window
|
|
6
|
+
* loses on-screen area. Neither shows up as an ICE failure, so the bubble's
|
|
7
|
+
* transport can be perfectly connected while the user stares at WebKit's
|
|
8
|
+
* start-playback overlay — a black circle with a white triangle. The bubble
|
|
9
|
+
* retries `play()` on a heartbeat, and uses the predicates here to decide when
|
|
10
|
+
* retrying has stopped being plausible and the popover should be asked for the
|
|
11
|
+
* canvas pump instead.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Re-play cadence for the WKWebView pause/block trap — matches `bubble-pump.ts`. */
|
|
15
|
+
export const BUBBLE_PLAY_HEARTBEAT_MS = 2000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How long a WebRTC track may sit without producing frames before we give up
|
|
19
|
+
* on it. Long enough to cover a slow first decode, short enough that the user
|
|
20
|
+
* is not left wondering whether the camera is broken.
|
|
21
|
+
*/
|
|
22
|
+
export const BUBBLE_RENDER_GRACE_MS = 2500;
|
|
23
|
+
|
|
24
|
+
export type BubblePlaybackProbe = {
|
|
25
|
+
/** When the current track arrived, or `null` when there is no track yet. */
|
|
26
|
+
trackArrivedAt: number | null;
|
|
27
|
+
now: number;
|
|
28
|
+
paused: boolean;
|
|
29
|
+
/** `0` until WebKit has decoded a frame, so it is the honest render signal. */
|
|
30
|
+
videoWidth: number;
|
|
31
|
+
alreadyReported: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether WebRTC video is actually on screen. `videoWidth > 0` is the part
|
|
36
|
+
* that matters: a `<video>` reports `paused === false` the moment `play()`
|
|
37
|
+
* resolves, well before any frame is decoded, so "playing" alone is not proof
|
|
38
|
+
* the user can see anything.
|
|
39
|
+
*/
|
|
40
|
+
export function isRenderingWebrtc(
|
|
41
|
+
probe: Pick<BubblePlaybackProbe, "paused" | "videoWidth">,
|
|
42
|
+
): boolean {
|
|
43
|
+
return !probe.paused && probe.videoWidth > 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether a `playing` event may give the WebRTC `<video>` the visible surface.
|
|
48
|
+
*
|
|
49
|
+
* Once the bubble has asked for the canvas pump the popover stops sending, so
|
|
50
|
+
* a later `playing` is the tail of a dead stream. Taking the surface there
|
|
51
|
+
* would park a still frame over the pump's live output. Only a fresh track
|
|
52
|
+
* clears `fallbackRequested` and re-arms this.
|
|
53
|
+
*/
|
|
54
|
+
export function shouldClaimWebrtcPath(input: {
|
|
55
|
+
fallbackRequested: boolean;
|
|
56
|
+
videoWidth: number;
|
|
57
|
+
}): boolean {
|
|
58
|
+
if (input.fallbackRequested) return false;
|
|
59
|
+
return input.videoWidth > 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether the bubble should ask the popover to fall back to the canvas pump. */
|
|
63
|
+
export function shouldReportUnrendered(probe: BubblePlaybackProbe): boolean {
|
|
64
|
+
if (probe.alreadyReported) return false;
|
|
65
|
+
if (probe.trackArrivedAt == null) return false;
|
|
66
|
+
if (isRenderingWebrtc(probe)) return false;
|
|
67
|
+
return probe.now - probe.trackArrivedAt >= BUBBLE_RENDER_GRACE_MS;
|
|
68
|
+
}
|
|
@@ -1672,6 +1672,9 @@ async function saveRecordingTranscript(
|
|
|
1672
1672
|
fullText: text,
|
|
1673
1673
|
segments: transcript.segments,
|
|
1674
1674
|
source: transcript.source ?? "whisper",
|
|
1675
|
+
...(transcript.failureReason
|
|
1676
|
+
? { failureReason: transcript.failureReason }
|
|
1677
|
+
: {}),
|
|
1675
1678
|
}),
|
|
1676
1679
|
signal: AbortSignal.timeout(TRANSCRIPT_SAVE_TIMEOUT_MS),
|
|
1677
1680
|
});
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
/** Grace period after stop for whisper to emit any flushed trailing finals. */
|
|
29
29
|
const WHISPER_STOP_SETTLE_MS = 1500;
|
|
30
30
|
const WEB_SPEECH_STOP_SETTLE_MS = 1200;
|
|
31
|
+
const WEB_SPEECH_RESTART_RETRY_BASE_MS = 400;
|
|
32
|
+
const WEB_SPEECH_MAX_RESTART_ATTEMPTS = 8;
|
|
31
33
|
|
|
32
34
|
function wait(ms: number): Promise<void> {
|
|
33
35
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
@@ -42,6 +44,10 @@ export interface CapturedTranscript {
|
|
|
42
44
|
segments: SourcedTranscriptSegment[];
|
|
43
45
|
/** Source stored with `save-browser-transcript`. */
|
|
44
46
|
source?: "web-speech" | "macos-native" | "whisper";
|
|
47
|
+
/** Set when capture died partway. Text plus a reason is what tells the server
|
|
48
|
+
* this transcript is truncated and has to be re-transcribed in the cloud;
|
|
49
|
+
* omitting it saves a partial capture as a complete one. */
|
|
50
|
+
failureReason?: string;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
export interface TranscriptionCapture {
|
|
@@ -170,15 +176,54 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
|
|
|
170
176
|
const transcriptBuffer = createWebSpeechTranscriptBuffer();
|
|
171
177
|
let stopResolver: ((value: CapturedTranscript) => void) | null = null;
|
|
172
178
|
let settleTimer: ReturnType<typeof window.setTimeout> | null = null;
|
|
179
|
+
let restartTimer: ReturnType<typeof window.setTimeout> | null = null;
|
|
180
|
+
let restartFailures = 0;
|
|
181
|
+
let failureReason: string | null = null;
|
|
173
182
|
|
|
174
183
|
const captured = (): CapturedTranscript => ({
|
|
175
184
|
text: transcriptBuffer.text(),
|
|
176
185
|
segments: [],
|
|
177
186
|
source: "web-speech",
|
|
187
|
+
failureReason: failureReason ?? undefined,
|
|
178
188
|
});
|
|
179
189
|
|
|
190
|
+
const clearRestartTimer = () => {
|
|
191
|
+
if (restartTimer === null) return;
|
|
192
|
+
window.clearTimeout(restartTimer);
|
|
193
|
+
restartTimer = null;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Chrome ends the recognizer on its own roughly every minute, and always
|
|
197
|
+
// throws if start() is called synchronously from inside `onend`. Nothing
|
|
198
|
+
// starts, so no further `onend` arrives — the retry has to live here or the
|
|
199
|
+
// first failure ends transcription for the rest of the recording.
|
|
200
|
+
const scheduleRestart = (delayMs: number) => {
|
|
201
|
+
if (restartTimer !== null) return;
|
|
202
|
+
restartTimer = window.setTimeout(() => {
|
|
203
|
+
restartTimer = null;
|
|
204
|
+
if (disposed || stopped || paused) return;
|
|
205
|
+
try {
|
|
206
|
+
recognition.start();
|
|
207
|
+
restartFailures = 0;
|
|
208
|
+
} catch (err) {
|
|
209
|
+
const attempt = ++restartFailures;
|
|
210
|
+
if (attempt >= WEB_SPEECH_MAX_RESTART_ATTEMPTS) {
|
|
211
|
+
failureReason =
|
|
212
|
+
"Web Speech transcription stopped mid-recording and could not be restarted.";
|
|
213
|
+
console.warn(
|
|
214
|
+
"[clips-recorder] Web Speech transcription restart failed:",
|
|
215
|
+
err,
|
|
216
|
+
);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
scheduleRestart(WEB_SPEECH_RESTART_RETRY_BASE_MS * attempt);
|
|
220
|
+
}
|
|
221
|
+
}, delayMs);
|
|
222
|
+
};
|
|
223
|
+
|
|
180
224
|
const settleStop = () => {
|
|
181
225
|
if (!stopResolver) return;
|
|
226
|
+
clearRestartTimer();
|
|
182
227
|
if (settleTimer) {
|
|
183
228
|
window.clearTimeout(settleTimer);
|
|
184
229
|
settleTimer = null;
|
|
@@ -200,6 +245,12 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
|
|
|
200
245
|
|
|
201
246
|
recognition.onerror = (event) => {
|
|
202
247
|
if (event.error === "no-speech" || event.error === "aborted") return;
|
|
248
|
+
// A non-benign error drops audio until `onend` restarts recognition. That
|
|
249
|
+
// restart usually succeeds, so without recording the gap here the transcript
|
|
250
|
+
// saves as complete and the server never schedules cloud transcription for
|
|
251
|
+
// the missing stretch. Deliberately not cleared by a later successful
|
|
252
|
+
// restart: recovering the engine does not recover the lost speech.
|
|
253
|
+
failureReason ??= `Web Speech transcription dropped audio after a "${event.error}" error.`;
|
|
203
254
|
console.warn(
|
|
204
255
|
"[clips-recorder] Web Speech transcription error:",
|
|
205
256
|
event.error,
|
|
@@ -215,14 +266,7 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
|
|
|
215
266
|
}
|
|
216
267
|
// While paused, keep the committed transcript but don't restart the engine.
|
|
217
268
|
if (paused) return;
|
|
218
|
-
|
|
219
|
-
recognition.start();
|
|
220
|
-
} catch (err) {
|
|
221
|
-
console.warn(
|
|
222
|
-
"[clips-recorder] Web Speech transcription restart failed:",
|
|
223
|
-
err,
|
|
224
|
-
);
|
|
225
|
-
}
|
|
269
|
+
scheduleRestart(0);
|
|
226
270
|
};
|
|
227
271
|
|
|
228
272
|
try {
|
|
@@ -250,6 +294,7 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
|
|
|
250
294
|
async cancel() {
|
|
251
295
|
disposed = true;
|
|
252
296
|
stopped = true;
|
|
297
|
+
clearRestartTimer();
|
|
253
298
|
if (settleTimer) {
|
|
254
299
|
window.clearTimeout(settleTimer);
|
|
255
300
|
settleTimer = null;
|
|
@@ -273,14 +318,18 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
|
|
|
273
318
|
async resume() {
|
|
274
319
|
if (disposed || stopped || !paused) return;
|
|
275
320
|
paused = false;
|
|
321
|
+
restartFailures = 0;
|
|
276
322
|
console.log("[clips-recorder] transcription resumed (web-speech)");
|
|
277
323
|
try {
|
|
278
324
|
recognition.start();
|
|
279
|
-
} catch
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
325
|
+
} catch {
|
|
326
|
+
// `paused` is already false, so the recording is live through the
|
|
327
|
+
// failed start and the retry delay. A later successful retry does not
|
|
328
|
+
// recover that speech, so the gap is recorded durably and the server
|
|
329
|
+
// still schedules cloud transcription.
|
|
330
|
+
failureReason ??=
|
|
331
|
+
"Web Speech transcription dropped audio while resuming after a pause.";
|
|
332
|
+
scheduleRestart(WEB_SPEECH_RESTART_RETRY_BASE_MS);
|
|
284
333
|
}
|
|
285
334
|
},
|
|
286
335
|
async resetTimeline() {
|
|
@@ -289,7 +338,10 @@ async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture
|
|
|
289
338
|
};
|
|
290
339
|
}
|
|
291
340
|
|
|
292
|
-
export const __test = {
|
|
341
|
+
export const __test = {
|
|
342
|
+
createWebSpeechTranscriptBuffer,
|
|
343
|
+
startBrowserTranscriptionCapture,
|
|
344
|
+
};
|
|
293
345
|
|
|
294
346
|
/**
|
|
295
347
|
* Local transcription opens a microphone capture of its own. System-only
|
|
@@ -321,6 +373,7 @@ export async function startTranscriptionCapture(
|
|
|
321
373
|
// asynchronously. Track when those are expected to have landed so a stop
|
|
322
374
|
// soon after a pause waits for them instead of dropping the last words.
|
|
323
375
|
let pauseFinalsSettleUntil = 0;
|
|
376
|
+
let transitionFailure: string | null = null;
|
|
324
377
|
const unlistens: UnlistenFn[] = [];
|
|
325
378
|
|
|
326
379
|
const cleanup = () => {
|
|
@@ -342,6 +395,7 @@ export async function startTranscriptionCapture(
|
|
|
342
395
|
text: transcriptFullText(lines),
|
|
343
396
|
segments: transcriptSegments(lines),
|
|
344
397
|
source: engine,
|
|
398
|
+
failureReason: transitionFailure ?? undefined,
|
|
345
399
|
});
|
|
346
400
|
|
|
347
401
|
let engine: TranscriptionEngine;
|
|
@@ -407,6 +461,7 @@ export async function startTranscriptionCapture(
|
|
|
407
461
|
// reset it) so the next pause/resume toggle retries and converges, and
|
|
408
462
|
// return early so we don't busy-loop re-applying a persistently failing
|
|
409
463
|
// transition. `paused` still reflects the real engine state.
|
|
464
|
+
transitionFailure = `Local transcription ${desiredPaused ? "pause" : "resume"} failed; engine still ${paused ? "paused" : "live"}.`;
|
|
410
465
|
console.warn(
|
|
411
466
|
`[clips-recorder] transcription ${desiredPaused ? "pause" : "resume"} failed; engine still ${paused ? "paused" : "live"}:`,
|
|
412
467
|
err,
|
|
@@ -416,6 +471,9 @@ export async function startTranscriptionCapture(
|
|
|
416
471
|
} finally {
|
|
417
472
|
transitioning = false;
|
|
418
473
|
}
|
|
474
|
+
// Reached only when the transition landed, so a failure the next toggle
|
|
475
|
+
// converged on is no longer a reason to re-transcribe in the cloud.
|
|
476
|
+
transitionFailure = null;
|
|
419
477
|
// Re-apply in case the desired state changed mid-transition.
|
|
420
478
|
void applyAudioState();
|
|
421
479
|
};
|
|
@@ -96,8 +96,9 @@ async function runCheck() {
|
|
|
96
96
|
// made "Restart to update" land on a binary that was already out of date and
|
|
97
97
|
// immediately ask to restart again — every check runs against the *running*
|
|
98
98
|
// version, so it re-offers the staged version until a newer one ships.
|
|
99
|
-
const
|
|
100
|
-
cachedStatus.state === "downloaded" ? cachedStatus
|
|
99
|
+
const stagedStatus =
|
|
100
|
+
cachedStatus.state === "downloaded" ? cachedStatus : null;
|
|
101
|
+
const staged = stagedStatus?.version ?? null;
|
|
101
102
|
|
|
102
103
|
lastCheckStartedAt = Date.now();
|
|
103
104
|
checkInFlight = (async () => {
|
|
@@ -114,7 +115,11 @@ async function runCheck() {
|
|
|
114
115
|
}
|
|
115
116
|
} catch (err) {
|
|
116
117
|
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
-
|
|
118
|
+
// A staged bundle is already on disk and `pendingUpdate` still points at
|
|
119
|
+
// it. Reporting the check failure here would replace the Restart banner
|
|
120
|
+
// with an error card that has no way back to it, and would make the next
|
|
121
|
+
// successful check re-download the same version from scratch.
|
|
122
|
+
setStatus(stagedStatus ?? { state: "error", message });
|
|
118
123
|
} finally {
|
|
119
124
|
checkInFlight = null;
|
|
120
125
|
}
|
|
@@ -3,6 +3,14 @@ import { invoke } from "@tauri-apps/api/core";
|
|
|
3
3
|
import { emit, listen } from "@tauri-apps/api/event";
|
|
4
4
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
5
5
|
import { useEffect, useRef, useState } from "react";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
BUBBLE_PLAY_HEARTBEAT_MS,
|
|
9
|
+
BUBBLE_RENDER_GRACE_MS,
|
|
10
|
+
shouldClaimWebrtcPath,
|
|
11
|
+
shouldReportUnrendered,
|
|
12
|
+
} from "../lib/bubble-playback";
|
|
13
|
+
|
|
6
14
|
type BubbleSize = "small" | "medium";
|
|
7
15
|
|
|
8
16
|
/**
|
|
@@ -41,10 +49,13 @@ type BubbleSize = "small" | "medium";
|
|
|
41
49
|
* feature degrades gracefully if the WebRTC handshake fails.
|
|
42
50
|
*
|
|
43
51
|
* The bubble always sets up BOTH receivers on mount. Whichever path
|
|
44
|
-
* delivers video first wins — the other stays passive.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
52
|
+
* delivers video first wins — the other stays passive. The popover
|
|
53
|
+
* starts the canvas pump instead when the WebRTC handshake fails (ICE
|
|
54
|
+
* timeout / failed state) OR when this bubble reports that a connected
|
|
55
|
+
* track never rendered (`clips:bubble-webrtc-unrendered`; see the
|
|
56
|
+
* playback watchdog below). Both signals are needed — ICE reaching
|
|
57
|
+
* `connected` says the transport works, never that WebKit painted a
|
|
58
|
+
* frame.
|
|
48
59
|
*
|
|
49
60
|
* # Hover controls (Loom-style)
|
|
50
61
|
*
|
|
@@ -59,16 +70,29 @@ type BubbleSize = "small" | "medium";
|
|
|
59
70
|
export function Bubble() {
|
|
60
71
|
// Dual-path rendering: <video> for WebRTC, <canvas> for the legacy
|
|
61
72
|
// JPEG stream. CSS stacks them in the same circle — whichever has a
|
|
62
|
-
// stream / frames visible fills the same space.
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// frames start arriving we flip to "canvas".
|
|
73
|
+
// stream / frames visible fills the same space. `data-path` on the root
|
|
74
|
+
// hides the loser. It flips to "webrtc" only once the <video> actually
|
|
75
|
+
// renders a frame, and to "canvas" on the first JPEG frame.
|
|
66
76
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
67
77
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
68
78
|
const firstFrameAtRef = useRef<number | null>(null);
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
79
|
+
const firstTrackAtRef = useRef<number | null>(null);
|
|
80
|
+
// When the current WebRTC track arrived, so the playback watchdog can tell
|
|
81
|
+
// "no track yet" from "track arrived and WebKit still isn't rendering it".
|
|
82
|
+
const trackArrivedAtRef = useRef<number | null>(null);
|
|
83
|
+
// Assigned by the playback watchdog effect; called from `ontrack`, which
|
|
84
|
+
// lives in a different effect and must not own the retry policy.
|
|
85
|
+
const attemptPlayRef = useRef<() => void>(() => {});
|
|
86
|
+
// True once we've told the popover this track is dead and asked for the
|
|
87
|
+
// canvas pump. Shared across both effects: the watchdog sets it, and only a
|
|
88
|
+
// fresh track clears it, so the surface can't be handed back to a stream the
|
|
89
|
+
// popover has already stopped sending.
|
|
90
|
+
const fallbackRequestedRef = useRef(false);
|
|
91
|
+
// Which transport last put pixels on screen. Starts as "none"; the
|
|
92
|
+
// playback watchdog flips it to "webrtc" on a `playing` event with real
|
|
93
|
+
// frame dimensions, and the canvas sink flips it on the first JPEG frame.
|
|
94
|
+
// A track merely arriving is NOT enough — claiming "webrtc" there hid the
|
|
95
|
+
// canvas fallback exactly when WebKit had refused to play the video.
|
|
72
96
|
const [activePath, setActivePath] = useState<"none" | "webrtc" | "canvas">(
|
|
73
97
|
"none",
|
|
74
98
|
);
|
|
@@ -153,6 +177,121 @@ export function Bubble() {
|
|
|
153
177
|
}
|
|
154
178
|
};
|
|
155
179
|
|
|
180
|
+
// ---- playback watchdog --------------------------------------------------
|
|
181
|
+
// WKWebView will not start a MediaStream-backed `<video>` that has had no
|
|
182
|
+
// user gesture in the page, and pauses one again whenever its window loses
|
|
183
|
+
// on-screen area. One `play()` at `ontrack` time therefore leaves the
|
|
184
|
+
// element parked behind WebKit's start-playback overlay — the black circle
|
|
185
|
+
// with a white triangle users report — and nothing ever retries it. The
|
|
186
|
+
// popover's offscreen pump video already defends itself the same way (see
|
|
187
|
+
// `bubble-pump.ts`); this is the bubble's copy of that defense.
|
|
188
|
+
//
|
|
189
|
+
// It also owns the `activePath` transition: only a `playing` event with real
|
|
190
|
+
// frame dimensions proves WebRTC is on screen. Flipping on `ontrack` instead
|
|
191
|
+
// hid the canvas fallback exactly when it was needed.
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const video = videoRef.current;
|
|
194
|
+
if (!video) return;
|
|
195
|
+
let stopped = false;
|
|
196
|
+
let lastFailure: string | null = null;
|
|
197
|
+
|
|
198
|
+
const attemptPlay = () => {
|
|
199
|
+
if (stopped) return;
|
|
200
|
+
if (!video.srcObject || !video.paused) return;
|
|
201
|
+
video.play().catch((err: unknown) => {
|
|
202
|
+
// Name and message, not the raw object: `console.warn` on a
|
|
203
|
+
// DOMException logs only its stack, which cannot tell NotAllowedError
|
|
204
|
+
// (playback blocked) from AbortError (load interrupted) — the two
|
|
205
|
+
// point at different bugs. Log each distinct reason once so the retry
|
|
206
|
+
// loop cannot bury the rest of the log.
|
|
207
|
+
const name = (err as { name?: string } | null)?.name ?? "unknown";
|
|
208
|
+
const message =
|
|
209
|
+
(err as { message?: string } | null)?.message ?? String(err);
|
|
210
|
+
const signature = `${name}: ${message}`;
|
|
211
|
+
if (lastFailure === signature) return;
|
|
212
|
+
lastFailure = signature;
|
|
213
|
+
console.warn("[bubble] video.play() rejected —", signature);
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
attemptPlayRef.current = attemptPlay;
|
|
217
|
+
|
|
218
|
+
const maybeClaimWebrtcPath = () => {
|
|
219
|
+
const claims = shouldClaimWebrtcPath({
|
|
220
|
+
fallbackRequested: fallbackRequestedRef.current,
|
|
221
|
+
videoWidth: video.videoWidth,
|
|
222
|
+
});
|
|
223
|
+
if (!claims) return;
|
|
224
|
+
setActivePath((current) => {
|
|
225
|
+
if (current !== "webrtc") {
|
|
226
|
+
console.log(
|
|
227
|
+
"[bubble] webrtc playback started %dx%d",
|
|
228
|
+
video.videoWidth,
|
|
229
|
+
video.videoHeight,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return "webrtc";
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const onPlaying = () => {
|
|
237
|
+
if (stopped) return;
|
|
238
|
+
lastFailure = null;
|
|
239
|
+
maybeClaimWebrtcPath();
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
video.addEventListener("playing", onPlaying);
|
|
243
|
+
video.addEventListener("loadedmetadata", attemptPlay);
|
|
244
|
+
video.addEventListener("canplay", attemptPlay);
|
|
245
|
+
video.addEventListener("pause", attemptPlay);
|
|
246
|
+
document.addEventListener("visibilitychange", attemptPlay);
|
|
247
|
+
|
|
248
|
+
// Heartbeat: covers the resume cases no media event fires for (window
|
|
249
|
+
// shrunk to no visible area and back, a gesture landing on a control
|
|
250
|
+
// rather than the video). `play()` on an already-playing element is a
|
|
251
|
+
// no-op, so this is cheap. It doubles as the unrendered detector — if a
|
|
252
|
+
// track has been sitting there for BUBBLE_RENDER_GRACE_MS with no frames,
|
|
253
|
+
// tell the popover so it can start the proven canvas pump instead of
|
|
254
|
+
// leaving the user with a dead circle.
|
|
255
|
+
const heartbeat = setInterval(() => {
|
|
256
|
+
if (stopped) return;
|
|
257
|
+
attemptPlay();
|
|
258
|
+
maybeClaimWebrtcPath();
|
|
259
|
+
const unrendered = shouldReportUnrendered({
|
|
260
|
+
trackArrivedAt: trackArrivedAtRef.current,
|
|
261
|
+
now: Date.now(),
|
|
262
|
+
paused: video.paused,
|
|
263
|
+
videoWidth: video.videoWidth,
|
|
264
|
+
alreadyReported: fallbackRequestedRef.current,
|
|
265
|
+
});
|
|
266
|
+
if (!unrendered) return;
|
|
267
|
+
fallbackRequestedRef.current = true;
|
|
268
|
+
console.warn(
|
|
269
|
+
"[bubble] webrtc track is not rendering after %dms (paused=%o readyState=%d videoWidth=%d) — asking popover for the canvas pump",
|
|
270
|
+
BUBBLE_RENDER_GRACE_MS,
|
|
271
|
+
video.paused,
|
|
272
|
+
video.readyState,
|
|
273
|
+
video.videoWidth,
|
|
274
|
+
);
|
|
275
|
+
emit("clips:bubble-webrtc-unrendered", {
|
|
276
|
+
paused: video.paused,
|
|
277
|
+
readyState: video.readyState,
|
|
278
|
+
videoWidth: video.videoWidth,
|
|
279
|
+
lastPlayFailure: lastFailure,
|
|
280
|
+
}).catch(() => {});
|
|
281
|
+
}, BUBBLE_PLAY_HEARTBEAT_MS);
|
|
282
|
+
|
|
283
|
+
return () => {
|
|
284
|
+
stopped = true;
|
|
285
|
+
clearInterval(heartbeat);
|
|
286
|
+
video.removeEventListener("playing", onPlaying);
|
|
287
|
+
video.removeEventListener("loadedmetadata", attemptPlay);
|
|
288
|
+
video.removeEventListener("canplay", attemptPlay);
|
|
289
|
+
video.removeEventListener("pause", attemptPlay);
|
|
290
|
+
document.removeEventListener("visibilitychange", attemptPlay);
|
|
291
|
+
attemptPlayRef.current = () => {};
|
|
292
|
+
};
|
|
293
|
+
}, []);
|
|
294
|
+
|
|
156
295
|
// ---- WebRTC receiver ----------------------------------------------------
|
|
157
296
|
// Sets up a fresh RTCPeerConnection on mount, emits `bubble-ready`
|
|
158
297
|
// so the popover knows to start the handshake, and renegotiates on
|
|
@@ -187,6 +326,7 @@ export function Bubble() {
|
|
|
187
326
|
};
|
|
188
327
|
|
|
189
328
|
function teardownPeer() {
|
|
329
|
+
trackArrivedAtRef.current = null;
|
|
190
330
|
if (pc) {
|
|
191
331
|
// Detach the incoming video <video>.srcObject BEFORE closing the
|
|
192
332
|
// peer — otherwise WKWebView's media pipeline keeps the decoder
|
|
@@ -272,17 +412,19 @@ export function Bubble() {
|
|
|
272
412
|
const incomingStream = ev.streams[0];
|
|
273
413
|
if (!incomingStream) return;
|
|
274
414
|
videoEl.srcObject = incomingStream;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
415
|
+
// Do NOT flip `activePath` here. A track arriving proves the
|
|
416
|
+
// transport works, not that WebKit will render it — see the
|
|
417
|
+
// playback watchdog below, which owns that transition and the
|
|
418
|
+
// fallback when playback never starts.
|
|
419
|
+
trackArrivedAtRef.current = Date.now();
|
|
420
|
+
// A fresh track is a fresh chance: re-arm the watchdog so it can both
|
|
421
|
+
// grant this one the surface and report it dead in turn.
|
|
422
|
+
fallbackRequestedRef.current = false;
|
|
423
|
+
attemptPlayRef.current();
|
|
424
|
+
if (firstTrackAtRef.current == null) {
|
|
425
|
+
firstTrackAtRef.current = Date.now();
|
|
283
426
|
console.log("[bubble] first webrtc track received");
|
|
284
427
|
}
|
|
285
|
-
setActivePath("webrtc");
|
|
286
428
|
};
|
|
287
429
|
|
|
288
430
|
try {
|