@agent-native/core 0.69.0 → 0.70.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -15
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +109 -0
- package/corpus/core/docs/design/durable-agent-runs.md +217 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +89 -0
- package/corpus/core/src/agent/model-config.ts +0 -2
- package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
- package/corpus/core/src/changelog/parse.ts +245 -0
- package/corpus/core/src/cli/changelog.ts +199 -0
- package/corpus/core/src/cli/index.ts +19 -0
- package/corpus/core/src/cli/recap.ts +24 -1
- package/corpus/core/src/cli/skills.ts +74 -47
- package/corpus/core/src/client/AgentPanel.tsx +5 -3
- package/corpus/core/src/client/AssistantChat.tsx +8 -0
- package/corpus/core/src/client/CommandMenu.tsx +183 -75
- package/corpus/core/src/client/agent-chat-adapter.ts +44 -7
- package/corpus/core/src/client/analytics.ts +181 -0
- package/corpus/core/src/client/changelog/Changelog.tsx +321 -0
- package/corpus/core/src/client/client-surface.ts +41 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
- package/corpus/core/src/client/feedback-context.ts +4 -0
- package/corpus/core/src/client/index.ts +12 -0
- package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +54 -0
- package/corpus/core/src/client/settings/useBuilderStatus.ts +82 -20
- package/corpus/core/src/client/sse-event-processor.ts +7 -1
- package/corpus/core/src/mcp/build-server.ts +57 -1
- package/corpus/core/src/mcp/embed-app.ts +197 -7
- package/corpus/core/src/mcp/oauth-route.ts +109 -13
- package/corpus/core/src/server/attribution.ts +211 -0
- package/corpus/core/src/server/auth.ts +6 -0
- package/corpus/core/src/server/better-auth-instance.ts +49 -5
- package/corpus/core/src/server/builder-browser.ts +27 -1
- package/corpus/core/src/server/core-routes-plugin.ts +136 -0
- package/corpus/core/src/server/google-oauth.ts +19 -6
- package/corpus/core/src/server/onboarding-html.ts +35 -0
- package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
- package/corpus/core/src/styles/agent-conversation.css +21 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
- package/corpus/templates/analytics/AGENTS.md +8 -1
- package/corpus/templates/analytics/CHANGELOG.md +10 -0
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +99 -4
- package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +39 -8
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
- package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +180 -144
- package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -3
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -5
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +221 -215
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +6 -6
- package/corpus/templates/analytics/app/lib/demo-dashboard-path.ts +1 -1
- package/corpus/templates/analytics/app/lib/last-opened.ts +2 -1
- package/corpus/templates/analytics/app/pages/Settings.tsx +4 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +2 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +101 -99
- package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +1 -1
- package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +18 -5
- package/corpus/templates/analytics/app/routes/catalog.tsx +1 -1
- package/corpus/templates/analytics/app/routes/dashboards.$id.tsx +9 -0
- package/corpus/templates/analytics/app/routes/traffic.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-23-the-dashboards-and-analyses-sidebar-settings-buttons-now-sta.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +145 -0
- package/corpus/templates/analytics/server/db/index.ts +1 -1
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -2
- package/corpus/templates/analytics/server/lib/demo-dashboards.ts +1 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -2
- package/corpus/templates/analytics/server/plugins/core-routes.ts +6 -6
- package/corpus/templates/assets/CHANGELOG.md +10 -0
- package/corpus/templates/assets/actions/generate-image.ts +24 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
- package/corpus/templates/assets/app/global.css +17 -1
- package/corpus/templates/assets/app/root.tsx +7 -1
- package/corpus/templates/assets/app/routes/_index.tsx +94 -14
- package/corpus/templates/assets/app/routes/settings.tsx +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
- package/corpus/templates/brain/CHANGELOG.md +10 -0
- package/corpus/templates/brain/app/root.tsx +7 -1
- package/corpus/templates/brain/app/routes/settings.tsx +8 -1
- package/corpus/templates/calendar/CHANGELOG.md +10 -0
- package/corpus/templates/calendar/app/pages/Settings.tsx +4 -0
- package/corpus/templates/calendar/app/root.tsx +7 -1
- package/corpus/templates/chat/CHANGELOG.md +10 -0
- package/corpus/templates/chat/app/root.tsx +7 -1
- package/corpus/templates/clips/CHANGELOG.md +10 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +33 -7
- package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +8 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +150 -20
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +7 -11
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +31 -0
- package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +71 -0
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
- package/corpus/templates/clips/app/root.tsx +7 -1
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +15 -1
- package/corpus/templates/clips/app/routes/record.tsx +78 -5
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -5
- package/corpus/templates/clips/changelog/2026-06-23-clips-shared-in-slack-no-longer-show-a-could-not-start-playb.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-made-the-play-button-on-shared-and-embedded-clips-scale-with.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-public-clip-share-links-now-show-whether-they-ll-play-inline.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-screen-camera-recordings-now-keep-the-presenter-s-face-in-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
- package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
- package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
- package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
- package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
- package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
- package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
- package/corpus/templates/clips/desktop/src/overlays/region-record-border.tsx +76 -0
- package/corpus/templates/clips/desktop/src/styles.css +119 -2
- package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +140 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +4 -1
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +85 -6
- package/corpus/templates/clips/shared/share-attribution.ts +79 -0
- package/corpus/templates/content/AGENTS.md +20 -3
- package/corpus/templates/content/CHANGELOG.md +10 -0
- package/corpus/templates/content/actions/_database-utils.ts +15 -0
- package/corpus/templates/content/actions/_property-utils.ts +371 -4
- package/corpus/templates/content/actions/configure-document-property.ts +35 -1
- package/corpus/templates/content/actions/create-content-database.ts +5 -1
- package/corpus/templates/content/actions/delete-document-property.ts +52 -2
- package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
- package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
- package/corpus/templates/content/actions/set-document-property.ts +39 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
- package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
- package/corpus/templates/content/app/root.tsx +7 -1
- package/corpus/templates/content/server/db/schema.ts +30 -0
- package/corpus/templates/content/server/plugins/db.ts +87 -0
- package/corpus/templates/content/shared/api.ts +7 -0
- package/corpus/templates/content/shared/properties.ts +109 -0
- package/corpus/templates/design/CHANGELOG.md +10 -0
- package/corpus/templates/design/actions/generate-design.ts +4 -0
- package/corpus/templates/design/app/root.tsx +7 -1
- package/corpus/templates/design/app/routes/settings.tsx +10 -2
- package/corpus/templates/dispatch/CHANGELOG.md +10 -0
- package/corpus/templates/dispatch/app/root.tsx +7 -1
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
- package/corpus/templates/forms/CHANGELOG.md +10 -0
- package/corpus/templates/forms/actions/export-responses.ts +6 -0
- package/corpus/templates/forms/actions/list-responses.ts +2 -0
- package/corpus/templates/forms/actions/response-insights.ts +2 -0
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
- package/corpus/templates/forms/app/root.tsx +7 -1
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
- package/corpus/templates/forms/server/db/schema.ts +7 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
- package/corpus/templates/forms/server/lib/integrations.ts +76 -2
- package/corpus/templates/forms/server/plugins/db.ts +20 -0
- package/corpus/templates/forms/shared/types.ts +12 -0
- package/corpus/templates/macros/CHANGELOG.md +10 -0
- package/corpus/templates/macros/app/root.tsx +7 -1
- package/corpus/templates/mail/CHANGELOG.md +10 -0
- package/corpus/templates/mail/app/components/layout/CommandPalette.tsx +3 -0
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +27 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +31 -17
- package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -4
- package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +34 -24
- package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -4
- package/corpus/templates/plan/AGENTS.md +4 -0
- package/corpus/templates/plan/CHANGELOG.md +10 -0
- package/corpus/templates/plan/actions/view-screen.ts +5 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +98 -1
- package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
- package/corpus/templates/plan/app/pages/PlansPage.tsx +107 -7
- package/corpus/templates/plan/app/root.tsx +7 -1
- package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
- package/corpus/templates/plan/shared/share-attribution.ts +72 -0
- package/corpus/templates/slides/CHANGELOG.md +10 -0
- package/corpus/templates/slides/app/root.tsx +7 -1
- package/corpus/templates/videos/CHANGELOG.md +4 -151
- package/corpus/templates/videos/app/root.tsx +7 -1
- package/dist/action.js +87 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/engine/builder-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/model-config.d.ts +2 -2
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +0 -2
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +13 -0
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +44 -0
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/changelog/parse.d.ts +70 -0
- package/dist/changelog/parse.d.ts.map +1 -0
- package/dist/changelog/parse.js +200 -0
- package/dist/changelog/parse.js.map +1 -0
- package/dist/cli/changelog.d.ts +4 -0
- package/dist/cli/changelog.d.ts.map +1 -0
- package/dist/cli/changelog.js +166 -0
- package/dist/cli/changelog.js.map +1 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +22 -1
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -3
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +74 -47
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/CommandMenu.d.ts +17 -1
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +49 -13
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +43 -6
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/analytics.d.ts +18 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +177 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/changelog/Changelog.d.ts +47 -0
- package/dist/client/changelog/Changelog.d.ts.map +1 -0
- package/dist/client/changelog/Changelog.js +138 -0
- package/dist/client/changelog/Changelog.js.map +1 -0
- package/dist/client/client-surface.d.ts +17 -0
- package/dist/client/client-surface.d.ts.map +1 -0
- package/dist/client/client-surface.js +24 -0
- package/dist/client/client-surface.js.map +1 -0
- package/dist/client/composer/TiptapComposer.d.ts +26 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +27 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/feedback-context.d.ts +3 -0
- package/dist/client/feedback-context.d.ts.map +1 -1
- package/dist/client/feedback-context.js +2 -0
- package/dist/client/feedback-context.js.map +1 -1
- package/dist/client/index.d.ts +3 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
- package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.js +42 -1
- package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +2 -0
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +58 -10
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +21 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +43 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +197 -7
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/oauth-route.d.ts.map +1 -1
- package/dist/mcp/oauth-route.js +95 -12
- package/dist/mcp/oauth-route.js.map +1 -1
- package/dist/server/attribution.d.ts +83 -0
- package/dist/server/attribution.d.ts.map +1 -0
- package/dist/server/attribution.js +178 -0
- package/dist/server/attribution.js.map +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +7 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts +9 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +32 -2
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/builder-browser.d.ts +4 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +20 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +25 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +101 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +16 -2
- package/dist/server/google-oauth.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +35 -0
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/styles/agent-conversation.css +21 -0
- package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/docs/design/durable-agent-runs.md +217 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/corpus/templates/analytics/app/pages/About.tsx +0 -108
- package/corpus/templates/analytics/app/routes/about.tsx +0 -9
- /package/corpus/templates/analytics/app/routes/{dashboards.tsx → dashboards._index.tsx} +0 -0
|
@@ -115,6 +115,7 @@ type BrowserDiagnosticsData = {
|
|
|
115
115
|
|
|
116
116
|
type NativeRecordingStatus =
|
|
117
117
|
| "recording"
|
|
118
|
+
| "paused"
|
|
118
119
|
| "stopping"
|
|
119
120
|
| "uploading"
|
|
120
121
|
| "complete"
|
|
@@ -124,6 +125,7 @@ type NativeRecording = {
|
|
|
124
125
|
sessionId: string;
|
|
125
126
|
recordingId: string;
|
|
126
127
|
clipsBaseUrl: string;
|
|
128
|
+
uploadUrl: string;
|
|
127
129
|
targetTabId: number;
|
|
128
130
|
targetTitle: string | null;
|
|
129
131
|
targetUrl: string | null;
|
|
@@ -196,6 +198,243 @@ const sessions = new Map<string, CaptureSession>();
|
|
|
196
198
|
const tabToSession = new Map<number, string>();
|
|
197
199
|
let activeNativeRecording: NativeRecording | null = null;
|
|
198
200
|
|
|
201
|
+
// ----- Loom-style in-page overlay state -------------------------------------
|
|
202
|
+
// The service worker is the single source of truth for the recording phase and
|
|
203
|
+
// the elapsed-time base. The phase decides which overlay "parts" each tab should
|
|
204
|
+
// show; the timer base lets every overlay iframe tick the same clock locally.
|
|
205
|
+
|
|
206
|
+
type CaptureMode = "screen" | "camera";
|
|
207
|
+
type OverlayPhase = "idle" | "countdown" | "recording" | "paused" | "saving";
|
|
208
|
+
type OverlayPart = "bubble" | "countdown" | "toolbar" | "saving";
|
|
209
|
+
|
|
210
|
+
let overlayPhase: OverlayPhase = "idle";
|
|
211
|
+
let overlayBaseElapsedMs = 0;
|
|
212
|
+
let overlayBaseEpochMs = 0;
|
|
213
|
+
let overlayShowsBubble = false;
|
|
214
|
+
let countdownEndsAtMs = 0;
|
|
215
|
+
|
|
216
|
+
function desiredParts(): OverlayPart[] {
|
|
217
|
+
// The on-page controls match the desktop app: a left-edge vertical pill plus
|
|
218
|
+
// the face bubble. (Both are captured in full-screen/window recordings — same
|
|
219
|
+
// tradeoff as the desktop's on-screen controls; that's the intended UX.)
|
|
220
|
+
if (overlayPhase === "countdown") {
|
|
221
|
+
return overlayShowsBubble ? ["bubble", "countdown"] : ["countdown"];
|
|
222
|
+
}
|
|
223
|
+
if (overlayPhase === "recording" || overlayPhase === "paused") {
|
|
224
|
+
return overlayShowsBubble ? ["bubble", "toolbar"] : ["toolbar"];
|
|
225
|
+
}
|
|
226
|
+
if (overlayPhase === "saving") {
|
|
227
|
+
return ["saving"];
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function overlayStateForBroadcast(): {
|
|
233
|
+
phase: OverlayPhase;
|
|
234
|
+
baseElapsedMs: number;
|
|
235
|
+
baseEpochMs: number;
|
|
236
|
+
countdownEndsAtMs: number;
|
|
237
|
+
} {
|
|
238
|
+
return {
|
|
239
|
+
phase: overlayPhase,
|
|
240
|
+
baseElapsedMs: overlayBaseElapsedMs,
|
|
241
|
+
baseEpochMs: overlayBaseEpochMs,
|
|
242
|
+
countdownEndsAtMs,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The countdown clock lives here in the worker, not in the overlay. The overlay
|
|
247
|
+
// only *visualizes* it. This is critical: on chrome:// pages (and any page where
|
|
248
|
+
// the overlay can't be injected) the recorder must still start. Without this,
|
|
249
|
+
// recording would silently never begin on those pages.
|
|
250
|
+
const COUNTDOWN_SECONDS = 3;
|
|
251
|
+
|
|
252
|
+
function clearCountdownTimer(): void {
|
|
253
|
+
countdownEndsAtMs = 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Toggle the toolbar popup off while recording so clicking the extension icon
|
|
257
|
+
// fires onClicked (immediate stop & save) instead of opening the popup.
|
|
258
|
+
function setActionPopup(path: string): void {
|
|
259
|
+
try {
|
|
260
|
+
void chrome.action.setPopup({ popup: path });
|
|
261
|
+
} catch {
|
|
262
|
+
/* action API unavailable */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Reaches every extension-origin overlay iframe across all tabs at once.
|
|
267
|
+
function broadcastOverlayState(): void {
|
|
268
|
+
void persistOverlay();
|
|
269
|
+
try {
|
|
270
|
+
chrome.runtime.sendMessage(
|
|
271
|
+
{ type: "CLIPS_OVERLAY_STATE", state: overlayStateForBroadcast() },
|
|
272
|
+
() => void chrome.runtime.lastError,
|
|
273
|
+
);
|
|
274
|
+
} catch {
|
|
275
|
+
/* no overlay listening yet */
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// MV3 can suspend the service worker mid-recording, wiping these module vars.
|
|
280
|
+
// Persist them to session storage (survives suspend/revive within the browser
|
|
281
|
+
// session) and restore on worker startup so the overlay controls keep working.
|
|
282
|
+
function persistOverlay(): Promise<void> {
|
|
283
|
+
return sessionStorageSet({
|
|
284
|
+
overlayRuntime: {
|
|
285
|
+
phase: overlayPhase,
|
|
286
|
+
baseElapsedMs: overlayBaseElapsedMs,
|
|
287
|
+
baseEpochMs: overlayBaseEpochMs,
|
|
288
|
+
showsBubble: overlayShowsBubble,
|
|
289
|
+
countdownEndsAtMs,
|
|
290
|
+
},
|
|
291
|
+
}).catch(() => undefined);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function restoreRuntimeState(): Promise<void> {
|
|
295
|
+
const stored = await sessionStorageGet([
|
|
296
|
+
"activeNativeRecording",
|
|
297
|
+
"overlayRuntime",
|
|
298
|
+
]);
|
|
299
|
+
const rec = stored.activeNativeRecording as NativeRecording | undefined;
|
|
300
|
+
if (rec && typeof rec.sessionId === "string" && !activeNativeRecording) {
|
|
301
|
+
activeNativeRecording = rec;
|
|
302
|
+
}
|
|
303
|
+
const rt = stored.overlayRuntime as
|
|
304
|
+
| {
|
|
305
|
+
phase?: OverlayPhase;
|
|
306
|
+
baseElapsedMs?: number;
|
|
307
|
+
baseEpochMs?: number;
|
|
308
|
+
showsBubble?: boolean;
|
|
309
|
+
countdownEndsAtMs?: number;
|
|
310
|
+
}
|
|
311
|
+
| undefined;
|
|
312
|
+
if (rt && typeof rt.phase === "string" && overlayPhase === "idle") {
|
|
313
|
+
overlayPhase = rt.phase;
|
|
314
|
+
overlayBaseElapsedMs =
|
|
315
|
+
typeof rt.baseElapsedMs === "number" ? rt.baseElapsedMs : 0;
|
|
316
|
+
overlayBaseEpochMs =
|
|
317
|
+
typeof rt.baseEpochMs === "number" ? rt.baseEpochMs : 0;
|
|
318
|
+
overlayShowsBubble = Boolean(rt.showsBubble);
|
|
319
|
+
countdownEndsAtMs =
|
|
320
|
+
typeof rt.countdownEndsAtMs === "number" ? rt.countdownEndsAtMs : 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (overlayPhase !== "idle" && activeNativeRecording) {
|
|
324
|
+
// Recording survived a worker restart. The offscreen document owns the
|
|
325
|
+
// recorder + pre-roll timer, so it kept running; just restore immediate-stop
|
|
326
|
+
// mode. If the pre-roll already elapsed, assume the offscreen started the
|
|
327
|
+
// recorder so the icon click does Stop (not Discard).
|
|
328
|
+
setActionPopup("");
|
|
329
|
+
if (
|
|
330
|
+
overlayPhase === "countdown" &&
|
|
331
|
+
countdownEndsAtMs > 0 &&
|
|
332
|
+
nowMs() >= countdownEndsAtMs
|
|
333
|
+
) {
|
|
334
|
+
overlayPhase = "recording";
|
|
335
|
+
overlayBaseEpochMs = countdownEndsAtMs;
|
|
336
|
+
countdownEndsAtMs = 0;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function sendTabMessage(
|
|
342
|
+
tabId: number,
|
|
343
|
+
message: Record<string, unknown>,
|
|
344
|
+
): Promise<void> {
|
|
345
|
+
return new Promise((resolve) => {
|
|
346
|
+
try {
|
|
347
|
+
chrome.tabs.sendMessage(tabId, message, () => {
|
|
348
|
+
void chrome.runtime.lastError;
|
|
349
|
+
resolve();
|
|
350
|
+
});
|
|
351
|
+
} catch {
|
|
352
|
+
resolve();
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function ensureContentScript(tabId: number): Promise<void> {
|
|
358
|
+
const scripting = (
|
|
359
|
+
chrome as typeof chrome & {
|
|
360
|
+
scripting?: {
|
|
361
|
+
executeScript: (args: {
|
|
362
|
+
target: { tabId: number };
|
|
363
|
+
files: string[];
|
|
364
|
+
}) => Promise<unknown>;
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
).scripting;
|
|
368
|
+
if (!scripting) return;
|
|
369
|
+
await scripting
|
|
370
|
+
.executeScript({ target: { tabId }, files: ["assets/content-script.js"] })
|
|
371
|
+
.catch(() => undefined);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function mountOverlayOnTab(tabId: number): Promise<void> {
|
|
375
|
+
await ensureContentScript(tabId);
|
|
376
|
+
await sendTabMessage(tabId, {
|
|
377
|
+
type: "CLIPS_OVERLAY_MOUNT",
|
|
378
|
+
parts: desiredParts(),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function allTabs(): Promise<chrome.tabs.Tab[]> {
|
|
383
|
+
return new Promise((resolve) => {
|
|
384
|
+
chrome.tabs.query({}, (tabs) => resolve(tabs ?? []));
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function broadcastMount(): Promise<void> {
|
|
389
|
+
const parts = desiredParts();
|
|
390
|
+
const tabs = await allTabs();
|
|
391
|
+
await Promise.all(
|
|
392
|
+
tabs.map((tab) =>
|
|
393
|
+
typeof tab.id === "number"
|
|
394
|
+
? sendTabMessage(tab.id, { type: "CLIPS_OVERLAY_MOUNT", parts })
|
|
395
|
+
: Promise.resolve(),
|
|
396
|
+
),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function broadcastUnmount(): Promise<void> {
|
|
401
|
+
const tabs = await allTabs();
|
|
402
|
+
await Promise.all(
|
|
403
|
+
tabs.map((tab) =>
|
|
404
|
+
typeof tab.id === "number"
|
|
405
|
+
? sendTabMessage(tab.id, { type: "CLIPS_OVERLAY_UNMOUNT" })
|
|
406
|
+
: Promise.resolve(),
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function resetOverlay(): void {
|
|
412
|
+
overlayPhase = "idle";
|
|
413
|
+
overlayBaseElapsedMs = 0;
|
|
414
|
+
overlayBaseEpochMs = 0;
|
|
415
|
+
overlayShowsBubble = false;
|
|
416
|
+
clearCountdownTimer();
|
|
417
|
+
setRecordingFlag(false);
|
|
418
|
+
// Bring back the toolbar popup now that we're idle again.
|
|
419
|
+
setActionPopup("src/popup.html");
|
|
420
|
+
void persistOverlay();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Mirrored into chrome.storage.local so content scripts can cheaply tell whether
|
|
424
|
+
// a recording is in progress without waking the service worker on every page
|
|
425
|
+
// load. Content scripts can read storage.local by default; storage.session
|
|
426
|
+
// cannot, which is why we use local here.
|
|
427
|
+
function setRecordingFlag(active: boolean): void {
|
|
428
|
+
try {
|
|
429
|
+
chrome.storage.local.set(
|
|
430
|
+
{ clipsRecordingActive: active },
|
|
431
|
+
() => void chrome.runtime.lastError,
|
|
432
|
+
);
|
|
433
|
+
} catch {
|
|
434
|
+
/* storage unavailable */
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
199
438
|
function nowIso(): string {
|
|
200
439
|
return new Date().toISOString();
|
|
201
440
|
}
|
|
@@ -356,14 +595,32 @@ function originOf(raw: string | null | undefined): string | null {
|
|
|
356
595
|
}
|
|
357
596
|
}
|
|
358
597
|
|
|
598
|
+
// Auth lives in chrome.storage.local (not .session) so the user stays signed in
|
|
599
|
+
// across extension reloads and browser restarts. It is the user's own session
|
|
600
|
+
// token — treated like a cookie — and is cleared on sign-out or when invalid.
|
|
601
|
+
function localStorageSet(value: Record<string, unknown>): Promise<void> {
|
|
602
|
+
return new Promise((resolve) => {
|
|
603
|
+
chrome.storage.local.set(value, () => {
|
|
604
|
+
void chrome.runtime.lastError;
|
|
605
|
+
resolve();
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function localStorageGet(keys: string[]): Promise<Record<string, unknown>> {
|
|
611
|
+
return new Promise((resolve) => {
|
|
612
|
+
chrome.storage.local.get(keys, (value) => resolve(value ?? {}));
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
359
616
|
async function saveAuthSession(auth: StoredAuth): Promise<void> {
|
|
360
|
-
await
|
|
617
|
+
await localStorageSet({ clipsAuth: auth });
|
|
361
618
|
}
|
|
362
619
|
|
|
363
620
|
async function readAuthSession(
|
|
364
621
|
settings: ExtensionSettings,
|
|
365
622
|
): Promise<StoredAuth | null> {
|
|
366
|
-
const stored = await
|
|
623
|
+
const stored = await localStorageGet(["clipsAuth"]);
|
|
367
624
|
const auth = stored.clipsAuth as Partial<StoredAuth> | undefined;
|
|
368
625
|
if (
|
|
369
626
|
!auth ||
|
|
@@ -506,23 +763,6 @@ async function postAction<T>(
|
|
|
506
763
|
return (data ?? {}) as T;
|
|
507
764
|
}
|
|
508
765
|
|
|
509
|
-
function getTabMediaStreamId(tabId: number): Promise<string> {
|
|
510
|
-
return new Promise((resolve, reject) => {
|
|
511
|
-
chrome.tabCapture.getMediaStreamId({ targetTabId: tabId }, (streamId) => {
|
|
512
|
-
const error = chromeLastError();
|
|
513
|
-
if (error) {
|
|
514
|
-
reject(error);
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
if (!streamId) {
|
|
518
|
-
reject(new Error("Chrome did not provide a tab capture stream."));
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
resolve(streamId);
|
|
522
|
-
});
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
|
|
526
766
|
async function ensureOffscreenDocument(): Promise<void> {
|
|
527
767
|
const path = "src/offscreen.html";
|
|
528
768
|
const offscreenUrl = chrome.runtime.getURL(path);
|
|
@@ -568,38 +808,6 @@ function recordingUrl(
|
|
|
568
808
|
return `${recording.clipsBaseUrl}/r/${encodeURIComponent(recording.recordingId)}`;
|
|
569
809
|
}
|
|
570
810
|
|
|
571
|
-
function buildRecordUrl(
|
|
572
|
-
settings: ExtensionSettings,
|
|
573
|
-
sessionId: string,
|
|
574
|
-
tab: ChromeTab,
|
|
575
|
-
): string {
|
|
576
|
-
const recordUrl = new URL(`${settings.clipsBaseUrl}/record`);
|
|
577
|
-
const mode =
|
|
578
|
-
settings.captureSurface === "camera"
|
|
579
|
-
? "camera"
|
|
580
|
-
: settings.includeCamera
|
|
581
|
-
? "screen+camera"
|
|
582
|
-
: "screen";
|
|
583
|
-
const surface =
|
|
584
|
-
settings.captureSurface === "camera" ? "browser" : settings.captureSurface;
|
|
585
|
-
|
|
586
|
-
recordUrl.searchParams.set("mode", mode);
|
|
587
|
-
recordUrl.searchParams.set("surface", surface);
|
|
588
|
-
recordUrl.searchParams.set("clipsExtensionId", chrome.runtime.id);
|
|
589
|
-
recordUrl.searchParams.set("clipsCaptureSessionId", sessionId);
|
|
590
|
-
recordUrl.searchParams.set(
|
|
591
|
-
"developerLogs",
|
|
592
|
-
settings.includeDeveloperLogs ? "1" : "0",
|
|
593
|
-
);
|
|
594
|
-
recordUrl.searchParams.set(
|
|
595
|
-
"microphone",
|
|
596
|
-
settings.includeMicrophone ? "1" : "0",
|
|
597
|
-
);
|
|
598
|
-
if (tab.title) recordUrl.searchParams.set("sourceTitle", tab.title);
|
|
599
|
-
if (tab.url) recordUrl.searchParams.set("sourceUrl", tab.url);
|
|
600
|
-
return recordUrl.toString();
|
|
601
|
-
}
|
|
602
|
-
|
|
603
811
|
function createSession(
|
|
604
812
|
sessionId: string,
|
|
605
813
|
tab: ChromeTab,
|
|
@@ -650,61 +858,80 @@ async function handlePopupStart(message: PopupStartMessage) {
|
|
|
650
858
|
}
|
|
651
859
|
|
|
652
860
|
const sessionId = crypto.randomUUID();
|
|
653
|
-
|
|
654
|
-
settings.captureSurface === "browser" ||
|
|
655
|
-
settings.captureSurface === "camera"
|
|
656
|
-
) {
|
|
657
|
-
return startNativeRecording({ sessionId, tab, settings });
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
createSession(sessionId, tab, settings);
|
|
661
|
-
await createTab(buildRecordUrl(settings, sessionId, tab));
|
|
662
|
-
|
|
663
|
-
return { ok: true, sessionId };
|
|
861
|
+
return armRecording({ sessionId, tab, settings });
|
|
664
862
|
}
|
|
665
863
|
|
|
666
|
-
|
|
864
|
+
// Arm a Loom-style in-page recording: show the native picker, create the row,
|
|
865
|
+
// then hand the recorder its pre-roll delay. The MediaRecorder starts inside the
|
|
866
|
+
// offscreen document after that delay, which reports "recording" back here
|
|
867
|
+
// (markRecordingStarted) — reliable even when no overlay can be injected.
|
|
868
|
+
async function armRecording(args: {
|
|
667
869
|
sessionId: string;
|
|
668
870
|
tab: ChromeTab;
|
|
669
871
|
settings: ExtensionSettings;
|
|
670
872
|
}) {
|
|
671
873
|
const { sessionId, tab, settings } = args;
|
|
672
|
-
const
|
|
673
|
-
settings.captureSurface === "
|
|
674
|
-
|
|
675
|
-
|
|
874
|
+
const mode: CaptureMode =
|
|
875
|
+
settings.captureSurface === "camera" ? "camera" : "screen";
|
|
876
|
+
const surface: "browser" | "window" | "monitor" =
|
|
877
|
+
settings.captureSurface === "camera" ? "browser" : settings.captureSurface;
|
|
878
|
+
|
|
879
|
+
// Pre-warmed on popup open, so this is effectively instant and keeps the
|
|
880
|
+
// getDisplayMedia() call close to the user's click.
|
|
676
881
|
await ensureOffscreenDocument();
|
|
677
882
|
|
|
883
|
+
// 1) Native "Choose what to share" picker. Do this before any network round
|
|
884
|
+
// trip so the picker stays tied to the user gesture. Throws if cancelled.
|
|
885
|
+
await sendOffscreenMessage<{ ok: boolean; width: number; height: number }>({
|
|
886
|
+
type: "CLIPS_OFFSCREEN_ACQUIRE",
|
|
887
|
+
sessionId,
|
|
888
|
+
mode,
|
|
889
|
+
surface,
|
|
890
|
+
includeMicrophone: settings.includeMicrophone,
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// 2) Create the recording row now that we know the user picked a source.
|
|
678
894
|
type CreatedRecording = {
|
|
679
895
|
id?: string;
|
|
680
896
|
uploadChunkUrl?: string;
|
|
681
897
|
abortUrl?: string;
|
|
682
898
|
};
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
"create-recording",
|
|
686
|
-
{
|
|
899
|
+
let created: CreatedRecording;
|
|
900
|
+
try {
|
|
901
|
+
created = await postAction<CreatedRecording>(settings, "create-recording", {
|
|
687
902
|
title: tab.title || "Untitled recording",
|
|
688
903
|
titleSource: tab.title ? "context" : "default",
|
|
689
904
|
sourceAppName: "Chrome",
|
|
690
905
|
sourceWindowTitle: tab.title ?? null,
|
|
691
906
|
hasCamera: settings.captureSurface === "camera" || settings.includeCamera,
|
|
692
907
|
hasAudio:
|
|
693
|
-
settings.includeMicrophone || settings.captureSurface
|
|
908
|
+
settings.includeMicrophone || settings.captureSurface !== "camera",
|
|
694
909
|
visibility: "public",
|
|
695
|
-
}
|
|
696
|
-
)
|
|
910
|
+
});
|
|
911
|
+
} catch (err) {
|
|
912
|
+
await sendOffscreenMessage({
|
|
913
|
+
type: "CLIPS_OFFSCREEN_CANCEL",
|
|
914
|
+
sessionId,
|
|
915
|
+
}).catch(() => undefined);
|
|
916
|
+
throw err;
|
|
917
|
+
}
|
|
697
918
|
if (!created.id || !created.uploadChunkUrl) {
|
|
919
|
+
await sendOffscreenMessage({
|
|
920
|
+
type: "CLIPS_OFFSCREEN_CANCEL",
|
|
921
|
+
sessionId,
|
|
922
|
+
}).catch(() => undefined);
|
|
698
923
|
throw new Error("Clips did not create a recording target.");
|
|
699
924
|
}
|
|
700
925
|
|
|
701
926
|
const uploadUrl = `${settings.clipsBaseUrl}${created.uploadChunkUrl}`;
|
|
702
927
|
const session = createSession(sessionId, tab, settings);
|
|
703
928
|
session.recordingId = created.id;
|
|
929
|
+
|
|
704
930
|
activeNativeRecording = {
|
|
705
931
|
sessionId,
|
|
706
932
|
recordingId: created.id,
|
|
707
933
|
clipsBaseUrl: settings.clipsBaseUrl,
|
|
934
|
+
uploadUrl,
|
|
708
935
|
targetTabId: tab.id as number,
|
|
709
936
|
targetTitle: tab.title?.trim() || null,
|
|
710
937
|
targetUrl: tab.url?.trim() || null,
|
|
@@ -718,101 +945,250 @@ async function startNativeRecording(args: {
|
|
|
718
945
|
recordingUrl: `${settings.clipsBaseUrl}/r/${encodeURIComponent(created.id)}`,
|
|
719
946
|
error: null,
|
|
720
947
|
};
|
|
721
|
-
await saveActiveNativeRecording();
|
|
722
948
|
|
|
949
|
+
// 3) Enter the countdown phase and show the overlay on the launch tab. The
|
|
950
|
+
// camera bubble shows whenever a camera is involved: as the picture-in-
|
|
951
|
+
// picture for screen+camera, or as a live self-preview for camera-only.
|
|
952
|
+
overlayPhase = "countdown";
|
|
953
|
+
overlayBaseElapsedMs = 0;
|
|
954
|
+
overlayBaseEpochMs = nowMs();
|
|
955
|
+
overlayShowsBubble = mode === "camera" || settings.includeCamera;
|
|
956
|
+
countdownEndsAtMs = nowMs() + COUNTDOWN_SECONDS * 1000;
|
|
957
|
+
setRecordingFlag(true);
|
|
958
|
+
// Clicking the icon now stops & saves immediately instead of opening the popup.
|
|
959
|
+
setActionPopup("");
|
|
960
|
+
|
|
961
|
+
// Hand the recorder its targets plus the pre-roll delay. The offscreen document
|
|
962
|
+
// (a reliable context, unlike the suspendable worker) owns the countdown timer
|
|
963
|
+
// and reports "recording" back when the recorder actually starts — that status
|
|
964
|
+
// flip (markRecordingStarted) advances our phase, so recording begins even on
|
|
965
|
+
// pages where no overlay can be injected (chrome://, the New Tab page, etc.).
|
|
966
|
+
const authToken = (await readAuthSession(settings))?.token;
|
|
723
967
|
try {
|
|
724
|
-
await sendOffscreenMessage
|
|
725
|
-
|
|
726
|
-
recordingId: string;
|
|
727
|
-
width: number;
|
|
728
|
-
height: number;
|
|
729
|
-
hasAudio: boolean;
|
|
730
|
-
hasCamera: boolean;
|
|
731
|
-
}>({
|
|
732
|
-
type: "CLIPS_OFFSCREEN_START",
|
|
968
|
+
await sendOffscreenMessage({
|
|
969
|
+
type: "CLIPS_OFFSCREEN_BEGIN",
|
|
733
970
|
sessionId,
|
|
734
971
|
recordingId: created.id,
|
|
735
972
|
uploadUrl,
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
includeMicrophone: settings.includeMicrophone,
|
|
740
|
-
title: tab.title ?? null,
|
|
973
|
+
hasCamera: mode === "camera" || settings.includeCamera,
|
|
974
|
+
startDelayMs: COUNTDOWN_SECONDS * 1000,
|
|
975
|
+
authToken,
|
|
741
976
|
});
|
|
742
|
-
await attachSession(session);
|
|
743
|
-
await chrome.action.setBadgeBackgroundColor({ color: "#e11d48" });
|
|
744
|
-
await chrome.action.setBadgeText({ text: "REC" });
|
|
745
|
-
return {
|
|
746
|
-
ok: true,
|
|
747
|
-
sessionId,
|
|
748
|
-
recordingId: created.id,
|
|
749
|
-
native: true,
|
|
750
|
-
};
|
|
751
977
|
} catch (err) {
|
|
752
|
-
await
|
|
753
|
-
activeNativeRecording = null;
|
|
754
|
-
await saveActiveNativeRecording();
|
|
755
|
-
await chrome.action.setBadgeText({ text: "" });
|
|
756
|
-
await postAction(settings, "trash-recording", { id: created.id }).catch(
|
|
757
|
-
() => undefined,
|
|
758
|
-
);
|
|
978
|
+
await cancelRecording();
|
|
759
979
|
throw err;
|
|
760
980
|
}
|
|
981
|
+
await saveActiveNativeRecording();
|
|
982
|
+
|
|
983
|
+
// Browser-tab diagnostics still attach to the launch tab only.
|
|
984
|
+
if (settings.captureSurface === "browser") {
|
|
985
|
+
await attachSession(session);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
await mountOverlayOnTab(tab.id as number);
|
|
989
|
+
broadcastOverlayState();
|
|
990
|
+
await chrome.action.setBadgeBackgroundColor({ color: "#e11d48" });
|
|
991
|
+
await chrome.action.setBadgeText({ text: "REC" });
|
|
992
|
+
|
|
993
|
+
return { ok: true, sessionId, recordingId: created.id, native: true };
|
|
761
994
|
}
|
|
762
995
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
996
|
+
// Called when the offscreen recorder reports it actually started (after its
|
|
997
|
+
// pre-roll countdown). Advances phase countdown -> recording and starts the
|
|
998
|
+
// on-page timer. Idempotent.
|
|
999
|
+
async function markRecordingStarted() {
|
|
1000
|
+
if (overlayPhase !== "countdown") return;
|
|
1001
|
+
clearCountdownTimer();
|
|
1002
|
+
overlayPhase = "recording";
|
|
1003
|
+
overlayBaseElapsedMs = 0;
|
|
1004
|
+
overlayBaseEpochMs = nowMs();
|
|
1005
|
+
if (activeNativeRecording) {
|
|
1006
|
+
activeNativeRecording.startedAtMs = overlayBaseEpochMs;
|
|
1007
|
+
activeNativeRecording.startedAt = new Date(
|
|
1008
|
+
overlayBaseEpochMs,
|
|
1009
|
+
).toISOString();
|
|
1010
|
+
activeNativeRecording.status = "recording";
|
|
1011
|
+
await saveActiveNativeRecording();
|
|
1012
|
+
}
|
|
1013
|
+
await broadcastMount();
|
|
1014
|
+
broadcastOverlayState();
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Skip the pre-roll: tell the offscreen recorder to start immediately. It will
|
|
1018
|
+
// report "recording", which flips our phase via markRecordingStarted.
|
|
1019
|
+
async function handleOverlaySkip() {
|
|
1020
|
+
if (overlayPhase !== "countdown" || !activeNativeRecording) {
|
|
1021
|
+
return { ok: true };
|
|
1022
|
+
}
|
|
1023
|
+
await sendOffscreenMessage({
|
|
1024
|
+
type: "CLIPS_OFFSCREEN_START_NOW",
|
|
1025
|
+
sessionId: activeNativeRecording.sessionId,
|
|
1026
|
+
}).catch(() => undefined);
|
|
1027
|
+
return { ok: true };
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function handleOverlayPause() {
|
|
1031
|
+
if (overlayPhase !== "recording") return { ok: true };
|
|
1032
|
+
overlayBaseElapsedMs += Math.max(0, nowMs() - overlayBaseEpochMs);
|
|
1033
|
+
overlayPhase = "paused";
|
|
1034
|
+
if (activeNativeRecording) {
|
|
1035
|
+
activeNativeRecording.status = "paused";
|
|
1036
|
+
void saveActiveNativeRecording();
|
|
1037
|
+
void sendOffscreenMessage({
|
|
1038
|
+
type: "CLIPS_OFFSCREEN_PAUSE",
|
|
1039
|
+
sessionId: activeNativeRecording.sessionId,
|
|
1040
|
+
}).catch(() => undefined);
|
|
1041
|
+
}
|
|
1042
|
+
broadcastOverlayState();
|
|
1043
|
+
return { ok: true };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function handleOverlayResume() {
|
|
1047
|
+
if (overlayPhase !== "paused") return { ok: true };
|
|
1048
|
+
overlayBaseEpochMs = nowMs();
|
|
1049
|
+
overlayPhase = "recording";
|
|
1050
|
+
if (activeNativeRecording) {
|
|
1051
|
+
activeNativeRecording.status = "recording";
|
|
1052
|
+
void saveActiveNativeRecording();
|
|
1053
|
+
void sendOffscreenMessage({
|
|
1054
|
+
type: "CLIPS_OFFSCREEN_RESUME",
|
|
1055
|
+
sessionId: activeNativeRecording.sessionId,
|
|
1056
|
+
}).catch(() => undefined);
|
|
1057
|
+
}
|
|
1058
|
+
broadcastOverlayState();
|
|
1059
|
+
return { ok: true };
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Restart: discard the in-progress capture but keep the same screen selection,
|
|
1063
|
+
// reset the server-side chunks, then replay the countdown.
|
|
1064
|
+
async function handleOverlayRestart() {
|
|
1065
|
+
const recording = activeNativeRecording;
|
|
1066
|
+
if (!recording) return { ok: true };
|
|
1067
|
+
try {
|
|
1068
|
+
await sendOffscreenMessage({
|
|
1069
|
+
type: "CLIPS_OFFSCREEN_RESTART",
|
|
1070
|
+
sessionId: recording.sessionId,
|
|
1071
|
+
});
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
return {
|
|
1074
|
+
ok: false,
|
|
1075
|
+
error: err instanceof Error ? err.message : "Could not restart.",
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
// If the previous take's chunks could not be cleared, do NOT re-arm with the
|
|
1079
|
+
// same recordingId — finalize would otherwise assemble stale chunk keys from
|
|
1080
|
+
// the aborted take into the restarted recording. Surface the failure instead.
|
|
1081
|
+
const chunksReset = await resetRecordingChunks(recording);
|
|
1082
|
+
if (!chunksReset) {
|
|
1083
|
+
return {
|
|
1084
|
+
ok: false,
|
|
1085
|
+
error:
|
|
1086
|
+
"Could not clear the previous take before restarting. Stop and start a new recording.",
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
overlayPhase = "countdown";
|
|
1090
|
+
overlayBaseElapsedMs = 0;
|
|
1091
|
+
overlayBaseEpochMs = nowMs();
|
|
1092
|
+
countdownEndsAtMs = nowMs() + COUNTDOWN_SECONDS * 1000;
|
|
1093
|
+
recording.status = "recording";
|
|
1094
|
+
const restartAuthToken = (
|
|
1095
|
+
await readAuthSession({
|
|
772
1096
|
clipsBaseUrl: recording.clipsBaseUrl,
|
|
773
1097
|
captureSurface: recording.captureSurface,
|
|
774
1098
|
includeCamera: recording.includeCamera,
|
|
775
1099
|
includeMicrophone: recording.includeMicrophone,
|
|
776
1100
|
includeDeveloperLogs: recording.includeDeveloperLogs,
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1101
|
+
})
|
|
1102
|
+
)?.token;
|
|
1103
|
+
// Re-arm the recorder on the same (re-homed) source streams with a fresh
|
|
1104
|
+
// pre-roll. The offscreen reports "recording" when it restarts.
|
|
1105
|
+
try {
|
|
1106
|
+
await sendOffscreenMessage({
|
|
1107
|
+
type: "CLIPS_OFFSCREEN_BEGIN",
|
|
781
1108
|
sessionId: recording.sessionId,
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1109
|
+
recordingId: recording.recordingId,
|
|
1110
|
+
uploadUrl: recording.uploadUrl,
|
|
1111
|
+
hasCamera:
|
|
1112
|
+
recording.captureSurface === "camera" || recording.includeCamera,
|
|
1113
|
+
startDelayMs: COUNTDOWN_SECONDS * 1000,
|
|
1114
|
+
authToken: restartAuthToken,
|
|
1115
|
+
});
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
// Re-arming failed: tear the countdown overlay back down and report the
|
|
1118
|
+
// failure instead of leaving the user on a pre-roll with no recorder.
|
|
1119
|
+
recording.status = "error";
|
|
1120
|
+
recording.error =
|
|
1121
|
+
err instanceof Error ? err.message : "Could not restart the recorder.";
|
|
1122
|
+
resetOverlay();
|
|
1123
|
+
await saveActiveNativeRecording();
|
|
1124
|
+
await broadcastUnmount();
|
|
1125
|
+
broadcastOverlayState();
|
|
1126
|
+
return { ok: false, error: recording.error };
|
|
1127
|
+
}
|
|
1128
|
+
await saveActiveNativeRecording();
|
|
1129
|
+
await broadcastMount();
|
|
1130
|
+
broadcastOverlayState();
|
|
1131
|
+
return { ok: true };
|
|
794
1132
|
}
|
|
795
1133
|
|
|
796
|
-
async function
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1134
|
+
async function resetRecordingChunks(
|
|
1135
|
+
recording: NativeRecording,
|
|
1136
|
+
): Promise<boolean> {
|
|
1137
|
+
const url = `${recording.clipsBaseUrl}/api/uploads/${encodeURIComponent(
|
|
1138
|
+
recording.recordingId,
|
|
1139
|
+
)}/reset-chunks`;
|
|
1140
|
+
const headers = await authHeaders({
|
|
1141
|
+
clipsBaseUrl: recording.clipsBaseUrl,
|
|
1142
|
+
captureSurface: recording.captureSurface,
|
|
1143
|
+
includeCamera: recording.includeCamera,
|
|
1144
|
+
includeMicrophone: recording.includeMicrophone,
|
|
1145
|
+
includeDeveloperLogs: recording.includeDeveloperLogs,
|
|
1146
|
+
});
|
|
1147
|
+
const response = await fetch(url, {
|
|
1148
|
+
method: "POST",
|
|
1149
|
+
headers,
|
|
1150
|
+
credentials: "include",
|
|
1151
|
+
cache: "no-store",
|
|
1152
|
+
}).catch(() => undefined);
|
|
1153
|
+
return Boolean(response?.ok);
|
|
800
1154
|
}
|
|
801
1155
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1156
|
+
// Finalize a successful save: open the clip and tear down the "Saving…" overlay.
|
|
1157
|
+
// Guarded + claims the phase synchronously so it runs exactly once even if both
|
|
1158
|
+
// the STOP response and the offscreen "complete" status race to call it (the
|
|
1159
|
+
// latter is the safety net when the worker was suspended mid-upload).
|
|
1160
|
+
async function finishSaving(
|
|
1161
|
+
recording: NativeRecording,
|
|
1162
|
+
recordingIdFromStatus?: string,
|
|
1163
|
+
): Promise<boolean> {
|
|
1164
|
+
if (overlayPhase !== "saving") return false;
|
|
1165
|
+
resetOverlay(); // first statement sets overlayPhase = "idle" synchronously
|
|
1166
|
+
recording.status = "complete";
|
|
1167
|
+
recording.error = null;
|
|
1168
|
+
if (recordingIdFromStatus) recording.recordingId = recordingIdFromStatus;
|
|
1169
|
+
recording.recordingUrl = recordingUrl(recording);
|
|
1170
|
+
await saveNativeDiagnostics(recording);
|
|
1171
|
+
await deleteSession(recording.sessionId);
|
|
1172
|
+
await broadcastUnmount();
|
|
1173
|
+
broadcastOverlayState();
|
|
1174
|
+
await clearNativeRecording();
|
|
1175
|
+
await createTab(recording.recordingUrl);
|
|
1176
|
+
return true;
|
|
807
1177
|
}
|
|
808
1178
|
|
|
809
|
-
async function
|
|
1179
|
+
async function stopRecording() {
|
|
810
1180
|
const recording = activeNativeRecording;
|
|
811
|
-
if (!recording) {
|
|
812
|
-
return { ok: false, error: "No active Clips recording." };
|
|
813
|
-
}
|
|
1181
|
+
if (!recording) return { ok: false, error: "No active Clips recording." };
|
|
814
1182
|
recording.status = "stopping";
|
|
1183
|
+
// Keep an overlay up: swap the recording controls/bubble for a "Saving…" card
|
|
1184
|
+
// so the user has feedback during the upload gap (instead of everything
|
|
1185
|
+
// vanishing while the clip uploads invisibly). Mirrors the desktop Finalizing
|
|
1186
|
+
// overlay. The popup stays disabled so the icon can't interrupt the save.
|
|
1187
|
+
overlayPhase = "saving";
|
|
1188
|
+
countdownEndsAtMs = 0;
|
|
815
1189
|
await saveActiveNativeRecording();
|
|
1190
|
+
await broadcastMount();
|
|
1191
|
+
broadcastOverlayState();
|
|
816
1192
|
try {
|
|
817
1193
|
const response = await sendOffscreenMessage<{
|
|
818
1194
|
ok?: boolean;
|
|
@@ -821,16 +1197,33 @@ async function handlePopupStop() {
|
|
|
821
1197
|
type: "CLIPS_OFFSCREEN_STOP",
|
|
822
1198
|
sessionId: recording.sessionId,
|
|
823
1199
|
});
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1200
|
+
if (response.result && response.result.status === "cancelled") {
|
|
1201
|
+
// Stopped during the pre-roll countdown: no media was captured. Treat it
|
|
1202
|
+
// as an aborted take — discard the empty recording instead of opening a
|
|
1203
|
+
// playback tab for a finished-but-empty clip.
|
|
1204
|
+
await deleteSession(recording.sessionId);
|
|
1205
|
+
await postAction(
|
|
1206
|
+
{
|
|
1207
|
+
clipsBaseUrl: recording.clipsBaseUrl,
|
|
1208
|
+
captureSurface: recording.captureSurface,
|
|
1209
|
+
includeCamera: recording.includeCamera,
|
|
1210
|
+
includeMicrophone: recording.includeMicrophone,
|
|
1211
|
+
includeDeveloperLogs: recording.includeDeveloperLogs,
|
|
1212
|
+
},
|
|
1213
|
+
"trash-recording",
|
|
1214
|
+
{ id: recording.recordingId },
|
|
1215
|
+
).catch(() => undefined);
|
|
1216
|
+
resetOverlay();
|
|
1217
|
+
await broadcastUnmount();
|
|
1218
|
+
broadcastOverlayState();
|
|
1219
|
+
await clearNativeRecording();
|
|
1220
|
+
return { ok: true, cancelled: true };
|
|
828
1221
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
await
|
|
1222
|
+
const recordingId =
|
|
1223
|
+
response.result && typeof response.result.recordingId === "string"
|
|
1224
|
+
? response.result.recordingId
|
|
1225
|
+
: undefined;
|
|
1226
|
+
await finishSaving(recording, recordingId);
|
|
834
1227
|
return {
|
|
835
1228
|
ok: true,
|
|
836
1229
|
recordingId: recording.recordingId,
|
|
@@ -840,14 +1233,24 @@ async function handlePopupStop() {
|
|
|
840
1233
|
recording.status = "error";
|
|
841
1234
|
recording.error =
|
|
842
1235
|
err instanceof Error ? err.message : "Could not stop recording.";
|
|
1236
|
+
resetOverlay();
|
|
1237
|
+
await broadcastUnmount();
|
|
1238
|
+
broadcastOverlayState();
|
|
843
1239
|
await saveActiveNativeRecording();
|
|
844
1240
|
return { ok: false, error: recording.error };
|
|
845
1241
|
}
|
|
846
1242
|
}
|
|
847
1243
|
|
|
848
|
-
async function
|
|
1244
|
+
async function cancelRecording() {
|
|
849
1245
|
const recording = activeNativeRecording;
|
|
850
|
-
|
|
1246
|
+
resetOverlay();
|
|
1247
|
+
await broadcastUnmount();
|
|
1248
|
+
broadcastOverlayState();
|
|
1249
|
+
await chrome.action.setBadgeText({ text: "" });
|
|
1250
|
+
if (!recording) {
|
|
1251
|
+
await clearNativeRecording();
|
|
1252
|
+
return { ok: true };
|
|
1253
|
+
}
|
|
851
1254
|
await sendOffscreenMessage({
|
|
852
1255
|
type: "CLIPS_OFFSCREEN_CANCEL",
|
|
853
1256
|
sessionId: recording.sessionId,
|
|
@@ -868,6 +1271,60 @@ async function handlePopupCancel() {
|
|
|
868
1271
|
return { ok: true };
|
|
869
1272
|
}
|
|
870
1273
|
|
|
1274
|
+
async function saveNativeDiagnostics(
|
|
1275
|
+
recording: NativeRecording,
|
|
1276
|
+
): Promise<void> {
|
|
1277
|
+
if (!recording.includeDeveloperLogs) return;
|
|
1278
|
+
const session = sessions.get(recording.sessionId);
|
|
1279
|
+
if (!session) return;
|
|
1280
|
+
const diagnostics = snapshotSession(session);
|
|
1281
|
+
await postAction(
|
|
1282
|
+
{
|
|
1283
|
+
clipsBaseUrl: recording.clipsBaseUrl,
|
|
1284
|
+
captureSurface: recording.captureSurface,
|
|
1285
|
+
includeCamera: recording.includeCamera,
|
|
1286
|
+
includeMicrophone: recording.includeMicrophone,
|
|
1287
|
+
includeDeveloperLogs: recording.includeDeveloperLogs,
|
|
1288
|
+
},
|
|
1289
|
+
"save-browser-diagnostics",
|
|
1290
|
+
{
|
|
1291
|
+
recordingId: recording.recordingId,
|
|
1292
|
+
sessionId: recording.sessionId,
|
|
1293
|
+
source: "extension",
|
|
1294
|
+
phase: "recording",
|
|
1295
|
+
pageUrl: diagnostics.pageUrl,
|
|
1296
|
+
userAgent: diagnostics.userAgent,
|
|
1297
|
+
startedAt: diagnostics.startedAt,
|
|
1298
|
+
endedAt: diagnostics.endedAt,
|
|
1299
|
+
consoleLogs: diagnostics.consoleLogs,
|
|
1300
|
+
networkRequests: diagnostics.networkRequests,
|
|
1301
|
+
},
|
|
1302
|
+
).catch((err) => {
|
|
1303
|
+
console.warn("[clips-extension] diagnostics save failed:", err);
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
async function clearNativeRecording(): Promise<void> {
|
|
1308
|
+
activeNativeRecording = null;
|
|
1309
|
+
await saveActiveNativeRecording();
|
|
1310
|
+
await chrome.action.setBadgeText({ text: "" });
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async function handlePopupStatus() {
|
|
1314
|
+
return {
|
|
1315
|
+
ok: true,
|
|
1316
|
+
activeRecording: activeNativeRecording,
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
async function handlePopupStop() {
|
|
1321
|
+
return stopRecording();
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async function handlePopupCancel() {
|
|
1325
|
+
return cancelRecording();
|
|
1326
|
+
}
|
|
1327
|
+
|
|
871
1328
|
async function handlePopupOpen() {
|
|
872
1329
|
const recording = activeNativeRecording;
|
|
873
1330
|
if (!recording) return { ok: false, error: "No active recording." };
|
|
@@ -1328,13 +1785,61 @@ async function handleExternalMessage(
|
|
|
1328
1785
|
return { ok: false, error: "Unknown message." };
|
|
1329
1786
|
}
|
|
1330
1787
|
|
|
1331
|
-
chrome.runtime.onInstalled.addListener(() => {
|
|
1788
|
+
chrome.runtime.onInstalled.addListener((details) => {
|
|
1332
1789
|
void readSettings().then((settings) => storageSet(settings));
|
|
1790
|
+
// Ask for camera/mic once on install so the grant is ready before the first
|
|
1791
|
+
// recording (the record-time gate in the popup is the fallback).
|
|
1792
|
+
if (details.reason === "install") {
|
|
1793
|
+
void createTab(chrome.runtime.getURL("src/permission.html")).catch(
|
|
1794
|
+
() => undefined,
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1333
1797
|
});
|
|
1334
1798
|
|
|
1799
|
+
// Restore recording/overlay state whenever the service worker (re)starts so an
|
|
1800
|
+
// in-progress recording survives MV3 worker suspension. Memoized so every entry
|
|
1801
|
+
// point can `await ensureRestored()` before reading activeNativeRecording — the
|
|
1802
|
+
// worker can be revived by the very event (icon click, status message) that
|
|
1803
|
+
// needs the state, and the module globals start out empty until restore runs.
|
|
1804
|
+
let restorePromise: Promise<void> | null = null;
|
|
1805
|
+
function ensureRestored(): Promise<void> {
|
|
1806
|
+
if (!restorePromise) {
|
|
1807
|
+
restorePromise = restoreRuntimeState().catch(() => undefined);
|
|
1808
|
+
}
|
|
1809
|
+
return restorePromise;
|
|
1810
|
+
}
|
|
1811
|
+
void ensureRestored();
|
|
1812
|
+
|
|
1335
1813
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
1336
1814
|
if (!message || typeof message !== "object") return false;
|
|
1337
|
-
|
|
1815
|
+
void (async () => {
|
|
1816
|
+
// Critical: never read activeNativeRecording/overlayPhase before state is
|
|
1817
|
+
// restored, or a freshly-woken worker drops the message (e.g. the offscreen
|
|
1818
|
+
// "recording" status, or an overlay Stop click).
|
|
1819
|
+
await ensureRestored();
|
|
1820
|
+
let response: unknown;
|
|
1821
|
+
try {
|
|
1822
|
+
response = await dispatchRuntimeMessage(message);
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
response = {
|
|
1825
|
+
ok: false,
|
|
1826
|
+
error: err instanceof Error ? err.message : "Could not use Clips.",
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
try {
|
|
1830
|
+
sendResponse(response);
|
|
1831
|
+
} catch {
|
|
1832
|
+
/* message port already closed (fire-and-forget sender) */
|
|
1833
|
+
}
|
|
1834
|
+
})();
|
|
1835
|
+
return true;
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
|
|
1839
|
+
const type = (message as { type?: unknown }).type;
|
|
1840
|
+
|
|
1841
|
+
// Status updates streamed from the offscreen recorder.
|
|
1842
|
+
if (type === "CLIPS_NATIVE_STATUS") {
|
|
1338
1843
|
const status = message as OffscreenStatusMessage;
|
|
1339
1844
|
if (
|
|
1340
1845
|
activeNativeRecording &&
|
|
@@ -1349,35 +1854,119 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
|
1349
1854
|
activeNativeRecording,
|
|
1350
1855
|
);
|
|
1351
1856
|
}
|
|
1352
|
-
|
|
1857
|
+
await saveActiveNativeRecording();
|
|
1858
|
+
// The offscreen recorder finished its pre-roll and actually started —
|
|
1859
|
+
// advance from countdown to recording (the reliable phase flip).
|
|
1860
|
+
if (status.status === "recording" && overlayPhase === "countdown") {
|
|
1861
|
+
await markRecordingStarted();
|
|
1862
|
+
}
|
|
1863
|
+
// Safety net: if the worker was suspended during the upload, stopRecording's
|
|
1864
|
+
// await was lost — finalize here when the offscreen reports completion.
|
|
1865
|
+
if (status.status === "complete" && overlayPhase === "saving") {
|
|
1866
|
+
await finishSaving(
|
|
1867
|
+
activeNativeRecording,
|
|
1868
|
+
typeof status.recordingId === "string"
|
|
1869
|
+
? status.recordingId
|
|
1870
|
+
: undefined,
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
if (status.status === "error") {
|
|
1874
|
+
resetOverlay();
|
|
1875
|
+
await broadcastUnmount();
|
|
1876
|
+
await chrome.action.setBadgeText({ text: "" });
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return { ok: true };
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// The user stopped sharing from Chrome's native control bar.
|
|
1883
|
+
if (type === "CLIPS_NATIVE_ENDED") {
|
|
1884
|
+
const sessionId = (message as { sessionId?: string }).sessionId;
|
|
1885
|
+
if (
|
|
1886
|
+
activeNativeRecording &&
|
|
1887
|
+
activeNativeRecording.sessionId === sessionId &&
|
|
1888
|
+
overlayPhase !== "idle" &&
|
|
1889
|
+
overlayPhase !== "saving"
|
|
1890
|
+
) {
|
|
1891
|
+
return stopRecording();
|
|
1353
1892
|
}
|
|
1354
|
-
return
|
|
1893
|
+
return { ok: true };
|
|
1355
1894
|
}
|
|
1356
1895
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
task = handlePopupStart(popupMessage);
|
|
1361
|
-
} else if (popupMessage.type === "CLIPS_POPUP_STATUS") {
|
|
1362
|
-
task = handlePopupStatus();
|
|
1363
|
-
} else if (popupMessage.type === "CLIPS_POPUP_STOP") {
|
|
1364
|
-
task = handlePopupStop();
|
|
1365
|
-
} else if (popupMessage.type === "CLIPS_POPUP_CANCEL") {
|
|
1366
|
-
task = handlePopupCancel();
|
|
1367
|
-
} else if (popupMessage.type === "CLIPS_POPUP_OPEN") {
|
|
1368
|
-
task = handlePopupOpen();
|
|
1369
|
-
} else if (popupMessage.type === "CLIPS_POPUP_SIGN_IN") {
|
|
1370
|
-
task = handlePopupSignIn(popupMessage);
|
|
1896
|
+
// Content script asking which overlay parts to show on its page.
|
|
1897
|
+
if (type === "CLIPS_CONTENT_HELLO") {
|
|
1898
|
+
return { ok: true, parts: desiredParts() };
|
|
1371
1899
|
}
|
|
1372
|
-
if (!task) return false;
|
|
1373
1900
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1901
|
+
// Overlay iframe handshake — rebroadcast the current timer/phase to it.
|
|
1902
|
+
if (type === "CLIPS_OVERLAY_HELLO") {
|
|
1903
|
+
broadcastOverlayState();
|
|
1904
|
+
return { ok: true, state: overlayStateForBroadcast() };
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
if (type === "CLIPS_PREWARM") {
|
|
1908
|
+
void ensureOffscreenDocument().catch(() => undefined);
|
|
1909
|
+
return { ok: true };
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
switch (type) {
|
|
1913
|
+
case "CLIPS_POPUP_START":
|
|
1914
|
+
return handlePopupStart(message as PopupStartMessage);
|
|
1915
|
+
case "CLIPS_POPUP_STATUS":
|
|
1916
|
+
return handlePopupStatus();
|
|
1917
|
+
case "CLIPS_POPUP_STOP":
|
|
1918
|
+
return handlePopupStop();
|
|
1919
|
+
case "CLIPS_POPUP_CANCEL":
|
|
1920
|
+
return handlePopupCancel();
|
|
1921
|
+
case "CLIPS_POPUP_OPEN":
|
|
1922
|
+
return handlePopupOpen();
|
|
1923
|
+
case "CLIPS_POPUP_SIGN_IN":
|
|
1924
|
+
return handlePopupSignIn(
|
|
1925
|
+
message as { settings?: Partial<ExtensionSettings> },
|
|
1926
|
+
);
|
|
1927
|
+
case "CLIPS_OVERLAY_COUNTDOWN_DONE":
|
|
1928
|
+
// Skip button on the countdown overlay: start the recorder now.
|
|
1929
|
+
return handleOverlaySkip();
|
|
1930
|
+
case "CLIPS_OVERLAY_PAUSE":
|
|
1931
|
+
return handleOverlayPause();
|
|
1932
|
+
case "CLIPS_OVERLAY_RESUME":
|
|
1933
|
+
return handleOverlayResume();
|
|
1934
|
+
case "CLIPS_OVERLAY_STOP":
|
|
1935
|
+
return stopRecording();
|
|
1936
|
+
case "CLIPS_OVERLAY_CANCEL":
|
|
1937
|
+
return cancelRecording();
|
|
1938
|
+
case "CLIPS_OVERLAY_RESTART":
|
|
1939
|
+
return handleOverlayRestart();
|
|
1940
|
+
default:
|
|
1941
|
+
return { ok: false, error: "Unknown message." };
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// While a recording is active, keep the overlay following the user as they
|
|
1946
|
+
// switch tabs (programmatic injection covers tabs opened before the extension
|
|
1947
|
+
// loaded; declared content scripts cover navigations).
|
|
1948
|
+
chrome.tabs.onActivated.addListener((info) => {
|
|
1949
|
+
void (async () => {
|
|
1950
|
+
await ensureRestored();
|
|
1951
|
+
if (overlayPhase === "idle" || !activeNativeRecording) return;
|
|
1952
|
+
await mountOverlayOnTab(info.tabId);
|
|
1953
|
+
})();
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
// While recording, the popup is disabled (setActionPopup("")), so clicking the
|
|
1957
|
+
// extension icon lands here: stop & save immediately (or discard if we're still
|
|
1958
|
+
// in the pre-roll countdown). When idle, the popup opens and this never fires.
|
|
1959
|
+
chrome.action.onClicked.addListener(() => {
|
|
1960
|
+
void (async () => {
|
|
1961
|
+
// Await restore — the click may have woken the worker, leaving the module
|
|
1962
|
+
// globals empty until this resolves. Without it, Stop silently does nothing.
|
|
1963
|
+
await ensureRestored();
|
|
1964
|
+
if (!activeNativeRecording) return;
|
|
1965
|
+
if (overlayPhase === "countdown") await cancelRecording();
|
|
1966
|
+
else if (overlayPhase === "recording" || overlayPhase === "paused") {
|
|
1967
|
+
await stopRecording();
|
|
1968
|
+
}
|
|
1969
|
+
})();
|
|
1381
1970
|
});
|
|
1382
1971
|
|
|
1383
1972
|
chrome.runtime.onMessageExternal.addListener(
|