@agent-native/core 0.103.1 → 0.105.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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +61 -0
- package/corpus/core/package.json +9 -2
- package/corpus/core/src/agent/engine/credential-errors.ts +2 -2
- package/corpus/core/src/agent/production-agent.ts +30 -0
- package/corpus/core/src/agent/run-store.ts +12 -2
- package/corpus/core/src/cli/create.ts +1 -0
- package/corpus/core/src/cli/skills-content/visual-plan-skill.ts +30 -11
- package/corpus/core/src/cli/templates-meta.ts +4 -3
- package/corpus/core/src/client/AgentAskPopover.tsx +89 -0
- package/corpus/core/src/client/AgentChatHome.tsx +4 -0
- package/corpus/core/src/client/AgentPanel.tsx +321 -223
- package/corpus/core/src/client/AssistantChat.tsx +6 -10
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +53 -372
- package/corpus/core/src/client/agent-chat-adapter.ts +13 -1
- package/corpus/core/src/client/agent-page/AgentContextTab.tsx +133 -149
- package/corpus/core/src/client/agent-page/AgentEmptyState.tsx +44 -0
- package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +153 -82
- package/corpus/core/src/client/agent-page/AgentTabFrame.tsx +39 -0
- package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +368 -356
- package/corpus/core/src/client/agent-page/types.ts +1 -1
- package/corpus/core/src/client/analytics.ts +51 -5
- package/corpus/core/src/client/chat/run-recovery.tsx +12 -4
- package/corpus/core/src/client/composer/PromptComposer.tsx +5 -15
- package/corpus/core/src/client/composer/TiptapComposer.tsx +32 -3
- package/corpus/core/src/client/dynamic-suggestions.ts +3 -3
- package/corpus/core/src/client/index.ts +16 -1
- package/corpus/core/src/client/resources/ResourceTree.tsx +249 -38
- package/corpus/core/src/client/resources/ResourcesPanel.tsx +307 -140
- package/corpus/core/src/client/session-replay.ts +158 -5
- package/corpus/core/src/client/settings/AutomationsSection.tsx +30 -10
- package/corpus/core/src/client/settings/SettingsPanel.tsx +3 -2
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +2 -0
- package/corpus/core/src/client/settings/agent-settings-search.ts +2 -2
- package/corpus/core/src/client/use-chat-threads.ts +2 -0
- package/corpus/core/src/connections/catalog.ts +49 -7
- package/corpus/core/src/db/index.ts +6 -1
- package/corpus/core/src/deploy/build.ts +23 -0
- package/corpus/core/src/extensions/actions.ts +106 -16
- package/corpus/core/src/extensions/content-patch.ts +22 -0
- package/corpus/core/src/extensions/routes.ts +18 -0
- package/corpus/core/src/extensions/store.ts +8 -0
- package/corpus/core/src/index.ts +3 -0
- package/corpus/core/src/ingestion/docx.ts +158 -0
- package/corpus/core/src/ingestion/figma-node-to-html.ts +1860 -0
- package/corpus/core/src/ingestion/figma.ts +548 -0
- package/corpus/core/src/ingestion/index.ts +107 -0
- package/corpus/core/src/ingestion/media.ts +264 -0
- package/corpus/core/src/ingestion/notion.ts +121 -0
- package/corpus/core/src/ingestion/office.ts +214 -0
- package/corpus/core/src/ingestion/orchestration.ts +160 -0
- package/corpus/core/src/ingestion/pptx.ts +333 -0
- package/corpus/core/src/ingestion/selection.ts +81 -0
- package/corpus/core/src/ingestion/website.ts +151 -0
- package/corpus/core/src/integrations/plugin.ts +45 -37
- package/corpus/core/src/localization/default-messages.ts +35 -12
- package/corpus/core/src/observability/hosted-model-experiment.ts +118 -0
- package/corpus/core/src/observability/traces.ts +5 -3
- package/corpus/core/src/provider-api/index.ts +452 -8
- package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +18 -1
- package/corpus/core/src/search-utils/index.ts +112 -0
- package/corpus/core/src/secrets/register-framework-secrets.ts +61 -0
- package/corpus/core/src/server/action-discovery.ts +33 -8
- package/corpus/core/src/server/agent-chat/framework-prompts.ts +2 -2
- package/corpus/core/src/server/agent-chat/prompt-resources.ts +144 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +6 -0
- package/corpus/core/src/server/core-routes-plugin.ts +12 -0
- package/corpus/core/src/server/index.ts +30 -0
- package/corpus/core/src/server/workspace-provider-oauth.ts +583 -0
- package/corpus/core/src/styles/agent-native.css +9 -0
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +3 -3
- package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +14 -5
- package/corpus/core/src/templates/workspace-root/.env.example +12 -0
- package/corpus/core/src/workspace-connections/index.ts +6 -0
- package/corpus/core/src/workspace-connections/lifecycle.ts +43 -0
- package/corpus/core/src/workspace-connections/store.ts +16 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +46 -3
- package/corpus/templates/analytics/AGENTS.md +9 -0
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +120 -6
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +92 -30
- package/corpus/templates/analytics/actions/update-dashboard.ts +5 -5
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +313 -220
- package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +2 -2
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +9 -6
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +90 -64
- package/corpus/templates/analytics/app/i18n-data.ts +3 -3
- package/corpus/templates/analytics/app/lib/dashboard-list-loading.ts +25 -0
- package/corpus/templates/analytics/app/lib/item-popularity.ts +13 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/pivot.ts +8 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +1 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +15 -3
- package/corpus/templates/analytics/changelog/2026-07-16-dashboard-filter-defaults-can-now-be-changed-reliably-withou.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-16-dashboard-lists-load-in-one-stable-order-without-popping-bet.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-16-fixed-dashboard-filter-dropdown-text-alignment.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-16-recurring-signed-in-users-can-now-be-viewed-as-weekly-bars-b.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-16-the-analytics-agent-can-now-duplicate-a-chart-and-place-the-.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +41 -1
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +1 -1
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +47 -1
- package/corpus/templates/analytics/server/lib/session-replay.ts +68 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/analytics/server/routes/sessions/lookup.get.ts +47 -0
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +16 -9
- package/corpus/templates/assets/.agents/skills/creative-context/SKILL.md +86 -0
- package/corpus/templates/assets/AGENTS.md +10 -0
- package/corpus/templates/assets/actions/create-generation-session.ts +59 -1
- package/corpus/templates/assets/actions/edit-image.ts +2 -0
- package/corpus/templates/assets/actions/generate-asset.ts +52 -0
- package/corpus/templates/assets/actions/generate-image-batch.ts +55 -0
- package/corpus/templates/assets/actions/generate-image.ts +219 -1
- package/corpus/templates/assets/actions/open-asset-picker.ts +14 -0
- package/corpus/templates/assets/actions/refine-image.ts +3 -0
- package/corpus/templates/assets/actions/restyle-image.ts +2 -0
- package/corpus/templates/assets/agent-native.app-skill.json +4 -0
- package/corpus/templates/assets/app/components/layout/Layout.tsx +2 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/assets/app/i18n-data.ts +29 -13
- package/corpus/templates/assets/app/routes/agent.tsx +7 -1
- package/corpus/templates/assets/app/routes/library.tsx +9 -0
- package/corpus/templates/assets/app/routes/settings.tsx +3 -0
- package/corpus/templates/assets/changelog/2026-07-16-creative-context-library.md +6 -0
- package/corpus/templates/assets/package.json +1 -0
- package/corpus/templates/assets/server/lib/image-processing.ts +1 -22
- package/corpus/templates/assets/server/plugins/creative-context.ts +205 -0
- package/corpus/templates/brain/app/i18n-data.ts +3 -3
- package/corpus/templates/brain/app/lib/brain.ts +1 -1
- package/corpus/templates/brain/server/lib/search.ts +17 -67
- package/corpus/templates/calendar/app/i18n-data.ts +3 -3
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +19 -18
- package/corpus/templates/chat/app/i18n/en-US.ts +3 -3
- package/corpus/templates/chat/app/lib/agent-page.tsx +10 -1
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +1 -1
- package/corpus/templates/clips/AGENTS.md +4 -0
- package/corpus/templates/clips/actions/cleanup-transcript.ts +35 -4
- package/corpus/templates/clips/actions/create-meeting.ts +6 -2
- package/corpus/templates/clips/actions/create-organization.ts +3 -2
- package/corpus/templates/clips/actions/create-recording.ts +4 -1
- package/corpus/templates/clips/actions/finalize-recording.ts +44 -7
- package/corpus/templates/clips/actions/import-loom-recording.ts +4 -1
- package/corpus/templates/clips/actions/lib/create-recording-schema.ts +2 -2
- package/corpus/templates/clips/actions/list-meetings.ts +23 -5
- package/corpus/templates/clips/actions/list-organization-state.ts +1 -1
- package/corpus/templates/clips/actions/remove-silences.ts +10 -1
- package/corpus/templates/clips/actions/request-transcript.ts +53 -11
- package/corpus/templates/clips/actions/search-recordings-utils.ts +7 -0
- package/corpus/templates/clips/actions/search-recordings.ts +5 -8
- package/corpus/templates/clips/actions/set-organization-branding.ts +2 -2
- package/corpus/templates/clips/actions/stitch-recordings.ts +10 -5
- package/corpus/templates/clips/actions/update-ai-request-status.ts +38 -0
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +3 -4
- package/corpus/templates/clips/app/components/editor/editor-selection.ts +19 -0
- package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +10 -10
- package/corpus/templates/clips/app/components/library/bulk-action-toolbar.tsx +1 -2
- package/corpus/templates/clips/app/components/library/library-grid.tsx +101 -93
- package/corpus/templates/clips/app/components/player/video-player.tsx +11 -0
- package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +53 -1
- package/corpus/templates/clips/app/i18n/ar-SA.ts +7 -1
- package/corpus/templates/clips/app/i18n/de-DE.ts +7 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +10 -4
- package/corpus/templates/clips/app/i18n/es-ES.ts +7 -1
- package/corpus/templates/clips/app/i18n/fr-FR.ts +7 -1
- package/corpus/templates/clips/app/i18n/hi-IN.ts +7 -1
- package/corpus/templates/clips/app/i18n/ja-JP.ts +7 -1
- package/corpus/templates/clips/app/i18n/ko-KR.ts +7 -1
- package/corpus/templates/clips/app/i18n/pt-BR.ts +7 -1
- package/corpus/templates/clips/app/i18n/zh-CN.ts +6 -1
- package/corpus/templates/clips/app/i18n/zh-TW.ts +6 -1
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +5 -9
- package/corpus/templates/clips/app/routes/_app.settings.organization.tsx +6 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +129 -27
- package/corpus/templates/clips/app/routes/record.tsx +23 -2
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -2
- package/corpus/templates/clips/app/routes.ts +3 -1
- package/corpus/templates/clips/changelog/2026-07-16-authorized-viewers-can-open-shared-private-clips-without-a-m.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-camera-bubble-hover-controls-appear-above-the-face.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-chrome-extension-recovers-stale-recorder-state.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-clips-agent-chat-no-longer-shows-a-separate-workspace-header.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-clips-search-now-matches-recording-content-without-requiring.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-declined-meetings-no-longer-show-desktop-reminders.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-failed-desktop-saves-now-show-their-local-recovery-copy.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-long-desktop-clips-now-preserve-complete-playback-synced-aud.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-long-recording-lists-keep-stitch-and-library-actions-visible.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-organization-admins-can-choose-new-recording-visibility.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-remove-silences-now-shows-queued-active-completed-and-failed.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-settings-no-longer-repeats-the-builder-connection-status-in-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-16-transcripts-now-appear-from-native-browser-speech-sooner-wit.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +73 -1
- package/corpus/templates/clips/chrome-extension/src/native-recording-state.ts +32 -0
- package/corpus/templates/clips/chrome-extension/src/native-transcription.ts +248 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +106 -0
- package/corpus/templates/clips/desktop/src/app.tsx +66 -28
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +335 -285
- package/corpus/templates/clips/desktop/src/lib/single-flight.ts +7 -0
- package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +51 -0
- package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +2 -2
- package/corpus/templates/clips/desktop/src/overlays/finalizing.tsx +10 -2
- package/corpus/templates/clips/desktop/src/styles.css +13 -11
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +8 -9
- package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +88 -13
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +13 -2
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +241 -6
- package/corpus/templates/clips/server/db/schema.ts +2 -2
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +64 -67
- package/corpus/templates/clips/server/lib/calendar-event-classification.ts +19 -0
- package/corpus/templates/clips/server/lib/google-calendar-client.ts +1 -0
- package/corpus/templates/clips/server/lib/recordings.ts +40 -1
- package/corpus/templates/clips/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/clips/server/plugins/db.ts +2 -2
- package/corpus/templates/clips/server/routes/api/auth/google-calendar.get.ts +1 -1
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +14 -5
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +51 -1
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +1 -64
- package/corpus/templates/clips/shared/media-worker-contract.ts +32 -15
- package/corpus/templates/content/.agents/skills/content/SKILL.md +16 -0
- package/corpus/templates/content/.agents/skills/creative-context/SKILL.md +85 -0
- package/corpus/templates/content/.agents/skills/notion-integration/SKILL.md +8 -0
- package/corpus/templates/content/AGENTS.md +9 -0
- package/corpus/templates/content/actions/create-document.ts +94 -0
- package/corpus/templates/content/actions/edit-document.ts +144 -4
- package/corpus/templates/content/actions/update-document.ts +138 -0
- package/corpus/templates/content/agent-native.app-skill.json +4 -0
- package/corpus/templates/content/app/components/layout/Layout.tsx +2 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/content/app/i18n-data.ts +13 -4
- package/corpus/templates/content/app/routes/_app.agent.tsx +2 -1
- package/corpus/templates/content/app/routes/_app.settings.tsx +3 -0
- package/corpus/templates/content/changelog/2026-07-16-creative-context-library.md +6 -0
- package/corpus/templates/content/package.json +1 -0
- package/corpus/templates/content/server/plugins/creative-context.ts +31 -0
- package/corpus/templates/design/.agents/skills/creative-context/SKILL.md +116 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +16 -0
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
- package/corpus/templates/design/AGENTS.md +6 -0
- package/corpus/templates/design/actions/clone-creative-context-design.ts +176 -0
- package/corpus/templates/design/actions/edit-design.ts +128 -0
- package/corpus/templates/design/actions/generate-design.ts +278 -14
- package/corpus/templates/design/actions/generate-screens.ts +42 -1
- package/corpus/templates/design/agent-native.app-skill.json +4 -0
- package/corpus/templates/design/app/components/layout/Layout.tsx +7 -3
- package/corpus/templates/design/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/design/app/i18n-data.ts +10 -4
- package/corpus/templates/design/app/routes/agent.tsx +2 -1
- package/corpus/templates/design/app/routes/settings.tsx +3 -0
- package/corpus/templates/design/changelog/2026-07-16-creative-context-library.md +6 -0
- package/corpus/templates/design/changelog/2026-07-16-design-agent-chat-no-longer-shows-a-separate-workspace-header.md +6 -0
- package/corpus/templates/design/package.json +1 -0
- package/corpus/templates/design/server/lib/figma-design-context.ts +5 -335
- package/corpus/templates/design/server/lib/figma-node-to-html.ts +1 -1859
- package/corpus/templates/design/server/lib/import-design-files.ts +6 -8
- package/corpus/templates/design/server/plugins/creative-context.ts +31 -0
- package/corpus/templates/design/shared/generation-session.ts +7 -0
- package/corpus/templates/dispatch/app/i18n-data.ts +3 -3
- package/corpus/templates/forms/app/i18n/en-US.ts +3 -3
- package/corpus/templates/macros/app/i18n/en-US.ts +3 -3
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +35 -5
- package/corpus/templates/mail/app/i18n/en-US.ts +3 -3
- package/corpus/templates/mail/changelog/2026-07-16-mail-previews-now-expand-to-show-the-full-message-without-an.md +6 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +30 -11
- package/corpus/templates/plan/AGENTS.md +21 -1
- package/corpus/templates/plan/actions/get-visual-plan.ts +1 -1
- package/corpus/templates/plan/actions/patch-visual-plan-source.ts +161 -0
- package/corpus/templates/plan/actions/read-visual-plan-source.ts +1 -1
- package/corpus/templates/plan/actions/update-visual-plan.ts +126 -10
- package/corpus/templates/plan/app/hooks/use-plans.ts +2 -0
- package/corpus/templates/plan/app/i18n/en-US.ts +3 -3
- package/corpus/templates/plan/app/pages/PlansPage.tsx +65 -62
- package/corpus/templates/plan/changelog/2026-07-16-local-plan-previews-now-load-annotations-containing-html-lik.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-16-plan-agents-now-prevent-stale-edits-from-overwriting-newer-p.md +6 -0
- package/corpus/templates/plan/server/plan-mdx.ts +75 -2
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +21 -4
- package/corpus/templates/slides/.agents/skills/creative-context/SKILL.md +122 -0
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +11 -0
- package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +12 -3
- package/corpus/templates/slides/AGENTS.md +6 -0
- package/corpus/templates/slides/actions/add-slide.ts +230 -14
- package/corpus/templates/slides/actions/clone-context-slide.ts +302 -0
- package/corpus/templates/slides/actions/create-deck.ts +130 -0
- package/corpus/templates/slides/actions/extract-pdf.ts +6 -2
- package/corpus/templates/slides/actions/import-file.ts +5 -4
- package/corpus/templates/slides/actions/patch-deck.ts +200 -10
- package/corpus/templates/slides/actions/update-slide.ts +208 -15
- package/corpus/templates/slides/app/components/layout/Layout.tsx +2 -0
- package/corpus/templates/slides/app/components/visual-editor/CanvasCommentPins.tsx +16 -1
- package/corpus/templates/slides/app/i18n/ar-SA.ts +3 -0
- package/corpus/templates/slides/app/i18n/de-DE.ts +3 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +6 -3
- package/corpus/templates/slides/app/i18n/es-ES.ts +3 -0
- package/corpus/templates/slides/app/i18n/fr-FR.ts +3 -0
- package/corpus/templates/slides/app/i18n/hi-IN.ts +3 -0
- package/corpus/templates/slides/app/i18n/ja-JP.ts +3 -0
- package/corpus/templates/slides/app/i18n/ko-KR.ts +3 -0
- package/corpus/templates/slides/app/i18n/pt-BR.ts +3 -0
- package/corpus/templates/slides/app/i18n/zh-CN.ts +3 -0
- package/corpus/templates/slides/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/slides/app/lib/validate-native-slide-html.ts +176 -0
- package/corpus/templates/slides/app/routes/agent.tsx +7 -1
- package/corpus/templates/slides/app/routes/settings.tsx +3 -0
- package/corpus/templates/slides/changelog/2026-07-16-creative-context-library.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-16-google-slides-native-cloning.md +6 -0
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/server/handlers/import/docx-parser.ts +4 -85
- package/corpus/templates/slides/server/handlers/import/pptx-parser.ts +7 -421
- package/corpus/templates/slides/server/plugins/creative-context.ts +166 -0
- package/dist/agent/engine/credential-errors.d.ts +1 -1
- package/dist/agent/engine/credential-errors.d.ts.map +1 -1
- package/dist/agent/engine/credential-errors.js +2 -2
- package/dist/agent/engine/credential-errors.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +25 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +12 -2
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +1 -0
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
- package/dist/cli/skills-content/visual-plan-skill.d.ts.map +1 -1
- package/dist/cli/skills-content/visual-plan-skill.js +30 -11
- package/dist/cli/skills-content/visual-plan-skill.js.map +1 -1
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +4 -3
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/client/AgentAskPopover.d.ts +11 -0
- package/dist/client/AgentAskPopover.d.ts.map +1 -0
- package/dist/client/AgentAskPopover.js +30 -0
- package/dist/client/AgentAskPopover.js.map +1 -0
- package/dist/client/AgentChatHome.d.ts +1 -1
- package/dist/client/AgentChatHome.d.ts.map +1 -1
- package/dist/client/AgentChatHome.js +2 -2
- package/dist/client/AgentChatHome.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +9 -19
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +119 -80
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +4 -4
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts +3 -9
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +49 -189
- 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 +12 -1
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/agent-page/AgentContextTab.d.ts.map +1 -1
- package/dist/client/agent-page/AgentContextTab.js +26 -28
- package/dist/client/agent-page/AgentContextTab.js.map +1 -1
- package/dist/client/agent-page/AgentEmptyState.d.ts +13 -0
- package/dist/client/agent-page/AgentEmptyState.d.ts.map +1 -0
- package/dist/client/agent-page/AgentEmptyState.js +6 -0
- package/dist/client/agent-page/AgentEmptyState.js.map +1 -0
- package/dist/client/agent-page/AgentJobsTab.d.ts +1 -1
- package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
- package/dist/client/agent-page/AgentJobsTab.js +90 -58
- package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
- package/dist/client/agent-page/AgentTabFrame.d.ts +12 -0
- package/dist/client/agent-page/AgentTabFrame.d.ts.map +1 -0
- package/dist/client/agent-page/AgentTabFrame.js +7 -0
- package/dist/client/agent-page/AgentTabFrame.js.map +1 -0
- package/dist/client/agent-page/AgentTabsPage.d.ts +9 -1
- package/dist/client/agent-page/AgentTabsPage.d.ts.map +1 -1
- package/dist/client/agent-page/AgentTabsPage.js +135 -99
- package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
- package/dist/client/agent-page/types.d.ts +1 -1
- package/dist/client/agent-page/types.d.ts.map +1 -1
- package/dist/client/agent-page/types.js.map +1 -1
- package/dist/client/analytics.d.ts +10 -3
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +37 -2
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/chat/run-recovery.d.ts.map +1 -1
- package/dist/client/chat/run-recovery.js +10 -3
- package/dist/client/chat/run-recovery.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +2 -2
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +15 -3
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/dynamic-suggestions.js +3 -3
- package/dist/client/dynamic-suggestions.js.map +1 -1
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/resources/ResourceTree.d.ts +5 -1
- package/dist/client/resources/ResourceTree.d.ts.map +1 -1
- package/dist/client/resources/ResourceTree.js +74 -5
- package/dist/client/resources/ResourceTree.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts +11 -2
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +105 -31
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/client/session-replay.d.ts +28 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +122 -8
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/settings/AutomationsSection.d.ts +3 -1
- package/dist/client/settings/AutomationsSection.d.ts.map +1 -1
- package/dist/client/settings/AutomationsSection.js +16 -6
- package/dist/client/settings/AutomationsSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +3 -3
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts +2 -0
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
- package/dist/client/settings/SettingsTabsPage.js.map +1 -1
- package/dist/client/settings/agent-settings-search.js +2 -2
- package/dist/client/settings/agent-settings-search.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts +2 -0
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/connections/catalog.d.ts +41 -7
- package/dist/connections/catalog.d.ts.map +1 -1
- package/dist/connections/catalog.js +40 -7
- package/dist/connections/catalog.js.map +1 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +2 -2
- package/dist/db/index.js.map +1 -1
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +17 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +85 -15
- package/dist/extensions/actions.js.map +1 -1
- package/dist/extensions/content-patch.d.ts +4 -0
- package/dist/extensions/content-patch.d.ts.map +1 -1
- package/dist/extensions/content-patch.js +18 -0
- package/dist/extensions/content-patch.js.map +1 -1
- package/dist/extensions/routes.d.ts.map +1 -1
- package/dist/extensions/routes.js +14 -0
- package/dist/extensions/routes.js.map +1 -1
- package/dist/extensions/store.d.ts +1 -0
- package/dist/extensions/store.d.ts.map +1 -1
- package/dist/extensions/store.js +4 -1
- package/dist/extensions/store.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ingestion/docx.d.ts +16 -0
- package/dist/ingestion/docx.d.ts.map +1 -0
- package/dist/ingestion/docx.js +129 -0
- package/dist/ingestion/docx.js.map +1 -0
- package/dist/ingestion/figma-node-to-html.d.ts +304 -0
- package/dist/ingestion/figma-node-to-html.d.ts.map +1 -0
- package/dist/ingestion/figma-node-to-html.js +1292 -0
- package/dist/ingestion/figma-node-to-html.js.map +1 -0
- package/dist/ingestion/figma.d.ts +115 -0
- package/dist/ingestion/figma.d.ts.map +1 -0
- package/dist/ingestion/figma.js +399 -0
- package/dist/ingestion/figma.js.map +1 -0
- package/dist/ingestion/index.d.ts +11 -0
- package/dist/ingestion/index.d.ts.map +1 -0
- package/dist/ingestion/index.js +11 -0
- package/dist/ingestion/index.js.map +1 -0
- package/dist/ingestion/media.d.ts +36 -0
- package/dist/ingestion/media.d.ts.map +1 -0
- package/dist/ingestion/media.js +178 -0
- package/dist/ingestion/media.js.map +1 -0
- package/dist/ingestion/notion.d.ts +12 -0
- package/dist/ingestion/notion.d.ts.map +1 -0
- package/dist/ingestion/notion.js +104 -0
- package/dist/ingestion/notion.js.map +1 -0
- package/dist/ingestion/office.d.ts +45 -0
- package/dist/ingestion/office.d.ts.map +1 -0
- package/dist/ingestion/office.js +141 -0
- package/dist/ingestion/office.js.map +1 -0
- package/dist/ingestion/orchestration.d.ts +52 -0
- package/dist/ingestion/orchestration.d.ts.map +1 -0
- package/dist/ingestion/orchestration.js +89 -0
- package/dist/ingestion/orchestration.js.map +1 -0
- package/dist/ingestion/pptx.d.ts +28 -0
- package/dist/ingestion/pptx.d.ts.map +1 -0
- package/dist/ingestion/pptx.js +255 -0
- package/dist/ingestion/pptx.js.map +1 -0
- package/dist/ingestion/selection.d.ts +20 -0
- package/dist/ingestion/selection.d.ts.map +1 -0
- package/dist/ingestion/selection.js +52 -0
- package/dist/ingestion/selection.js.map +1 -0
- package/dist/ingestion/website.d.ts +27 -0
- package/dist/ingestion/website.d.ts.map +1 -0
- package/dist/ingestion/website.js +100 -0
- package/dist/ingestion/website.js.map +1 -0
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +32 -27
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/localization/default-messages.d.ts +22 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +31 -9
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/hosted-model-experiment.d.ts +39 -0
- package/dist/observability/hosted-model-experiment.d.ts.map +1 -0
- package/dist/observability/hosted-model-experiment.js +90 -0
- package/dist/observability/hosted-model-experiment.js.map +1 -0
- package/dist/observability/routes.d.ts +5 -5
- package/dist/observability/traces.d.ts.map +1 -1
- package/dist/observability/traces.js +5 -3
- package/dist/observability/traces.js.map +1 -1
- package/dist/provider-api/index.d.ts +35 -4
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +339 -8
- package/dist/provider-api/index.js.map +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.js +18 -1
- package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
- package/dist/search-utils/index.d.ts +12 -0
- package/dist/search-utils/index.d.ts.map +1 -0
- package/dist/search-utils/index.js +95 -0
- package/dist/search-utils/index.js.map +1 -0
- package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
- package/dist/secrets/register-framework-secrets.js +57 -0
- package/dist/secrets/register-framework-secrets.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-discovery.d.ts +8 -0
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +26 -9
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/agent-chat/framework-prompts.js +2 -2
- package/dist/server/agent-chat/framework-prompts.js.map +1 -1
- package/dist/server/agent-chat/prompt-resources.d.ts +19 -0
- package/dist/server/agent-chat/prompt-resources.d.ts.map +1 -1
- package/dist/server/agent-chat/prompt-resources.js +95 -0
- package/dist/server/agent-chat/prompt-resources.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +7 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +5 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/index.d.ts +5 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +5 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/workspace-provider-oauth.d.ts +48 -0
- package/dist/server/workspace-provider-oauth.d.ts.map +1 -0
- package/dist/server/workspace-provider-oauth.js +415 -0
- package/dist/server/workspace-provider-oauth.js.map +1 -0
- package/dist/styles/agent-native.css +9 -0
- package/dist/templates/chat/app/i18n/en-US.ts +3 -3
- package/dist/templates/chat/app/lib/agent-page.tsx +10 -1
- package/dist/templates/default/app/i18n/en-US.ts +3 -3
- package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +14 -5
- package/dist/templates/workspace-root/.env.example +12 -0
- package/dist/workspace-connections/index.d.ts +1 -0
- package/dist/workspace-connections/index.d.ts.map +1 -1
- package/dist/workspace-connections/index.js +1 -0
- package/dist/workspace-connections/index.js.map +1 -1
- package/dist/workspace-connections/lifecycle.d.ts +16 -0
- package/dist/workspace-connections/lifecycle.d.ts.map +1 -0
- package/dist/workspace-connections/lifecycle.js +14 -0
- package/dist/workspace-connections/lifecycle.js.map +1 -0
- package/dist/workspace-connections/store.d.ts.map +1 -1
- package/dist/workspace-connections/store.js +16 -0
- package/dist/workspace-connections/store.js.map +1 -1
- package/package.json +10 -3
- package/src/templates/chat/app/i18n/en-US.ts +3 -3
- package/src/templates/chat/app/lib/agent-page.spec.tsx +10 -1
- package/src/templates/chat/app/lib/agent-page.tsx +10 -1
- package/src/templates/default/app/i18n/en-US.ts +3 -3
- package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +14 -5
- package/src/templates/workspace-root/.env.example +12 -0
|
@@ -76,11 +76,16 @@ import {
|
|
|
76
76
|
type PauseTransitionQueue,
|
|
77
77
|
} from "./pause-transition";
|
|
78
78
|
import { buildCaptureTitle, type CaptureTitleResult } from "./recording-title";
|
|
79
|
+
import { singleFlight } from "./single-flight";
|
|
79
80
|
import {
|
|
80
81
|
startTranscriptionCapture,
|
|
81
82
|
type CapturedTranscript,
|
|
82
83
|
type TranscriptionCapture,
|
|
83
84
|
} from "./transcription-capture";
|
|
85
|
+
import {
|
|
86
|
+
verifyFinalizeReceipt,
|
|
87
|
+
type FinalizeReceipt,
|
|
88
|
+
} from "./upload-verification";
|
|
84
89
|
import { shouldResampleVideoForUpload } from "./upload-video-stream";
|
|
85
90
|
|
|
86
91
|
export type { LocalExportedFile } from "./local-export";
|
|
@@ -102,6 +107,8 @@ const LEGACY_DEV_REAL_CAPTURE_FLAG = "clips:dev-real-capture";
|
|
|
102
107
|
const LIVE_UPLOAD_CHUNK_MS = 2_000;
|
|
103
108
|
const NATIVE_FULLSCREEN_SEGMENT_MS = 5 * 60_000;
|
|
104
109
|
const NATIVE_FULLSCREEN_MIME_TYPE = "video/mp4";
|
|
110
|
+
const MEDIA_RECORDER_STOP_TIMEOUT_MS = 15_000;
|
|
111
|
+
const THUMBNAIL_UPLOAD_TIMEOUT_MS = 8_000;
|
|
105
112
|
// GCS resumable uploads require every non-final chunk to be a multiple of
|
|
106
113
|
// 256 KiB. MediaRecorder emits arbitrary blob sizes, so on the streaming path
|
|
107
114
|
// we buffer raw blobs and only PUT aligned slices; the unaligned remainder is
|
|
@@ -761,9 +768,10 @@ async function recoverReadyRecordingAfterFinalizeError({
|
|
|
761
768
|
preferAuthenticated: true,
|
|
762
769
|
});
|
|
763
770
|
if (!recovered) return false;
|
|
764
|
-
await
|
|
765
|
-
|
|
766
|
-
|
|
771
|
+
await markBrowserRecordingBackupError(
|
|
772
|
+
recordingId,
|
|
773
|
+
"Upload completed, but its final receipt could not be verified. The local backup was kept.",
|
|
774
|
+
).catch(() => {});
|
|
767
775
|
return true;
|
|
768
776
|
}
|
|
769
777
|
|
|
@@ -800,7 +808,7 @@ async function postBackupChunk(
|
|
|
800
808
|
url: string,
|
|
801
809
|
blob: Blob,
|
|
802
810
|
authToken?: string,
|
|
803
|
-
): Promise<
|
|
811
|
+
): Promise<FinalizeReceipt | null> {
|
|
804
812
|
const res = await fetch(url, {
|
|
805
813
|
method: "POST",
|
|
806
814
|
headers: buildRetryHeaders(
|
|
@@ -810,13 +818,13 @@ async function postBackupChunk(
|
|
|
810
818
|
credentials: "include",
|
|
811
819
|
body: blob,
|
|
812
820
|
});
|
|
821
|
+
const body = await res.text().catch(() => "");
|
|
813
822
|
if (!res.ok) {
|
|
814
|
-
const body = await res.text().catch(() => "");
|
|
815
823
|
throw new Error(
|
|
816
824
|
`Upload retry failed (${res.status}): ${body.slice(0, 200)}`,
|
|
817
825
|
);
|
|
818
826
|
}
|
|
819
|
-
|
|
827
|
+
return body ? (JSON.parse(body) as FinalizeReceipt) : null;
|
|
820
828
|
}
|
|
821
829
|
|
|
822
830
|
async function resetBrowserRecordingBackupUpload(
|
|
@@ -855,7 +863,7 @@ async function replayBrowserBackupToResumableSession(
|
|
|
855
863
|
meta: BrowserRecordingBackupMeta,
|
|
856
864
|
chunks: BrowserRecordingBackupChunk[],
|
|
857
865
|
authToken?: string,
|
|
858
|
-
): Promise<
|
|
866
|
+
): Promise<FinalizeReceipt | null> {
|
|
859
867
|
// The backup is stored in raw MediaRecorder blobs, which have arbitrary
|
|
860
868
|
// boundaries. A resumable provider needs every non-final request aligned,
|
|
861
869
|
// so replay a logical file rather than reusing those blob boundaries.
|
|
@@ -894,7 +902,7 @@ async function replayBrowserBackupToResumableSession(
|
|
|
894
902
|
// aligned it is intentionally empty; the route sends the provider's close
|
|
895
903
|
// request with the bytes committed by the previous chunks.
|
|
896
904
|
const finalBody = recording.slice(offset, recording.size, meta.mimeType);
|
|
897
|
-
|
|
905
|
+
return postBackupChunk(
|
|
898
906
|
chunkUrl(meta.serverUrl, meta.recordingId, fullChunks, true, {
|
|
899
907
|
total: String(totalPosts),
|
|
900
908
|
mimeType: meta.mimeType,
|
|
@@ -938,11 +946,12 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
938
946
|
|
|
939
947
|
if (uploadMode === "streaming") {
|
|
940
948
|
try {
|
|
941
|
-
await replayBrowserBackupToResumableSession(
|
|
949
|
+
const receipt = await replayBrowserBackupToResumableSession(
|
|
942
950
|
meta,
|
|
943
951
|
validatedChunks,
|
|
944
952
|
input.authToken,
|
|
945
953
|
);
|
|
954
|
+
verifyFinalizeReceipt(receipt, meta);
|
|
946
955
|
} catch (err) {
|
|
947
956
|
if (
|
|
948
957
|
await recoverReadyRecordingAfterFinalizeError({
|
|
@@ -993,11 +1002,12 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
993
1002
|
},
|
|
994
1003
|
);
|
|
995
1004
|
try {
|
|
996
|
-
await postBackupChunk(
|
|
1005
|
+
const receipt = await postBackupChunk(
|
|
997
1006
|
finalChunkUrl,
|
|
998
1007
|
new Blob([], { type: meta.mimeType }),
|
|
999
1008
|
input.authToken,
|
|
1000
1009
|
);
|
|
1010
|
+
verifyFinalizeReceipt(receipt, meta);
|
|
1001
1011
|
} catch (err) {
|
|
1002
1012
|
if (
|
|
1003
1013
|
await recoverReadyRecordingAfterFinalizeError({
|
|
@@ -1057,7 +1067,6 @@ async function createServerRecording(
|
|
|
1057
1067
|
hasCamera,
|
|
1058
1068
|
hasAudio,
|
|
1059
1069
|
spaceIds: [],
|
|
1060
|
-
visibility: "public",
|
|
1061
1070
|
...(options?.requestStreaming
|
|
1062
1071
|
? {
|
|
1063
1072
|
requestStreaming: true,
|
|
@@ -1355,6 +1364,7 @@ async function captureAndUploadRecordingThumbnail(params: {
|
|
|
1355
1364
|
headers: buildRetryHeaders(blob.type || "image/jpeg", params.authToken),
|
|
1356
1365
|
credentials: "include",
|
|
1357
1366
|
body: blob,
|
|
1367
|
+
signal: AbortSignal.timeout(THUMBNAIL_UPLOAD_TIMEOUT_MS),
|
|
1358
1368
|
});
|
|
1359
1369
|
if (!res.ok) {
|
|
1360
1370
|
const body = await res.text().catch(() => "");
|
|
@@ -3317,15 +3327,23 @@ async function startRecordingInner(
|
|
|
3317
3327
|
backupMeta = { ...backupMeta, ...patch };
|
|
3318
3328
|
await putBrowserRecordingBackupMeta(backupMeta);
|
|
3319
3329
|
};
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3330
|
+
let backupFailure: Error | null = null;
|
|
3331
|
+
const backupWrites = new Set<Promise<void>>();
|
|
3332
|
+
let initialBackupWrite: Promise<void>;
|
|
3333
|
+
initialBackupWrite = persistBackupMeta()
|
|
3334
|
+
.catch((err) => {
|
|
3335
|
+
backupFailure = err instanceof Error ? err : new Error(String(err));
|
|
3336
|
+
console.warn("[clips-recorder] local backup metadata failed:", err);
|
|
3337
|
+
})
|
|
3338
|
+
.finally(() => {
|
|
3339
|
+
backupWrites.delete(initialBackupWrite);
|
|
3340
|
+
});
|
|
3341
|
+
backupWrites.add(initialBackupWrite);
|
|
3323
3342
|
|
|
3324
3343
|
// Every raw MediaRecorder blob is mirrored to IndexedDB on both upload paths.
|
|
3325
3344
|
// If uploads fail the recording can still be recovered locally — replayed to
|
|
3326
3345
|
// the server (the retry first resets any resumable session so replay routes
|
|
3327
3346
|
// through the buffered chunk path) or exported to a local file.
|
|
3328
|
-
const backupWrites = new Set<Promise<void>>();
|
|
3329
3347
|
const backupChunkLocally = (blob: Blob): Promise<void> => {
|
|
3330
3348
|
const backupIdx = backupChunkCount++;
|
|
3331
3349
|
backupBytes += blob.size;
|
|
@@ -3347,6 +3365,7 @@ async function startRecordingInner(
|
|
|
3347
3365
|
mimeType: chunkMimeType,
|
|
3348
3366
|
});
|
|
3349
3367
|
} catch (err) {
|
|
3368
|
+
backupFailure = err instanceof Error ? err : new Error(String(err));
|
|
3350
3369
|
console.warn("[clips-recorder] local chunk backup failed:", err);
|
|
3351
3370
|
}
|
|
3352
3371
|
})().finally(() => {
|
|
@@ -3565,74 +3584,127 @@ async function startRecordingInner(
|
|
|
3565
3584
|
// with cameraOn. The recorder reuses that camera stream for the saved
|
|
3566
3585
|
// video composite and flips the toolbar from disabled → enabled above.
|
|
3567
3586
|
|
|
3568
|
-
const
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3587
|
+
const performStop = async (): Promise<RecorderStopResult> => {
|
|
3588
|
+
if (stopped) return { recordingId: id, viewUrl: `/r/${id}` };
|
|
3589
|
+
stopped = true;
|
|
3590
|
+
// Stamped right after the recorder fully stops below (see stoppedAt).
|
|
3591
|
+
// Duration must measure recorded content — through the final flushed
|
|
3592
|
+
// chunk — but NOT the transcript-finalize + thumbnail + upload awaits that
|
|
3593
|
+
// follow, which add ~seconds and would overstate the saved duration.
|
|
3594
|
+
let stoppedAt = 0;
|
|
3595
|
+
const viewUrl = `/r/${id}`;
|
|
3596
|
+
const absoluteViewUrl = `${params.serverUrl.replace(/\/+$/, "")}${viewUrl}`;
|
|
3597
|
+
console.log("[clips-recorder] stop requested");
|
|
3598
|
+
showFinalizingFeedback();
|
|
3599
|
+
if (tickHandle) clearInterval(tickHandle);
|
|
3600
|
+
stateUnlistens.forEach((u) => u());
|
|
3601
|
+
stateUnlistens = [];
|
|
3602
|
+
|
|
3603
|
+
// Flush the in-flight recorder buffer, then wait for it to fully stop
|
|
3604
|
+
// so we get the trailing dataavailable event.
|
|
3605
|
+
const recorderStopped = new Promise<void>((resolve) => {
|
|
3606
|
+
if (recorder.state === "inactive") {
|
|
3607
|
+
stoppedAt = Date.now();
|
|
3608
|
+
resolve();
|
|
3609
|
+
return;
|
|
3610
|
+
}
|
|
3611
|
+
recorder.addEventListener(
|
|
3612
|
+
"stop",
|
|
3613
|
+
() => {
|
|
3589
3614
|
stoppedAt = Date.now();
|
|
3590
3615
|
resolve();
|
|
3591
|
-
|
|
3616
|
+
},
|
|
3617
|
+
{ once: true },
|
|
3618
|
+
);
|
|
3619
|
+
try {
|
|
3620
|
+
if (recorder.state === "paused") {
|
|
3621
|
+
recorder.resume();
|
|
3622
|
+
if (pausedAt) accumulatedPauseMs += Date.now() - pausedAt;
|
|
3623
|
+
pausedAt = null;
|
|
3592
3624
|
}
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3625
|
+
} catch {
|
|
3626
|
+
// ignore
|
|
3627
|
+
}
|
|
3628
|
+
try {
|
|
3629
|
+
recorder.requestData();
|
|
3630
|
+
} catch {
|
|
3631
|
+
// ignore
|
|
3632
|
+
}
|
|
3633
|
+
try {
|
|
3634
|
+
recorder.stop();
|
|
3635
|
+
} catch {
|
|
3636
|
+
// ignore
|
|
3637
|
+
}
|
|
3638
|
+
});
|
|
3639
|
+
// `recorder.stop()` has already been requested, so recorded duration is
|
|
3640
|
+
// fixed even if launching the browser takes a moment. Open the existing
|
|
3641
|
+
// recording row now and let its page poll while upload/finalize continues.
|
|
3642
|
+
//
|
|
3643
|
+
// This must claim the native upload-open slot before launching. The
|
|
3644
|
+
// Finalizing overlay receives the completion event later and uses the
|
|
3645
|
+
// same claim; without it, browser recordings opened once here and then a
|
|
3646
|
+
// second time when finalization completed.
|
|
3647
|
+
await openNativeUploadUrl(id, absoluteViewUrl);
|
|
3648
|
+
const recorderStopTimedOut = await Promise.race([
|
|
3649
|
+
recorderStopped.then(() => false),
|
|
3650
|
+
wait(MEDIA_RECORDER_STOP_TIMEOUT_MS).then(() => true),
|
|
3651
|
+
]);
|
|
3652
|
+
if (recorderStopTimedOut) {
|
|
3653
|
+
stoppedAt = Date.now();
|
|
3654
|
+
failed ??= new Error(
|
|
3655
|
+
"The recorder did not finish stopping. Your local backup was kept so you can retry or download it from Clips.",
|
|
3656
|
+
);
|
|
3657
|
+
void transcriptionCapture?.cancel().catch((err) => {
|
|
3658
|
+
console.warn(
|
|
3659
|
+
"[clips-recorder] transcription cancel after stop timeout failed:",
|
|
3660
|
+
err,
|
|
3600
3661
|
);
|
|
3601
|
-
try {
|
|
3602
|
-
if (recorder.state === "paused") {
|
|
3603
|
-
recorder.resume();
|
|
3604
|
-
if (pausedAt) accumulatedPauseMs += Date.now() - pausedAt;
|
|
3605
|
-
pausedAt = null;
|
|
3606
|
-
}
|
|
3607
|
-
} catch {
|
|
3608
|
-
// ignore
|
|
3609
|
-
}
|
|
3610
|
-
try {
|
|
3611
|
-
recorder.requestData();
|
|
3612
|
-
} catch {
|
|
3613
|
-
// ignore
|
|
3614
|
-
}
|
|
3615
|
-
try {
|
|
3616
|
-
recorder.stop();
|
|
3617
|
-
} catch {
|
|
3618
|
-
// ignore
|
|
3619
|
-
}
|
|
3620
3662
|
});
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3663
|
+
}
|
|
3664
|
+
// Use the recorder's stop event as the content boundary. If WebKit never
|
|
3665
|
+
// emits it, the watchdog boundary preserves the available chunks and keeps
|
|
3666
|
+
// post-processing time out of the recovery copy's duration.
|
|
3667
|
+
|
|
3668
|
+
const videoSettings = uploadPrimaryVideo.stream
|
|
3669
|
+
.getVideoTracks()[0]
|
|
3670
|
+
?.getSettings();
|
|
3671
|
+
const displaySettings = displayStream?.getVideoTracks()[0]?.getSettings();
|
|
3672
|
+
const durationMs = Math.max(
|
|
3673
|
+
0,
|
|
3674
|
+
Math.round(stoppedAt - startedAt - accumulatedPauseMs),
|
|
3675
|
+
);
|
|
3676
|
+
const width =
|
|
3677
|
+
typeof videoSettings?.width === "number"
|
|
3678
|
+
? videoSettings.width
|
|
3679
|
+
: typeof displaySettings?.width === "number"
|
|
3680
|
+
? displaySettings.width
|
|
3681
|
+
: null;
|
|
3682
|
+
const height =
|
|
3683
|
+
typeof videoSettings?.height === "number"
|
|
3684
|
+
? videoSettings.height
|
|
3685
|
+
: typeof displaySettings?.height === "number"
|
|
3686
|
+
? displaySettings.height
|
|
3687
|
+
: null;
|
|
3688
|
+
const finalMimeType = mimeType || backupMeta.mimeType || "video/webm";
|
|
3689
|
+
try {
|
|
3690
|
+
await persistBackupMeta({
|
|
3691
|
+
durationMs,
|
|
3692
|
+
width,
|
|
3693
|
+
height,
|
|
3694
|
+
bytes: backupBytes,
|
|
3695
|
+
hasAudio: uploadCombined.getAudioTracks().length > 0,
|
|
3696
|
+
hasCamera: wantsCamera,
|
|
3697
|
+
chunkCount: backupChunkCount,
|
|
3698
|
+
mimeType: finalMimeType,
|
|
3699
|
+
lastError: null,
|
|
3700
|
+
});
|
|
3701
|
+
} catch (err) {
|
|
3702
|
+
backupFailure = err instanceof Error ? err : new Error(String(err));
|
|
3703
|
+
console.warn("[clips-recorder] local backup final metadata failed:", err);
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
if (!recorderStopTimedOut) {
|
|
3707
|
+
void captureAndUploadRecordingThumbnail({
|
|
3636
3708
|
serverUrl: params.serverUrl,
|
|
3637
3709
|
recordingId: id,
|
|
3638
3710
|
stream: primaryVideo,
|
|
@@ -3659,224 +3731,202 @@ async function startRecordingInner(
|
|
|
3659
3731
|
"No speech was captured during this recording. If you spoke or played system audio, check System Audio, Microphone input, Speech Recognition permission, and the selected mic, then retry transcription.",
|
|
3660
3732
|
);
|
|
3661
3733
|
}
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
}
|
|
3668
|
-
|
|
3669
|
-
const videoSettings = uploadPrimaryVideo.stream
|
|
3670
|
-
.getVideoTracks()[0]
|
|
3671
|
-
?.getSettings();
|
|
3672
|
-
const displaySettings = displayStream?.getVideoTracks()[0]?.getSettings();
|
|
3673
|
-
const durationMs = Math.max(
|
|
3674
|
-
0,
|
|
3675
|
-
Math.round(stoppedAt - startedAt - accumulatedPauseMs),
|
|
3676
|
-
);
|
|
3677
|
-
const width =
|
|
3678
|
-
typeof videoSettings?.width === "number"
|
|
3679
|
-
? videoSettings.width
|
|
3680
|
-
: typeof displaySettings?.width === "number"
|
|
3681
|
-
? displaySettings.width
|
|
3682
|
-
: null;
|
|
3683
|
-
const height =
|
|
3684
|
-
typeof videoSettings?.height === "number"
|
|
3685
|
-
? videoSettings.height
|
|
3686
|
-
: typeof displaySettings?.height === "number"
|
|
3687
|
-
? displaySettings.height
|
|
3688
|
-
: null;
|
|
3689
|
-
const finalMimeType = mimeType || backupMeta.mimeType || "video/webm";
|
|
3690
|
-
await persistBackupMeta({
|
|
3691
|
-
durationMs,
|
|
3692
|
-
width,
|
|
3693
|
-
height,
|
|
3694
|
-
bytes: backupBytes,
|
|
3695
|
-
hasAudio: uploadCombined.getAudioTracks().length > 0,
|
|
3696
|
-
hasCamera: wantsCamera,
|
|
3697
|
-
chunkCount: backupChunkCount,
|
|
3698
|
-
mimeType: finalMimeType,
|
|
3699
|
-
lastError: null,
|
|
3700
|
-
}).catch((err) => {
|
|
3701
|
-
console.warn(
|
|
3702
|
-
"[clips-recorder] local backup final metadata failed:",
|
|
3703
|
-
err,
|
|
3704
|
-
);
|
|
3705
|
-
});
|
|
3706
|
-
|
|
3707
|
-
// Null the data handler so the final MediaRecorder teardown
|
|
3708
|
-
// doesn't keep the closure (which captures `inflight`, the URL
|
|
3709
|
-
// builder, and indirectly the MediaStream) reachable after we're
|
|
3710
|
-
// done with it. WebKit's MediaRecorder can retain a reference to
|
|
3711
|
-
// its event handler for the life of the object if you leave a
|
|
3712
|
-
// non-null ondataavailable in place — null it to break the chain.
|
|
3713
|
-
recorder.ondataavailable = null;
|
|
3714
|
-
// Clear MediaStream track lists — just removing our
|
|
3715
|
-
// references to the tracks is enough; the tracks themselves are
|
|
3716
|
-
// owned by `displayStream` / `audioStream` / `bubbleCameraStream`
|
|
3717
|
-
// and get stopped below.
|
|
3718
|
-
try {
|
|
3719
|
-
uploadCombined
|
|
3720
|
-
.getTracks()
|
|
3721
|
-
.forEach((t) => uploadCombined.removeTrack(t));
|
|
3722
|
-
combined.getTracks().forEach((t) => combined.removeTrack(t));
|
|
3723
|
-
} catch {
|
|
3724
|
-
// ignore — best-effort
|
|
3725
|
-
}
|
|
3734
|
+
}
|
|
3735
|
+
if (popoverOwnsCamera) {
|
|
3736
|
+
console.log("[clips-recorder] releasing popover camera");
|
|
3737
|
+
emit("clips:release-camera").catch(() => {});
|
|
3738
|
+
}
|
|
3726
3739
|
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3740
|
+
// Null the data handler so the final MediaRecorder teardown
|
|
3741
|
+
// doesn't keep the closure (which captures `inflight`, the URL
|
|
3742
|
+
// builder, and indirectly the MediaStream) reachable after we're
|
|
3743
|
+
// done with it. WebKit's MediaRecorder can retain a reference to
|
|
3744
|
+
// its event handler for the life of the object if you leave a
|
|
3745
|
+
// non-null ondataavailable in place — null it to break the chain.
|
|
3746
|
+
recorder.ondataavailable = null;
|
|
3747
|
+
// Clear MediaStream track lists — just removing our
|
|
3748
|
+
// references to the tracks is enough; the tracks themselves are
|
|
3749
|
+
// owned by `displayStream` / `audioStream` / `bubbleCameraStream`
|
|
3750
|
+
// and get stopped below.
|
|
3751
|
+
try {
|
|
3752
|
+
uploadCombined.getTracks().forEach((t) => uploadCombined.removeTrack(t));
|
|
3753
|
+
combined.getTracks().forEach((t) => combined.removeTrack(t));
|
|
3754
|
+
} catch {
|
|
3755
|
+
// ignore — best-effort
|
|
3756
|
+
}
|
|
3737
3757
|
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
);
|
|
3758
|
+
// Stop the streams WE own so OS permission indicators clear. The
|
|
3759
|
+
// camera stream is owned by the popover when reused — we leave it
|
|
3760
|
+
// alone so the bubble stays live if the popover is still open.
|
|
3761
|
+
[displayStream, audioStream].forEach((s) =>
|
|
3762
|
+
s?.getTracks().forEach((t) => t.stop()),
|
|
3763
|
+
);
|
|
3764
|
+
streamCleanups.forEach((cleanup) => cleanup());
|
|
3765
|
+
if (!popoverOwnsCamera) {
|
|
3766
|
+
bubbleCameraStream?.getTracks().forEach((t) => t.stop());
|
|
3767
|
+
}
|
|
3747
3768
|
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
await Promise.allSettled(pending);
|
|
3758
|
-
}
|
|
3759
|
-
inflight.clear();
|
|
3760
|
-
if (failed) {
|
|
3761
|
-
try {
|
|
3762
|
-
// Keep the guard until the final metadata and every trailing chunk
|
|
3763
|
-
// backup are durable, even though this upload cannot be finalized.
|
|
3764
|
-
await Promise.allSettled([...backupWrites]);
|
|
3765
|
-
console.error("[clips-recorder] chunk upload failed:", failed);
|
|
3766
|
-
await markBrowserRecordingBackupError(id, failed.message).catch(
|
|
3767
|
-
() => {},
|
|
3768
|
-
);
|
|
3769
|
-
await abortRecordingUpload(params.serverUrl, id, failed.message);
|
|
3770
|
-
} finally {
|
|
3771
|
-
await clearRecordingState();
|
|
3772
|
-
await publishFinalizingResult({
|
|
3773
|
-
recordingId: id,
|
|
3774
|
-
viewUrl: absoluteViewUrl,
|
|
3775
|
-
ok: false,
|
|
3776
|
-
error: failed.message,
|
|
3777
|
-
});
|
|
3778
|
-
}
|
|
3779
|
-
throw failed;
|
|
3780
|
-
}
|
|
3769
|
+
// Hide the recording-specific overlays (countdown + toolbar). The
|
|
3770
|
+
// bubble is managed by the popover's session effect — when the
|
|
3771
|
+
// popover is hidden or the user turns camera off, that effect tears
|
|
3772
|
+
// down the bubble. Closing it here would cause a flicker on the
|
|
3773
|
+
// cancel path where the popover re-appears with camera still on.
|
|
3774
|
+
console.log("[clips-recorder] hiding recording chrome");
|
|
3775
|
+
await invoke("hide_recording_chrome").catch((err) =>
|
|
3776
|
+
console.error(`[clips-recorder] hide_recording_chrome failed:`, err),
|
|
3777
|
+
);
|
|
3781
3778
|
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
durationMs: String(durationMs),
|
|
3796
|
-
...(width ? { width: String(width) } : {}),
|
|
3797
|
-
...(height ? { height: String(height) } : {}),
|
|
3798
|
-
hasAudio: backupMeta.hasAudio ? "1" : "0",
|
|
3799
|
-
hasCamera: wantsCamera ? "1" : "0",
|
|
3800
|
-
});
|
|
3801
|
-
console.log("[clips-recorder] finalize POST", finalizeUrl, {
|
|
3802
|
-
chunksSent: chunkIndex,
|
|
3803
|
-
inflightAtFinalize: pending.length,
|
|
3804
|
-
finalBodyBytes: finalBody.size,
|
|
3805
|
-
uploadMode,
|
|
3806
|
-
anyFailed: !!failed,
|
|
3807
|
-
});
|
|
3779
|
+
// Wait for any in-flight chunk uploads to settle before sending the
|
|
3780
|
+
// final chunk. Otherwise the server could finalize before the last
|
|
3781
|
+
// few bytes land. On the streaming path uploads are serialized through
|
|
3782
|
+
// `streamQueue`; on the buffered path they run concurrently and each
|
|
3783
|
+
// `.finally` has already removed settled entries from `inflight`.
|
|
3784
|
+
const pending = Array.from(inflight);
|
|
3785
|
+
if (uploadMode === "streaming") {
|
|
3786
|
+
await streamQueue;
|
|
3787
|
+
} else {
|
|
3788
|
+
await Promise.allSettled(pending);
|
|
3789
|
+
}
|
|
3790
|
+
inflight.clear();
|
|
3791
|
+
if (failed) {
|
|
3808
3792
|
try {
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
},
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
3820
|
-
credentials: "include",
|
|
3821
|
-
body: finalBody,
|
|
3822
|
-
signal: AbortSignal.timeout(FINALIZE_UPLOAD_TIMEOUT_MS),
|
|
3823
|
-
});
|
|
3824
|
-
const bodyText = await finalRes.text().catch(() => "");
|
|
3825
|
-
console.log(
|
|
3826
|
-
"[clips-recorder] finalize response:",
|
|
3827
|
-
finalRes.status,
|
|
3828
|
-
bodyText.slice(0, 500),
|
|
3829
|
-
);
|
|
3830
|
-
if (!finalRes.ok) {
|
|
3831
|
-
throw new Error(
|
|
3832
|
-
`Finalize failed (${finalRes.status}): ${bodyText.slice(0, 200)}`,
|
|
3833
|
-
);
|
|
3834
|
-
}
|
|
3835
|
-
} catch (err) {
|
|
3836
|
-
console.error("[clips-recorder] finalize fetch failed:", err);
|
|
3837
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
3838
|
-
if (
|
|
3839
|
-
await recoverReadyRecordingAfterFinalizeError({
|
|
3840
|
-
serverUrl: params.serverUrl,
|
|
3841
|
-
recordingId: id,
|
|
3842
|
-
authToken: params.authToken,
|
|
3843
|
-
})
|
|
3844
|
-
) {
|
|
3845
|
-
return { recordingId: id, viewUrl };
|
|
3846
|
-
}
|
|
3847
|
-
await markBrowserRecordingBackupError(id, error.message).catch(
|
|
3848
|
-
() => {},
|
|
3849
|
-
);
|
|
3850
|
-
await abortRecordingUpload(params.serverUrl, id, error.message);
|
|
3851
|
-
throw error;
|
|
3852
|
-
}
|
|
3853
|
-
await deleteBrowserRecordingBackup(id).catch((err) => {
|
|
3854
|
-
console.warn(
|
|
3855
|
-
"[clips-recorder] local backup cleanup failed:",
|
|
3856
|
-
err,
|
|
3857
|
-
);
|
|
3858
|
-
});
|
|
3859
|
-
|
|
3860
|
-
return { recordingId: id, viewUrl };
|
|
3861
|
-
},
|
|
3862
|
-
releaseGuard: clearRecordingState,
|
|
3863
|
-
});
|
|
3864
|
-
await publishFinalizingResult({
|
|
3865
|
-
recordingId: id,
|
|
3866
|
-
viewUrl: absoluteViewUrl,
|
|
3867
|
-
ok: true,
|
|
3868
|
-
});
|
|
3869
|
-
return result;
|
|
3870
|
-
} catch (err) {
|
|
3793
|
+
// Keep the guard until the final metadata and every trailing chunk
|
|
3794
|
+
// backup are durable, even though this upload cannot be finalized.
|
|
3795
|
+
await Promise.allSettled([...backupWrites]);
|
|
3796
|
+
console.error("[clips-recorder] chunk upload failed:", failed);
|
|
3797
|
+
await markBrowserRecordingBackupError(id, failed.message).catch(
|
|
3798
|
+
() => {},
|
|
3799
|
+
);
|
|
3800
|
+
await abortRecordingUpload(params.serverUrl, id, failed.message);
|
|
3801
|
+
} finally {
|
|
3802
|
+
await clearRecordingState();
|
|
3871
3803
|
await publishFinalizingResult({
|
|
3872
3804
|
recordingId: id,
|
|
3873
3805
|
viewUrl: absoluteViewUrl,
|
|
3874
3806
|
ok: false,
|
|
3875
|
-
error:
|
|
3807
|
+
error: failed.message,
|
|
3876
3808
|
});
|
|
3877
|
-
throw err;
|
|
3878
3809
|
}
|
|
3879
|
-
|
|
3810
|
+
throw failed;
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
// Streaming: the closing bytes are whatever remains under the 256 KiB
|
|
3814
|
+
// alignment boundary — send them as the final chunk so the resumable
|
|
3815
|
+
// session can complete. Buffered: bytes are already staged server-side,
|
|
3816
|
+
// so the final chunk is a 0-byte close sentinel.
|
|
3817
|
+
const finalBody =
|
|
3818
|
+
uploadMode === "streaming"
|
|
3819
|
+
? new Blob(pendingStreamBlobs, { type: finalMimeType })
|
|
3820
|
+
: new Blob([], { type: finalMimeType });
|
|
3821
|
+
pendingStreamBlobs = [];
|
|
3822
|
+
pendingStreamBytes = 0;
|
|
3823
|
+
|
|
3824
|
+
const finalizeUrl = chunkUrl(params.serverUrl, id, chunkIndex, true, {
|
|
3825
|
+
mimeType: finalMimeType,
|
|
3826
|
+
durationMs: String(durationMs),
|
|
3827
|
+
...(width ? { width: String(width) } : {}),
|
|
3828
|
+
...(height ? { height: String(height) } : {}),
|
|
3829
|
+
hasAudio: backupMeta.hasAudio ? "1" : "0",
|
|
3830
|
+
hasCamera: wantsCamera ? "1" : "0",
|
|
3831
|
+
});
|
|
3832
|
+
console.log("[clips-recorder] finalize POST", finalizeUrl, {
|
|
3833
|
+
chunksSent: chunkIndex,
|
|
3834
|
+
inflightAtFinalize: pending.length,
|
|
3835
|
+
finalBodyBytes: finalBody.size,
|
|
3836
|
+
uploadMode,
|
|
3837
|
+
anyFailed: !!failed,
|
|
3838
|
+
});
|
|
3839
|
+
try {
|
|
3840
|
+
const result = await finalizeAfterDurableBackup({
|
|
3841
|
+
// Opening the browser must not make the desktop look idle until every
|
|
3842
|
+
// trailing backup write and the final metadata are durable.
|
|
3843
|
+
ensureBackupDurable: async () => {
|
|
3844
|
+
await Promise.all([...backupWrites]);
|
|
3845
|
+
if (backupFailure) throw backupFailure;
|
|
3846
|
+
await persistBackupMeta({
|
|
3847
|
+
durationMs,
|
|
3848
|
+
width,
|
|
3849
|
+
height,
|
|
3850
|
+
bytes: backupBytes,
|
|
3851
|
+
hasAudio: uploadCombined.getAudioTracks().length > 0,
|
|
3852
|
+
hasCamera: wantsCamera,
|
|
3853
|
+
chunkCount: backupChunkCount,
|
|
3854
|
+
mimeType: finalMimeType,
|
|
3855
|
+
lastError: null,
|
|
3856
|
+
});
|
|
3857
|
+
},
|
|
3858
|
+
attemptFinalize: async () => {
|
|
3859
|
+
try {
|
|
3860
|
+
const finalRes = await fetch(finalizeUrl, {
|
|
3861
|
+
method: "POST",
|
|
3862
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
3863
|
+
credentials: "include",
|
|
3864
|
+
body: finalBody,
|
|
3865
|
+
signal: AbortSignal.timeout(FINALIZE_UPLOAD_TIMEOUT_MS),
|
|
3866
|
+
});
|
|
3867
|
+
const bodyText = await finalRes.text().catch(() => "");
|
|
3868
|
+
console.log(
|
|
3869
|
+
"[clips-recorder] finalize response:",
|
|
3870
|
+
finalRes.status,
|
|
3871
|
+
bodyText.slice(0, 500),
|
|
3872
|
+
);
|
|
3873
|
+
if (!finalRes.ok) {
|
|
3874
|
+
throw new Error(
|
|
3875
|
+
`Finalize failed (${finalRes.status}): ${bodyText.slice(0, 200)}`,
|
|
3876
|
+
);
|
|
3877
|
+
}
|
|
3878
|
+
const receipt = bodyText
|
|
3879
|
+
? (JSON.parse(bodyText) as FinalizeReceipt)
|
|
3880
|
+
: null;
|
|
3881
|
+
verifyFinalizeReceipt(receipt, {
|
|
3882
|
+
bytes: backupBytes,
|
|
3883
|
+
durationMs,
|
|
3884
|
+
});
|
|
3885
|
+
} catch (err) {
|
|
3886
|
+
console.error("[clips-recorder] finalize fetch failed:", err);
|
|
3887
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
3888
|
+
if (
|
|
3889
|
+
await recoverReadyRecordingAfterFinalizeError({
|
|
3890
|
+
serverUrl: params.serverUrl,
|
|
3891
|
+
recordingId: id,
|
|
3892
|
+
authToken: params.authToken,
|
|
3893
|
+
})
|
|
3894
|
+
) {
|
|
3895
|
+
return { recordingId: id, viewUrl };
|
|
3896
|
+
}
|
|
3897
|
+
await markBrowserRecordingBackupError(id, error.message).catch(
|
|
3898
|
+
() => {},
|
|
3899
|
+
);
|
|
3900
|
+
await abortRecordingUpload(params.serverUrl, id, error.message);
|
|
3901
|
+
throw error;
|
|
3902
|
+
}
|
|
3903
|
+
await deleteBrowserRecordingBackup(id).catch((err) => {
|
|
3904
|
+
console.warn("[clips-recorder] local backup cleanup failed:", err);
|
|
3905
|
+
});
|
|
3906
|
+
|
|
3907
|
+
return { recordingId: id, viewUrl };
|
|
3908
|
+
},
|
|
3909
|
+
releaseGuard: clearRecordingState,
|
|
3910
|
+
});
|
|
3911
|
+
await publishFinalizingResult({
|
|
3912
|
+
recordingId: id,
|
|
3913
|
+
viewUrl: absoluteViewUrl,
|
|
3914
|
+
ok: true,
|
|
3915
|
+
});
|
|
3916
|
+
return result;
|
|
3917
|
+
} catch (err) {
|
|
3918
|
+
await publishFinalizingResult({
|
|
3919
|
+
recordingId: id,
|
|
3920
|
+
viewUrl: absoluteViewUrl,
|
|
3921
|
+
ok: false,
|
|
3922
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3923
|
+
});
|
|
3924
|
+
throw err;
|
|
3925
|
+
}
|
|
3926
|
+
};
|
|
3927
|
+
|
|
3928
|
+
const handle: RecorderHandle = {
|
|
3929
|
+
stop: singleFlight(performStop),
|
|
3880
3930
|
|
|
3881
3931
|
async cancel() {
|
|
3882
3932
|
if (stopped) return;
|