@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
|
@@ -232,7 +232,10 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
232
232
|
const [currentMs, setCurrentMs] = useState(startMs ?? 0);
|
|
233
233
|
const [loomStartMs, setLoomStartMs] = useState<number | null>(null);
|
|
234
234
|
const [volume, setVolume] = useState(1);
|
|
235
|
-
|
|
235
|
+
// Autoplaying players (e.g. the Slack unfurl embed, `?autoplay=1`) must
|
|
236
|
+
// start muted or the browser blocks autoplay with a NotAllowedError. The
|
|
237
|
+
// share page (no autoplay) keeps full sound.
|
|
238
|
+
const [muted, setMuted] = useState(() => !!autoPlay);
|
|
236
239
|
const [speed, setSpeed] = useState(() =>
|
|
237
240
|
readPlaybackSpeedPreference(defaultSpeed),
|
|
238
241
|
);
|
|
@@ -354,18 +357,30 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
354
357
|
setIsPreparing(false);
|
|
355
358
|
}, []);
|
|
356
359
|
|
|
357
|
-
const rejectPlayAttempt = useCallback(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
360
|
+
const rejectPlayAttempt = useCallback(
|
|
361
|
+
(attemptId: number, err: unknown) => {
|
|
362
|
+
if (attemptId !== playAttemptIdRef.current) return;
|
|
363
|
+
playAttemptPendingRef.current = false;
|
|
364
|
+
setIsPlayPending(false);
|
|
365
|
+
setIsBuffering(false);
|
|
366
|
+
|
|
367
|
+
const name = err instanceof DOMException ? err.name : "";
|
|
368
|
+
// AbortError: a newer load/seek superseded this play() — not a failure.
|
|
369
|
+
// NotAllowedError: the browser blocked autoplay because there was no
|
|
370
|
+
// user gesture (this is what happens inside Slack's cross-origin unfurl
|
|
371
|
+
// iframe). Both are expected — fall back to the click-to-play overlay
|
|
372
|
+
// instead of showing a scary "Could not start playback" message.
|
|
373
|
+
if (name === "AbortError" || name === "NotAllowedError") return;
|
|
374
|
+
|
|
375
|
+
console.warn("[clips] playback start failed", err);
|
|
376
|
+
reportPlaybackIssue("play-start-failed", err, videoRef.current, {
|
|
377
|
+
recordingId,
|
|
378
|
+
autoPlay: !!autoPlay,
|
|
379
|
+
});
|
|
380
|
+
setPlayError("Could not start playback. Try again.");
|
|
381
|
+
},
|
|
382
|
+
[autoPlay, recordingId],
|
|
383
|
+
);
|
|
369
384
|
|
|
370
385
|
const attachPlayPromise = useCallback(
|
|
371
386
|
(playPromise: Promise<void> | undefined, attemptId: number) => {
|
|
@@ -882,7 +897,10 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
882
897
|
<div
|
|
883
898
|
ref={containerRef}
|
|
884
899
|
className={cn(
|
|
885
|
-
|
|
900
|
+
// `@container` lets the center play button scale with the player
|
|
901
|
+
// width (see CenterPlaybackOverlay) so it isn't oversized inside
|
|
902
|
+
// small embeds like the Slack unfurl iframe.
|
|
903
|
+
"relative @container bg-black overflow-hidden select-none group",
|
|
886
904
|
theaterMode ? "fixed inset-0 z-40" : "rounded-xl",
|
|
887
905
|
className,
|
|
888
906
|
)}
|
|
@@ -910,12 +928,19 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
910
928
|
ref={videoRef}
|
|
911
929
|
src={activeVideoSrc}
|
|
912
930
|
poster={resolveLocalUrl(thumbnailUrl)}
|
|
913
|
-
crossOrigin
|
|
931
|
+
// `crossOrigin` is only needed so the owner's canvas thumbnail
|
|
932
|
+
// capture isn't tainted. For everyone else (viewers, and the Slack
|
|
933
|
+
// unfurl embed) it adds nothing — but if the player is ever framed
|
|
934
|
+
// into a sandboxed/opaque-origin context (Slack double-iframes the
|
|
935
|
+
// embed), the resulting CORS check on the media bytes fails and the
|
|
936
|
+
// video won't load. Scope it to owners so embeds load cleanly.
|
|
937
|
+
crossOrigin={role === "owner" ? "anonymous" : undefined}
|
|
914
938
|
className={cn(
|
|
915
939
|
"w-full h-full",
|
|
916
940
|
cover ? "object-cover" : "object-contain",
|
|
917
941
|
)}
|
|
918
942
|
autoPlay={autoPlay}
|
|
943
|
+
muted={muted}
|
|
919
944
|
playsInline
|
|
920
945
|
onLoadStart={() => {
|
|
921
946
|
setCanPlay(false);
|
|
@@ -1014,12 +1039,26 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
1014
1039
|
setIsBuffering(false);
|
|
1015
1040
|
onEnded?.();
|
|
1016
1041
|
}}
|
|
1017
|
-
onError={() => {
|
|
1042
|
+
onError={(e) => {
|
|
1018
1043
|
playAttemptPendingRef.current = false;
|
|
1019
1044
|
setIsPlayPending(false);
|
|
1020
1045
|
setIsBuffering(false);
|
|
1021
1046
|
setIsPreparing(false);
|
|
1022
|
-
|
|
1047
|
+
const desc = describeMediaError(e.currentTarget.error);
|
|
1048
|
+
reportPlaybackIssue(
|
|
1049
|
+
"media-load-failed",
|
|
1050
|
+
e.currentTarget.error,
|
|
1051
|
+
e.currentTarget,
|
|
1052
|
+
{
|
|
1053
|
+
recordingId,
|
|
1054
|
+
videoSrc: activeVideoSrc,
|
|
1055
|
+
},
|
|
1056
|
+
);
|
|
1057
|
+
setPlayError(
|
|
1058
|
+
desc
|
|
1059
|
+
? `Video could not be loaded (${desc.label}).`
|
|
1060
|
+
: "Video could not be loaded.",
|
|
1061
|
+
);
|
|
1023
1062
|
}}
|
|
1024
1063
|
onVolumeChange={(e) => {
|
|
1025
1064
|
setVolume(e.currentTarget.volume);
|
|
@@ -1039,7 +1078,17 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
1039
1078
|
durationMs={resolvedDurationMs}
|
|
1040
1079
|
speed={speed}
|
|
1041
1080
|
playError={playError}
|
|
1042
|
-
onPlay={
|
|
1081
|
+
onPlay={() => {
|
|
1082
|
+
// An explicit click means the user wants to watch with sound, so
|
|
1083
|
+
// undo the muted-autoplay default (see `muted` state) before we
|
|
1084
|
+
// start playback.
|
|
1085
|
+
const v = videoRef.current;
|
|
1086
|
+
if (v && v.muted) {
|
|
1087
|
+
v.muted = false;
|
|
1088
|
+
setMuted(false);
|
|
1089
|
+
}
|
|
1090
|
+
requestPlay();
|
|
1091
|
+
}}
|
|
1043
1092
|
onSpeedChange={applySpeed}
|
|
1044
1093
|
menuPortalContainer={fullscreenMenuContainer}
|
|
1045
1094
|
/>
|
|
@@ -1193,9 +1242,9 @@ function CenterPlaybackOverlay({
|
|
|
1193
1242
|
e.stopPropagation();
|
|
1194
1243
|
onPlay();
|
|
1195
1244
|
}}
|
|
1196
|
-
className="pointer-events-auto flex h-
|
|
1245
|
+
className="pointer-events-auto flex h-[clamp(3rem,13cqw,6rem)] w-[clamp(3rem,13cqw,6rem)] items-center justify-center rounded-full bg-white text-black shadow-2xl ring-1 ring-white/35 transition-transform duration-150 hover:scale-105 hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-black"
|
|
1197
1246
|
>
|
|
1198
|
-
<IconPlayerPlay className="ml-
|
|
1247
|
+
<IconPlayerPlay className="ml-[6%] h-[clamp(1.5rem,6.5cqw,3rem)] w-[clamp(1.5rem,6.5cqw,3rem)] fill-current" />
|
|
1199
1248
|
</button>
|
|
1200
1249
|
|
|
1201
1250
|
<div
|
|
@@ -1302,6 +1351,87 @@ function formatSpeedLabel(rate: number): string {
|
|
|
1302
1351
|
return `${Number.isInteger(rate) ? rate : rate.toFixed(1)}x`;
|
|
1303
1352
|
}
|
|
1304
1353
|
|
|
1354
|
+
/** Human-readable label for an HTMLMediaElement `error` (MediaError). */
|
|
1355
|
+
function describeMediaError(
|
|
1356
|
+
err: MediaError | null,
|
|
1357
|
+
): { code: number; label: string } | null {
|
|
1358
|
+
if (!err) return null;
|
|
1359
|
+
const labels: Record<number, string> = {
|
|
1360
|
+
1: "load aborted",
|
|
1361
|
+
2: "network error",
|
|
1362
|
+
3: "decode error",
|
|
1363
|
+
4: "format not supported",
|
|
1364
|
+
};
|
|
1365
|
+
return { code: err.code, label: labels[err.code] ?? "unknown error" };
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Surface a playback failure to the console and Sentry with enough context to
|
|
1370
|
+
* debug it remotely — e.g. when a clip "could not be loaded" inside a Slack
|
|
1371
|
+
* unfurl where there's no visible console. Best-effort; never throws. Expected,
|
|
1372
|
+
* benign cases (AbortError / autoplay-blocked NotAllowedError) are filtered out
|
|
1373
|
+
* by the callers and never reach here.
|
|
1374
|
+
*/
|
|
1375
|
+
function reportPlaybackIssue(
|
|
1376
|
+
reason: string,
|
|
1377
|
+
err: unknown,
|
|
1378
|
+
video: HTMLVideoElement | null,
|
|
1379
|
+
extra: Record<string, unknown>,
|
|
1380
|
+
) {
|
|
1381
|
+
const mediaError =
|
|
1382
|
+
err && typeof err === "object" && "code" in err
|
|
1383
|
+
? (err as MediaError)
|
|
1384
|
+
: (video?.error ?? null);
|
|
1385
|
+
const name =
|
|
1386
|
+
err instanceof DOMException || err instanceof Error ? err.name : undefined;
|
|
1387
|
+
const message = err instanceof Error ? err.message : undefined;
|
|
1388
|
+
|
|
1389
|
+
let videoHost: string | undefined;
|
|
1390
|
+
try {
|
|
1391
|
+
if (video?.currentSrc) videoHost = new URL(video.currentSrc).host;
|
|
1392
|
+
} catch {
|
|
1393
|
+
// ignore unparseable src
|
|
1394
|
+
}
|
|
1395
|
+
let inIframe = false;
|
|
1396
|
+
try {
|
|
1397
|
+
inIframe = typeof window !== "undefined" && window.self !== window.top;
|
|
1398
|
+
} catch {
|
|
1399
|
+
inIframe = true;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const detail = {
|
|
1403
|
+
...extra,
|
|
1404
|
+
errorName: name,
|
|
1405
|
+
errorMessage: message,
|
|
1406
|
+
mediaErrorCode: mediaError?.code,
|
|
1407
|
+
mediaErrorLabel: describeMediaError(mediaError)?.label,
|
|
1408
|
+
videoHost,
|
|
1409
|
+
inIframe,
|
|
1410
|
+
readyState: video?.readyState,
|
|
1411
|
+
networkState: video?.networkState,
|
|
1412
|
+
};
|
|
1413
|
+
console.warn(`[clips] playback issue: ${reason}`, detail);
|
|
1414
|
+
|
|
1415
|
+
try {
|
|
1416
|
+
const reportable =
|
|
1417
|
+
err instanceof Error
|
|
1418
|
+
? err
|
|
1419
|
+
: new Error(
|
|
1420
|
+
`clips playback ${reason}: ${message ?? name ?? describeMediaError(mediaError)?.label ?? "unknown"}`,
|
|
1421
|
+
);
|
|
1422
|
+
captureClientException(reportable, {
|
|
1423
|
+
tags: {
|
|
1424
|
+
area: "clips-player",
|
|
1425
|
+
playbackIssue: reason,
|
|
1426
|
+
inIframe: String(inIframe),
|
|
1427
|
+
},
|
|
1428
|
+
extra: detail,
|
|
1429
|
+
});
|
|
1430
|
+
} catch {
|
|
1431
|
+
// Diagnostics must never break playback UI.
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1305
1435
|
function formatWatchDuration(ms: number): string {
|
|
1306
1436
|
if (!Number.isFinite(ms) || ms <= 0) return "0 sec";
|
|
1307
1437
|
const totalSeconds = Math.max(1, Math.round(ms / 1000));
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import { Button } from "@/components/ui/button";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { IconPlayerSkipForward, IconX } from "@tabler/icons-react";
|
|
4
3
|
|
|
5
4
|
export interface CountdownOverlayProps {
|
|
6
5
|
/** Total seconds to count down from. Default 3. */
|
|
7
6
|
seconds?: number;
|
|
8
|
-
/** Called when the countdown reaches 1. */
|
|
7
|
+
/** Called when the countdown reaches 1 (drives the recording-start cue). */
|
|
9
8
|
onOneSecond?: () => void;
|
|
10
|
-
/** Called when the countdown reaches 0. */
|
|
9
|
+
/** Called when the countdown reaches 0 (or the user skips). */
|
|
11
10
|
onComplete: () => void;
|
|
12
11
|
/** Called when the user cancels before recording begins. */
|
|
13
12
|
onCancel: () => void;
|
|
@@ -20,77 +19,102 @@ export function CountdownOverlay({
|
|
|
20
19
|
onCancel,
|
|
21
20
|
}: CountdownOverlayProps) {
|
|
22
21
|
const [remaining, setRemaining] = useState(seconds);
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
// even if its identity changes while `remaining` is already 0 (which would
|
|
26
|
-
// otherwise re-run this effect and call it again).
|
|
22
|
+
// Ensure each callback fires exactly once for the lifetime of the countdown,
|
|
23
|
+
// even if identities change or the user skips while the timer is mid-flight.
|
|
27
24
|
const hasCompletedRef = useRef(false);
|
|
28
25
|
const hasPlayedOneSecondCueRef = useRef(false);
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
if (
|
|
27
|
+
const playOneSecondCue = useCallback(() => {
|
|
28
|
+
if (hasPlayedOneSecondCueRef.current) return;
|
|
32
29
|
hasPlayedOneSecondCueRef.current = true;
|
|
33
30
|
onOneSecond?.();
|
|
34
|
-
}, [
|
|
31
|
+
}, [onOneSecond]);
|
|
32
|
+
|
|
33
|
+
const complete = useCallback(() => {
|
|
34
|
+
if (hasCompletedRef.current) return;
|
|
35
|
+
hasCompletedRef.current = true;
|
|
36
|
+
onComplete();
|
|
37
|
+
}, [onComplete]);
|
|
38
|
+
|
|
39
|
+
// Skip the rest of the countdown and start recording immediately. Still play
|
|
40
|
+
// the start cue so the user gets the same audible confirmation they'd get if
|
|
41
|
+
// the countdown had run to zero.
|
|
42
|
+
const handleSkip = useCallback(() => {
|
|
43
|
+
playOneSecondCue();
|
|
44
|
+
complete();
|
|
45
|
+
}, [playOneSecondCue, complete]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (remaining === 1) playOneSecondCue();
|
|
49
|
+
}, [remaining, playOneSecondCue]);
|
|
35
50
|
|
|
36
51
|
useEffect(() => {
|
|
37
52
|
if (remaining <= 0) {
|
|
38
|
-
|
|
39
|
-
hasCompletedRef.current = true;
|
|
40
|
-
onComplete();
|
|
41
|
-
}
|
|
53
|
+
complete();
|
|
42
54
|
return;
|
|
43
55
|
}
|
|
44
|
-
if (paused) return;
|
|
45
56
|
const id = window.setTimeout(() => setRemaining((v) => v - 1), 1000);
|
|
46
57
|
return () => window.clearTimeout(id);
|
|
47
|
-
}, [remaining,
|
|
58
|
+
}, [remaining, complete]);
|
|
59
|
+
|
|
60
|
+
// Keyboard parity with the desktop overlay: Enter starts now, Esc cancels.
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
63
|
+
if (event.key === "Enter") {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
handleSkip();
|
|
66
|
+
} else if (event.key === "Escape") {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
onCancel();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
window.addEventListener("keydown", onKeyDown);
|
|
72
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
73
|
+
}, [handleSkip, onCancel]);
|
|
74
|
+
|
|
75
|
+
const controlClasses =
|
|
76
|
+
"flex h-16 w-16 items-center justify-center rounded-full border border-white/25 bg-white/5 text-white/90 shadow-lg backdrop-blur transition-colors hover:border-white/60 hover:bg-white/15 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70";
|
|
48
77
|
|
|
49
78
|
return (
|
|
50
79
|
<div
|
|
51
|
-
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm"
|
|
80
|
+
className="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-black/70 backdrop-blur-sm"
|
|
52
81
|
aria-live="polite"
|
|
53
82
|
aria-label={`Recording starts in ${remaining}`}
|
|
54
83
|
>
|
|
55
|
-
<div className="flex
|
|
84
|
+
<div className="flex items-center gap-10 sm:gap-14">
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={onCancel}
|
|
88
|
+
aria-label="Cancel recording"
|
|
89
|
+
className={controlClasses}
|
|
90
|
+
>
|
|
91
|
+
<IconX className="h-7 w-7" stroke={1.75} />
|
|
92
|
+
</button>
|
|
93
|
+
|
|
56
94
|
<div
|
|
57
95
|
key={remaining}
|
|
58
|
-
className="flex h-
|
|
96
|
+
className="flex h-44 w-44 items-center justify-center rounded-full text-[112px] font-bold leading-none text-white shadow-2xl duration-200 animate-in zoom-in-75 fade-in"
|
|
59
97
|
style={{
|
|
60
98
|
background:
|
|
61
|
-
"radial-gradient(circle at 30% 30%, rgba(255,255,255,0.
|
|
99
|
+
"radial-gradient(circle at 30% 30%, rgba(255,255,255,0.18), transparent 60%), hsl(var(--primary))",
|
|
62
100
|
}}
|
|
63
101
|
>
|
|
64
102
|
{remaining > 0 ? remaining : "Go"}
|
|
65
103
|
</div>
|
|
66
104
|
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{paused ? (
|
|
76
|
-
<IconPlayerPlay className="h-4 w-4" />
|
|
77
|
-
) : (
|
|
78
|
-
<IconPlayerPause className="h-4 w-4" />
|
|
79
|
-
)}
|
|
80
|
-
{paused ? "Resume" : "Pause"}
|
|
81
|
-
</Button>
|
|
82
|
-
<Button
|
|
83
|
-
type="button"
|
|
84
|
-
variant="outline"
|
|
85
|
-
size="sm"
|
|
86
|
-
className="rounded-full border-white/20 bg-white/10 text-white hover:bg-white/20 hover:text-white"
|
|
87
|
-
onClick={onCancel}
|
|
88
|
-
>
|
|
89
|
-
<IconX className="h-4 w-4" />
|
|
90
|
-
Cancel
|
|
91
|
-
</Button>
|
|
92
|
-
</div>
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onClick={handleSkip}
|
|
108
|
+
aria-label="Skip countdown and start recording now"
|
|
109
|
+
className={controlClasses}
|
|
110
|
+
>
|
|
111
|
+
<IconPlayerSkipForward className="h-7 w-7" stroke={1.75} />
|
|
112
|
+
</button>
|
|
93
113
|
</div>
|
|
114
|
+
|
|
115
|
+
<p className="absolute bottom-16 left-1/2 -translate-x-1/2 rounded-full bg-black/50 px-3 py-1 text-xs font-medium text-white/70 backdrop-blur">
|
|
116
|
+
Esc to cancel · Enter to start now
|
|
117
|
+
</p>
|
|
94
118
|
</div>
|
|
95
119
|
);
|
|
96
120
|
}
|
|
@@ -308,8 +308,9 @@ export function PreRecordPanel({
|
|
|
308
308
|
}, [cameraId, cameras]);
|
|
309
309
|
|
|
310
310
|
// Camera-only mode needs a camera — coerce a restored "off" sentinel to
|
|
311
|
-
// "default" so Start doesn't forward it as an exact deviceId
|
|
312
|
-
//
|
|
311
|
+
// "default" so Start doesn't forward it as an exact deviceId. This is the
|
|
312
|
+
// single owner of that coercion: it covers both the mode-button click and the
|
|
313
|
+
// ?mode=camera deep-link/restore path.
|
|
313
314
|
useEffect(() => {
|
|
314
315
|
if (mode === "camera" && cameraId === NO_CAMERA_DEVICE_ID) {
|
|
315
316
|
setCameraId("default");
|
|
@@ -583,15 +584,10 @@ export function PreRecordPanel({
|
|
|
583
584
|
<button
|
|
584
585
|
key={opt.value}
|
|
585
586
|
type="button"
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
cameraId === NO_CAMERA_DEVICE_ID
|
|
591
|
-
) {
|
|
592
|
-
chooseCamera("default");
|
|
593
|
-
}
|
|
594
|
-
}}
|
|
587
|
+
// Camera-only mode needs a camera; the [mode, cameraId] effect
|
|
588
|
+
// below coerces a restored "off" sentinel to "default" for both
|
|
589
|
+
// this click and the ?mode=camera deep-link path.
|
|
590
|
+
onClick={() => chooseMode(opt.value)}
|
|
595
591
|
className={cn(
|
|
596
592
|
"flex min-h-20 min-w-0 flex-col justify-between rounded-xl border p-3 text-left transition-colors",
|
|
597
593
|
active
|
|
@@ -420,6 +420,13 @@ export class RecorderEngine {
|
|
|
420
420
|
*/
|
|
421
421
|
private cameraDisconnectNotified = false;
|
|
422
422
|
private micDisconnectNotified = false;
|
|
423
|
+
/**
|
|
424
|
+
* Set when a stale/unavailable `micDeviceId` forces a fallback to the system
|
|
425
|
+
* default mic during `acquire()`. The recording then runs on the default
|
|
426
|
+
* device, so live transcription (which can only ever use the default mic) is
|
|
427
|
+
* safe to start even though a specific mic was originally requested.
|
|
428
|
+
*/
|
|
429
|
+
private micFellBackToDefault = false;
|
|
423
430
|
|
|
424
431
|
private state: RecorderState = "idle";
|
|
425
432
|
|
|
@@ -448,10 +455,30 @@ export class RecorderEngine {
|
|
|
448
455
|
return this.cameraStream;
|
|
449
456
|
}
|
|
450
457
|
|
|
458
|
+
/**
|
|
459
|
+
* True when `acquire()` had to fall back from the requested `micDeviceId` to
|
|
460
|
+
* the system default mic. Lets the UI start live transcription for this
|
|
461
|
+
* session even though a specific mic was originally selected.
|
|
462
|
+
*/
|
|
463
|
+
didMicFallBackToDefault(): boolean {
|
|
464
|
+
return this.micFellBackToDefault;
|
|
465
|
+
}
|
|
466
|
+
|
|
451
467
|
getPreviewStream(): MediaStream | null {
|
|
452
468
|
return this.previewStream;
|
|
453
469
|
}
|
|
454
470
|
|
|
471
|
+
/**
|
|
472
|
+
* The composited screen+camera canvas stream that actually gets recorded
|
|
473
|
+
* (screen with the camera bubble drawn in), or `null` for screen-only /
|
|
474
|
+
* camera-only modes where the visible preview already matches the recording.
|
|
475
|
+
* Used to grab a thumbnail that includes the presenter's camera — the raw
|
|
476
|
+
* preview stream in screen+camera mode is screen-only and has no face.
|
|
477
|
+
*/
|
|
478
|
+
getCompositeStream(): MediaStream | null {
|
|
479
|
+
return this.cameraComposite?.stream ?? null;
|
|
480
|
+
}
|
|
481
|
+
|
|
455
482
|
getElapsedMs(): number {
|
|
456
483
|
if (this.startedAtMs === null) return 0;
|
|
457
484
|
const now = performance.now();
|
|
@@ -515,6 +542,7 @@ export class RecorderEngine {
|
|
|
515
542
|
const wantsCamera =
|
|
516
543
|
this.opts.mode === "camera" || this.opts.mode === "screen+camera";
|
|
517
544
|
const wantsMic = this.opts.micDeviceId !== NO_MIC_DEVICE_ID;
|
|
545
|
+
this.micFellBackToDefault = false;
|
|
518
546
|
|
|
519
547
|
try {
|
|
520
548
|
if (!isBrowserSecureContext()) {
|
|
@@ -634,6 +662,9 @@ export class RecorderEngine {
|
|
|
634
662
|
audio: voiceFocusedAudioConstraints(),
|
|
635
663
|
video: false,
|
|
636
664
|
});
|
|
665
|
+
// Recording is now on the system default mic, so live
|
|
666
|
+
// transcription can run for this session after all.
|
|
667
|
+
this.micFellBackToDefault = true;
|
|
637
668
|
} catch (retryErr) {
|
|
638
669
|
throw this.friendlyError(retryErr, "microphone");
|
|
639
670
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { IconBrandSlack } from "@tabler/icons-react";
|
|
2
|
+
import { appPath, useActionQuery } from "@agent-native/core/client";
|
|
3
|
+
|
|
4
|
+
interface SlackInstallation {
|
|
5
|
+
status: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface SlackInstallationsResponse {
|
|
9
|
+
oauthConfigured: boolean;
|
|
10
|
+
installations: SlackInstallation[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Contextual nudge shown in the share popover's Link tab for public clips.
|
|
15
|
+
*
|
|
16
|
+
* A public Clips link unfurls into a playable video when pasted in a connected
|
|
17
|
+
* Slack workspace, but that integration is otherwise only discoverable in
|
|
18
|
+
* Settings. When a workspace is connected we confirm the payoff; when none is,
|
|
19
|
+
* clip owners/admins get a one-click link to connect one in Settings.
|
|
20
|
+
*
|
|
21
|
+
* Only rendered for public clips (the caller gates on `isPublic`), so the
|
|
22
|
+
* `list-slack-installations` query never runs for private shares.
|
|
23
|
+
*/
|
|
24
|
+
export function SlackShareHint({ canManage }: { canManage: boolean }) {
|
|
25
|
+
const slack = useActionQuery<SlackInstallationsResponse>(
|
|
26
|
+
"list-slack-installations",
|
|
27
|
+
undefined,
|
|
28
|
+
{ retry: false },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const data = slack.data;
|
|
32
|
+
const connected =
|
|
33
|
+
data?.installations?.some((i) => i.status === "connected") ?? false;
|
|
34
|
+
|
|
35
|
+
if (connected) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex items-center gap-2.5 rounded-md border border-border bg-muted/40 px-3 py-2">
|
|
38
|
+
<IconBrandSlack className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
39
|
+
<div className="min-w-0">
|
|
40
|
+
<div className="text-xs font-medium">Plays inline in Slack</div>
|
|
41
|
+
<p className="text-[11px] text-muted-foreground">
|
|
42
|
+
Paste this link in any connected workspace to play it inline.
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// No workspace connected yet. Connecting needs the deployment's Slack OAuth
|
|
50
|
+
// credentials, so only point managers at Settings when it's actually set up —
|
|
51
|
+
// otherwise the Connect button there is disabled and the link is a dead end.
|
|
52
|
+
// While the query is loading or errored, `data` is undefined and we render
|
|
53
|
+
// nothing rather than guess at the workspace state.
|
|
54
|
+
if (!data || !canManage || !data.oauthConfigured) return null;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<a
|
|
58
|
+
href={appPath("/settings#slack")}
|
|
59
|
+
className="flex items-center gap-2.5 rounded-md border border-border px-3 py-2 transition-colors hover:bg-muted/50"
|
|
60
|
+
>
|
|
61
|
+
<IconBrandSlack className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
62
|
+
<div className="min-w-0 flex-1">
|
|
63
|
+
<div className="text-xs font-medium">Make it play inline in Slack</div>
|
|
64
|
+
<p className="text-[11px] text-muted-foreground">
|
|
65
|
+
Connect a workspace so this link unfurls as a video.
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
<span className="shrink-0 text-xs font-medium text-primary">Connect</span>
|
|
69
|
+
</a>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -8,35 +8,53 @@ const noopCountdownAudioCue: CountdownAudioCue = {
|
|
|
8
8
|
cleanup() {},
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* A soft, modern "ready" chime: a rising two-note (D5 → A5) with a faint high
|
|
13
|
+
* shimmer. Each voice has a fast attack and a smooth exponential tail so it
|
|
14
|
+
* reads as a gentle confirmation rather than a harsh beep. Kept under ~380ms so
|
|
15
|
+
* it lands cleanly just before capture.
|
|
16
|
+
*/
|
|
11
17
|
function scheduleCountdownTone(ctx: AudioContext): Promise<void> {
|
|
12
18
|
return new Promise<void>((resolve) => {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
19
|
+
const t0 = ctx.currentTime + 0.005;
|
|
20
|
+
const voices = [
|
|
21
|
+
{ freq: 587.33, at: 0.0, dur: 0.22, peak: 0.06 }, // D5
|
|
22
|
+
{ freq: 880.0, at: 0.06, dur: 0.26, peak: 0.075 }, // A5
|
|
23
|
+
{ freq: 1760.0, at: 0.065, dur: 0.14, peak: 0.02 }, // A6 shimmer
|
|
24
|
+
];
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
let lastStop = t0;
|
|
27
|
+
for (const voice of voices) {
|
|
28
|
+
const startAt = t0 + voice.at;
|
|
29
|
+
const stopAt = startAt + voice.dur;
|
|
30
|
+
lastStop = Math.max(lastStop, stopAt);
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
const oscillator = ctx.createOscillator();
|
|
33
|
+
oscillator.type = "sine";
|
|
34
|
+
oscillator.frequency.setValueAtTime(voice.freq, startAt);
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
const gain = ctx.createGain();
|
|
37
|
+
gain.gain.setValueAtTime(0.0001, startAt);
|
|
38
|
+
gain.gain.exponentialRampToValueAtTime(voice.peak, startAt + 0.012);
|
|
39
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, stopAt);
|
|
40
|
+
|
|
41
|
+
oscillator.connect(gain);
|
|
42
|
+
gain.connect(ctx.destination);
|
|
43
|
+
|
|
44
|
+
oscillator.start(startAt);
|
|
45
|
+
oscillator.stop(stopAt + 0.02);
|
|
46
|
+
}
|
|
27
47
|
|
|
28
48
|
let resolved = false;
|
|
29
49
|
const finish = () => {
|
|
30
50
|
if (resolved) return;
|
|
31
51
|
resolved = true;
|
|
32
|
-
window.clearTimeout(timer);
|
|
33
52
|
resolve();
|
|
34
53
|
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
oscillator.stop(startedAt + 0.2);
|
|
54
|
+
window.setTimeout(
|
|
55
|
+
finish,
|
|
56
|
+
Math.ceil((lastStop - ctx.currentTime) * 1000) + 60,
|
|
57
|
+
);
|
|
40
58
|
});
|
|
41
59
|
}
|
|
42
60
|
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "@agent-native/core/client";
|
|
22
22
|
import { IconCheck, IconSun, IconMoon } from "@tabler/icons-react";
|
|
23
23
|
import { useTheme } from "next-themes";
|
|
24
|
+
import changelog from "../CHANGELOG.md?raw";
|
|
24
25
|
import { Toaster } from "@/components/ui/sonner";
|
|
25
26
|
import { Button } from "@/components/ui/button";
|
|
26
27
|
import {
|
|
@@ -258,7 +259,12 @@ function AppContent() {
|
|
|
258
259
|
{standalonePublic ? null : <DbSyncSetup />}
|
|
259
260
|
{standalonePublic ? null : <ClipsExtensionAuthBridge />}
|
|
260
261
|
{standalonePublic ? null : (
|
|
261
|
-
<CommandMenu
|
|
262
|
+
<CommandMenu
|
|
263
|
+
open={cmdkOpen}
|
|
264
|
+
onOpenChange={setCmdkOpen}
|
|
265
|
+
changelog={changelog}
|
|
266
|
+
changelogKey="clips"
|
|
267
|
+
>
|
|
262
268
|
<CommandMenu.Group heading="Actions">
|
|
263
269
|
<CommandMenu.Item onSelect={() => {}}>Search</CommandMenu.Item>
|
|
264
270
|
</CommandMenu.Group>
|