@agent-native/core 0.87.0 → 0.90.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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +88 -2
- package/corpus/core/docs/content/agent-native-toolkit.mdx +103 -0
- package/corpus/core/docs/content/cloneable-saas.mdx +19 -19
- package/corpus/core/docs/content/components.mdx +16 -4
- package/corpus/core/docs/content/data-programs.mdx +329 -0
- package/corpus/core/docs/content/deployment.mdx +9 -9
- package/corpus/core/docs/content/extensions.mdx +8 -7
- package/corpus/core/docs/content/generative-ui.mdx +5 -3
- package/corpus/core/docs/content/key-concepts.mdx +6 -9
- package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +103 -0
- package/corpus/core/docs/content/locales/ar-SA/cloneable-saas.mdx +19 -19
- package/corpus/core/docs/content/locales/ar-SA/components.mdx +12 -2
- package/corpus/core/docs/content/locales/ar-SA/data-programs.mdx +329 -0
- package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +8 -9
- package/corpus/core/docs/content/locales/ar-SA/extensions.mdx +6 -4
- package/corpus/core/docs/content/locales/ar-SA/key-concepts.mdx +7 -9
- package/corpus/core/docs/content/locales/ar-SA/real-time-collaboration.mdx +9 -10
- package/corpus/core/docs/content/locales/ar-SA/sharing.mdx +15 -7
- package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +103 -0
- package/corpus/core/docs/content/locales/de-DE/cloneable-saas.mdx +19 -19
- package/corpus/core/docs/content/locales/de-DE/components.mdx +12 -2
- package/corpus/core/docs/content/locales/de-DE/data-programs.mdx +329 -0
- package/corpus/core/docs/content/locales/de-DE/deployment.mdx +8 -9
- package/corpus/core/docs/content/locales/de-DE/extensions.mdx +6 -4
- package/corpus/core/docs/content/locales/de-DE/key-concepts.mdx +8 -10
- package/corpus/core/docs/content/locales/de-DE/real-time-collaboration.mdx +9 -10
- package/corpus/core/docs/content/locales/de-DE/sharing.mdx +15 -7
- package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +103 -0
- package/corpus/core/docs/content/locales/es-ES/cloneable-saas.mdx +19 -19
- package/corpus/core/docs/content/locales/es-ES/components.mdx +12 -2
- package/corpus/core/docs/content/locales/es-ES/data-programs.mdx +329 -0
- package/corpus/core/docs/content/locales/es-ES/deployment.mdx +8 -9
- package/corpus/core/docs/content/locales/es-ES/extensions.mdx +6 -4
- package/corpus/core/docs/content/locales/es-ES/key-concepts.mdx +8 -10
- package/corpus/core/docs/content/locales/es-ES/real-time-collaboration.mdx +9 -10
- package/corpus/core/docs/content/locales/es-ES/sharing.mdx +15 -7
- package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +103 -0
- package/corpus/core/docs/content/locales/fr-FR/cloneable-saas.mdx +19 -19
- package/corpus/core/docs/content/locales/fr-FR/components.mdx +12 -2
- package/corpus/core/docs/content/locales/fr-FR/data-programs.mdx +329 -0
- package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +8 -9
- package/corpus/core/docs/content/locales/fr-FR/extensions.mdx +6 -4
- package/corpus/core/docs/content/locales/fr-FR/key-concepts.mdx +8 -10
- package/corpus/core/docs/content/locales/fr-FR/real-time-collaboration.mdx +9 -10
- package/corpus/core/docs/content/locales/fr-FR/sharing.mdx +15 -7
- package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +103 -0
- package/corpus/core/docs/content/locales/hi-IN/cloneable-saas.mdx +19 -19
- package/corpus/core/docs/content/locales/hi-IN/components.mdx +20 -10
- package/corpus/core/docs/content/locales/hi-IN/data-programs.mdx +329 -0
- package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +8 -9
- package/corpus/core/docs/content/locales/hi-IN/extensions.mdx +6 -4
- package/corpus/core/docs/content/locales/hi-IN/key-concepts.mdx +8 -10
- package/corpus/core/docs/content/locales/hi-IN/real-time-collaboration.mdx +9 -10
- package/corpus/core/docs/content/locales/hi-IN/sharing.mdx +15 -7
- package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +103 -0
- package/corpus/core/docs/content/locales/ja-JP/cloneable-saas.mdx +19 -19
- package/corpus/core/docs/content/locales/ja-JP/components.mdx +12 -2
- package/corpus/core/docs/content/locales/ja-JP/data-programs.mdx +329 -0
- package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +8 -9
- package/corpus/core/docs/content/locales/ja-JP/extensions.mdx +6 -4
- package/corpus/core/docs/content/locales/ja-JP/key-concepts.mdx +9 -11
- package/corpus/core/docs/content/locales/ja-JP/real-time-collaboration.mdx +9 -10
- package/corpus/core/docs/content/locales/ja-JP/sharing.mdx +15 -7
- package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +103 -0
- package/corpus/core/docs/content/locales/ko-KR/cloneable-saas.mdx +19 -19
- package/corpus/core/docs/content/locales/ko-KR/components.mdx +20 -10
- package/corpus/core/docs/content/locales/ko-KR/data-programs.mdx +329 -0
- package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +8 -9
- package/corpus/core/docs/content/locales/ko-KR/extensions.mdx +16 -14
- package/corpus/core/docs/content/locales/ko-KR/key-concepts.mdx +9 -11
- package/corpus/core/docs/content/locales/ko-KR/real-time-collaboration.mdx +9 -10
- package/corpus/core/docs/content/locales/ko-KR/sharing.mdx +15 -7
- package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +103 -0
- package/corpus/core/docs/content/locales/pt-BR/cloneable-saas.mdx +19 -19
- package/corpus/core/docs/content/locales/pt-BR/components.mdx +12 -2
- package/corpus/core/docs/content/locales/pt-BR/data-programs.mdx +329 -0
- package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +8 -9
- package/corpus/core/docs/content/locales/pt-BR/extensions.mdx +6 -4
- package/corpus/core/docs/content/locales/pt-BR/key-concepts.mdx +8 -11
- package/corpus/core/docs/content/locales/pt-BR/real-time-collaboration.mdx +9 -10
- package/corpus/core/docs/content/locales/pt-BR/sharing.mdx +15 -7
- package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +103 -0
- package/corpus/core/docs/content/locales/zh-CN/cloneable-saas.mdx +19 -19
- package/corpus/core/docs/content/locales/zh-CN/components.mdx +20 -10
- package/corpus/core/docs/content/locales/zh-CN/data-programs.mdx +329 -0
- package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +7 -9
- package/corpus/core/docs/content/locales/zh-CN/extensions.mdx +16 -14
- package/corpus/core/docs/content/locales/zh-CN/key-concepts.mdx +7 -11
- package/corpus/core/docs/content/locales/zh-CN/real-time-collaboration.mdx +10 -11
- package/corpus/core/docs/content/locales/zh-CN/sharing.mdx +15 -7
- package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +103 -0
- package/corpus/core/docs/content/locales/zh-TW/cloneable-saas.mdx +19 -19
- package/corpus/core/docs/content/locales/zh-TW/components.mdx +20 -10
- package/corpus/core/docs/content/locales/zh-TW/data-programs.mdx +329 -0
- package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +7 -9
- package/corpus/core/docs/content/locales/zh-TW/extensions.mdx +16 -14
- package/corpus/core/docs/content/locales/zh-TW/key-concepts.mdx +7 -11
- package/corpus/core/docs/content/locales/zh-TW/real-time-collaboration.mdx +10 -11
- package/corpus/core/docs/content/locales/zh-TW/sharing.mdx +15 -7
- package/corpus/core/docs/content/real-time-collaboration.mdx +10 -11
- package/corpus/core/docs/content/sharing.mdx +39 -7
- package/corpus/core/docs/content/toolkit-app-adapters.mdx +46 -0
- package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +48 -0
- package/corpus/core/docs/content/toolkit-sharing-ui.mdx +33 -0
- package/corpus/core/docs/content/toolkit-shell-hooks.mdx +27 -0
- package/corpus/core/docs/content/toolkit-ui.mdx +69 -0
- package/corpus/core/package.json +6 -1
- package/corpus/core/src/agent/durable-background.ts +53 -0
- package/corpus/core/src/agent/engine/ai-sdk-engine.ts +7 -1
- package/corpus/core/src/agent/engine/anthropic-engine.ts +29 -1
- package/corpus/core/src/agent/engine/builder-engine.ts +1 -0
- package/corpus/core/src/agent/engine/output-tokens.ts +26 -13
- package/corpus/core/src/agent/engine/registry.ts +45 -26
- package/corpus/core/src/agent/engine/translate-ai-sdk.ts +56 -7
- package/corpus/core/src/agent/engine/translate-anthropic.ts +43 -1
- package/corpus/core/src/agent/engine/types.ts +16 -0
- package/corpus/core/src/agent/model-config.ts +100 -0
- package/corpus/core/src/agent/production-agent.ts +492 -241
- package/corpus/core/src/agent/runtime-context.ts +50 -12
- package/corpus/core/src/agent/tool-result-images.ts +178 -0
- package/corpus/core/src/brand-kit/fig/index.ts +8 -4
- package/corpus/core/src/client/AgentPanel.tsx +88 -31
- package/corpus/core/src/client/AssistantChat.tsx +95 -46
- package/corpus/core/src/client/KeepTabOpenNotice.tsx +176 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +7 -0
- package/corpus/core/src/client/RunStuckBanner.tsx +13 -1
- package/corpus/core/src/client/chat/run-recovery.tsx +7 -1
- package/corpus/core/src/client/clipboard.ts +17 -13
- package/corpus/core/src/client/components/AgentPresenceChip.tsx +4 -69
- package/corpus/core/src/client/components/LiveCursorOverlay.tsx +5 -240
- package/corpus/core/src/client/components/PresenceBar.tsx +4 -346
- package/corpus/core/src/client/components/RecentEditHighlights.tsx +4 -219
- package/corpus/core/src/client/components/RemoteSelectionRings.tsx +5 -220
- package/corpus/core/src/client/composer/TiptapComposer.tsx +94 -38
- package/corpus/core/src/client/db-admin/SqlEditor.tsx +13 -3
- package/corpus/core/src/client/dynamic-suggestions.ts +6 -1
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -14
- package/corpus/core/src/client/extensions/ExtensionsListPage.tsx +29 -30
- package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +2 -1
- package/corpus/core/src/client/host-bridge.ts +29 -8
- package/corpus/core/src/client/i18n.tsx +7 -1
- package/corpus/core/src/client/index.ts +5 -0
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +34 -69
- package/corpus/core/src/client/rich-markdown-editor/SharedRichEditor.tsx +4 -0
- package/corpus/core/src/client/rich-markdown-editor/useCollabReconcile.ts +7 -1
- package/corpus/core/src/client/settings/SettingsPanel.tsx +409 -230
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +5 -7
- package/corpus/core/src/client/settings/index.ts +5 -1
- package/corpus/core/src/client/sharing/VisibilityBadge.tsx +4 -37
- package/corpus/core/src/client/use-run-stuck-detection.ts +10 -1
- package/corpus/core/src/coding-tools/run-code.ts +473 -74
- package/corpus/core/src/coding-tools/sandbox/background.ts +406 -0
- package/corpus/core/src/coding-tools/sandbox/executions-store.ts +517 -0
- package/corpus/core/src/coding-tools/sandbox/index.ts +53 -13
- package/corpus/core/src/coding-tools/sandbox/local-child-process-adapter.ts +61 -4
- package/corpus/core/src/collab/client.ts +870 -562
- package/corpus/core/src/collab/presence.ts +12 -23
- package/corpus/core/src/collab/recent-edits.ts +13 -32
- package/corpus/core/src/data-programs/actions.ts +335 -0
- package/corpus/core/src/data-programs/contract.ts +284 -0
- package/corpus/core/src/data-programs/execute.ts +693 -0
- package/corpus/core/src/data-programs/index.ts +72 -0
- package/corpus/core/src/data-programs/schema.ts +148 -0
- package/corpus/core/src/data-programs/store.ts +755 -0
- package/corpus/core/src/deploy/build.ts +34 -0
- package/corpus/core/src/extensions/actions.ts +2 -2
- package/corpus/core/src/file-upload/builder.ts +11 -0
- package/corpus/core/src/integrations/webhook-handler.ts +27 -8
- package/corpus/core/src/local-artifacts/index.ts +1 -1
- package/corpus/core/src/localization/default-messages.ts +1 -2
- package/corpus/core/src/mcp-client/index.ts +29 -0
- package/corpus/core/src/onboarding/default-steps.ts +5 -2
- package/corpus/core/src/resources/store.ts +34 -3
- package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +4 -8
- package/corpus/core/src/scripts/agent-engines/test-agent-engine.ts +9 -2
- package/corpus/core/src/scripts/db/tool-mode.ts +2 -2
- package/corpus/core/src/scripts/resources/migrate-learnings.ts +9 -13
- package/corpus/core/src/server/action-discovery.ts +4 -0
- package/corpus/core/src/server/action-routes.ts +8 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +154 -62
- package/corpus/core/src/server/agent-discovery.ts +67 -7
- package/corpus/core/src/server/auth.ts +90 -3
- package/corpus/core/src/server/builder-design-systems.ts +2 -2
- package/corpus/core/src/server/core-routes-plugin.ts +119 -22
- package/corpus/core/src/server/credential-provider.ts +28 -8
- package/corpus/core/src/server/csrf.ts +3 -0
- package/corpus/core/src/server/index.ts +9 -0
- package/corpus/core/src/server/oauth-helpers.ts +11 -1
- package/corpus/core/src/server/onboarding-html.ts +80 -48
- package/corpus/core/src/server/prompts/shared-rules.ts +2 -2
- package/corpus/core/src/server/transcribe-voice.ts +93 -9
- package/corpus/core/src/shared/agent-readable-resource.ts +111 -0
- package/corpus/core/src/shared/index.ts +9 -0
- package/corpus/core/src/sharing/actions/create-agent-resource-link.ts +91 -0
- package/corpus/core/src/sharing/registry.ts +19 -0
- package/corpus/core/src/styles/agent-native.css +57 -45
- package/corpus/core/src/templates/default/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/core/src/templates/default/AGENTS.md +11 -14
- package/corpus/core/src/templates/default/actions/navigate.ts +1 -1
- package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +0 -1
- package/corpus/core/src/templates/default/app/i18n/de-DE.ts +0 -1
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +0 -1
- package/corpus/core/src/templates/default/app/i18n/es-ES.ts +0 -1
- package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +0 -1
- package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +0 -1
- package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +0 -1
- package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +0 -1
- package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +0 -1
- package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +0 -1
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +0 -1
- package/corpus/core/src/templates/default/app/routes/settings.tsx +22 -33
- package/corpus/core/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +20 -7
- package/corpus/core/src/templates/headless/AGENTS.md +9 -6
- package/corpus/core/src/templates/workspace-core/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +92 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/data-programs/SKILL.md +128 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/harness-agents/SKILL.md +10 -4
- package/corpus/core/src/vite/agents-bundle-plugin.ts +26 -1
- package/corpus/core/src/vite/client.ts +13 -0
- package/corpus/templates/analytics/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +8 -0
- package/corpus/templates/analytics/.agents/skills/data-programs/SKILL.md +348 -0
- package/corpus/templates/analytics/.agents/skills/provider-api/SKILL.md +10 -0
- package/corpus/templates/analytics/AGENTS.md +31 -32
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1 -1
- package/corpus/templates/analytics/actions/delete-data-source-credentials.ts +32 -0
- package/corpus/templates/analytics/actions/ensure-risk-meeting-dashboard.ts +247 -0
- package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -113
- package/corpus/templates/analytics/actions/update-data-source-credentials.ts +123 -0
- package/corpus/templates/analytics/app/components/InlineMetric.tsx +2 -2
- package/corpus/templates/analytics/app/components/dashboard/CumulativeNetChart.tsx +2 -7
- package/corpus/templates/analytics/app/components/dashboard/DataTable.tsx +11 -16
- package/corpus/templates/analytics/app/components/dashboard/DateRangePicker.tsx +1 -2
- package/corpus/templates/analytics/app/components/dashboard/MetricCard.tsx +2 -7
- package/corpus/templates/analytics/app/components/dashboard/RecentActivity.tsx +3 -6
- package/corpus/templates/analytics/app/components/dashboard/RevenueChart.tsx +1 -6
- package/corpus/templates/analytics/app/components/dashboard/RevenueComparisonChart.tsx +2 -7
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +11 -15
- package/corpus/templates/analytics/app/components/dashboard/StatsCard.tsx +1 -7
- package/corpus/templates/analytics/app/components/dashboard/TimeSeriesChart.tsx +2 -7
- package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +16 -16
- package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -2
- package/corpus/templates/analytics/app/components/layout/HeaderActions.tsx +2 -1
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +2 -2
- package/corpus/templates/analytics/app/components/layout/MobileNav.tsx +33 -17
- package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +6 -6
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +4 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +119 -68
- package/corpus/templates/analytics/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/alert.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/avatar.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/button.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/calendar.tsx +1 -1
- package/corpus/templates/analytics/app/components/ui/card.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/collapsible.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/command.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/date-picker.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/input.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/label.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/progress.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/select.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/sonner.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/spinner.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/table.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/toaster.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/toggle-group.tsx +1 -0
- package/corpus/templates/analytics/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/analytics/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/analytics/app/global.css +26 -3
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +27 -4
- package/corpus/templates/analytics/app/i18n-data.ts +277 -42
- package/corpus/templates/analytics/app/pages/Agents.tsx +19 -19
- package/corpus/templates/analytics/app/pages/DataDictionary.tsx +21 -21
- package/corpus/templates/analytics/app/pages/DataSources.tsx +31 -54
- package/corpus/templates/analytics/app/pages/NotFound.tsx +2 -1
- package/corpus/templates/analytics/app/pages/Placeholder.tsx +2 -1
- package/corpus/templates/analytics/app/pages/Settings.tsx +19 -37
- package/corpus/templates/analytics/app/pages/adhoc/DashboardSkeleton.tsx +3 -2
- package/corpus/templates/analytics/app/pages/adhoc/_shared/KpiChart.tsx +8 -13
- package/corpus/templates/analytics/app/pages/adhoc/_shared/components/DateRangeInput.tsx +1 -2
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/ChartTypePicker.tsx +2 -4
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/DateRangePicker.tsx +1 -2
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/EventCombobox.tsx +7 -7
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/EventPanel.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/EventRow.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/ExplorerChart.tsx +3 -8
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/FilterBuilder.tsx +5 -4
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/GroupByPicker.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyCombobox.tsx +7 -7
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyValueCombobox.tsx +7 -7
- package/corpus/templates/analytics/app/pages/adhoc/explorer/components/SqlPreview.tsx +1 -1
- package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +16 -16
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +16 -21
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +32 -32
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +14 -14
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +14 -13
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +200 -52
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +23 -28
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewSqlPopover.tsx +13 -12
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewsMenu.tsx +15 -15
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +32 -36
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -1
- package/corpus/templates/analytics/app/pages/analyses/AnalysesList.tsx +8 -7
- package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +19 -19
- package/corpus/templates/analytics/app/pages/analyses/LegacyFusionAnalysis.tsx +11 -15
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +14 -14
- package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +7 -12
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +12 -12
- package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +25 -25
- package/corpus/templates/analytics/app/root.tsx +22 -19
- package/corpus/templates/analytics/app/routes/catalog.tsx +8 -8
- package/corpus/templates/analytics/changelog/2026-07-06-dashboard-lists-and-headers-stay-compact.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-06-dashboards-can-now-chart-data-programs-saved-server-side-scr.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-06-extension-pages-and-mobile-headers-now-share-one-background-shade.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-06-mobile-chat-header-is-more-compact-and-the-menu-button-opens-navigation.md +5 -0
- package/corpus/templates/analytics/learnings.defaults.md +36 -0
- package/corpus/templates/analytics/seeds/data-programs/risk-meeting-cohort.js +254 -0
- package/corpus/templates/analytics/seeds/data-programs/risk-meeting-pylon-early-warning.js +184 -0
- package/corpus/templates/analytics/server/db/index.ts +14 -0
- package/corpus/templates/analytics/server/handlers/sql-query.ts +1 -1
- package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +156 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +109 -1
- package/corpus/templates/analytics/server/routes/[...page].get.ts +92 -25
- package/corpus/templates/analytics/server/routes/api/analysis-agent-context.json.get.ts +94 -0
- package/corpus/templates/analytics/server/routes/api/dashboard-agent-context.json.get.ts +109 -0
- package/corpus/templates/analytics/shared/resource-agent-access.ts +6 -0
- package/corpus/templates/assets/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/assets/app/components/create/RecentDraftsSection.tsx +3 -2
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +16 -10
- package/corpus/templates/assets/app/components/layout/Header.tsx +1 -1
- package/corpus/templates/assets/app/components/layout/PageShell.tsx +4 -4
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +12 -12
- package/corpus/templates/assets/app/components/library/CreateLibraryDialog.tsx +8 -8
- package/corpus/templates/assets/app/components/library/EditLibraryDialog.tsx +7 -7
- package/corpus/templates/assets/app/components/library/LibraryCard.tsx +8 -8
- package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +3 -2
- package/corpus/templates/assets/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/button.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/card.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/input.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/label.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/select.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/separator.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/sonner.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/spinner.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/assets/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/assets/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/assets/app/global.css +1 -0
- package/corpus/templates/assets/app/root.tsx +12 -9
- package/corpus/templates/assets/app/routes/asset.$id.tsx +19 -19
- package/corpus/templates/assets/app/routes/audit.tsx +15 -14
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +49 -54
- package/corpus/templates/assets/app/routes/library.tsx +31 -31
- package/corpus/templates/assets/app/routes/settings.tsx +21 -35
- package/corpus/templates/assets/changelog/2026-07-06-mobile-layouts-no-longer-stack-two-top-headers.md +5 -0
- package/corpus/templates/brain/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/brain/.agents/skills/ask-across-everything/SKILL.md +6 -1
- package/corpus/templates/brain/.agents/skills/brain/SKILL.md +63 -5
- package/corpus/templates/brain/.agents/skills/ingestion-and-connectors/SKILL.md +112 -0
- package/corpus/templates/brain/AGENTS.md +93 -8
- package/corpus/templates/brain/app/components/brain/CanonicalPreviewSheet.tsx +6 -5
- package/corpus/templates/brain/app/components/brain/Surface.tsx +3 -8
- package/corpus/templates/brain/app/components/layout/Layout.tsx +7 -7
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +6 -6
- package/corpus/templates/brain/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/button.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/card.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/input.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/label.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/progress.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/select.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/separator.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/table.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/brain/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/brain/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/brain/app/global.css +1 -0
- package/corpus/templates/brain/app/hooks/use-distillation-bridge.ts +9 -4
- package/corpus/templates/brain/app/root.tsx +11 -8
- package/corpus/templates/brain/app/routes/knowledge.tsx +19 -19
- package/corpus/templates/brain/app/routes/ops.tsx +18 -18
- package/corpus/templates/brain/app/routes/review.tsx +22 -27
- package/corpus/templates/brain/app/routes/search.tsx +20 -20
- package/corpus/templates/brain/app/routes/settings.tsx +25 -40
- package/corpus/templates/brain/app/routes/sources.tsx +23 -23
- package/corpus/templates/calendar/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/calendar/actions/find-a-time.ts +29 -16
- package/corpus/templates/calendar/actions/list-bookings.ts +33 -2
- package/corpus/templates/calendar/actions/list-events.ts +34 -8
- package/corpus/templates/calendar/actions/update-calendar-visual-preferences.ts +105 -34
- package/corpus/templates/calendar/app/components/ThemeToggle.tsx +5 -5
- package/corpus/templates/calendar/app/components/TimezoneCombobox.tsx +6 -6
- package/corpus/templates/calendar/app/components/booking/BookingConfirmation.tsx +2 -1
- package/corpus/templates/calendar/app/components/booking/BookingForm.tsx +9 -8
- package/corpus/templates/calendar/app/components/booking/DatePicker.tsx +2 -2
- package/corpus/templates/calendar/app/components/booking/TimeSlotPicker.tsx +3 -2
- package/corpus/templates/calendar/app/components/calendar/AddCalendarDialog.tsx +10 -15
- package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +6 -6
- package/corpus/templates/calendar/app/components/calendar/CommandPalette.tsx +73 -3
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +27 -27
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +553 -267
- package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +9 -12
- package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +10 -13
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +7 -7
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +21 -21
- package/corpus/templates/calendar/app/components/calendar/EventDialog.tsx +11 -11
- package/corpus/templates/calendar/app/components/calendar/EventOptionControls.tsx +9 -9
- package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +9 -9
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +6 -6
- package/corpus/templates/calendar/app/components/calendar/GoogleSetupWizard.tsx +3 -3
- package/corpus/templates/calendar/app/components/calendar/GoogleSyncButton.tsx +1 -1
- package/corpus/templates/calendar/app/components/calendar/GuestNotificationDialog.tsx +8 -10
- package/corpus/templates/calendar/app/components/calendar/IntegrationsSidebar.tsx +12 -12
- package/corpus/templates/calendar/app/components/calendar/KeyboardShortcutsHelp.tsx +2 -2
- package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +93 -41
- package/corpus/templates/calendar/app/components/calendar/PeopleSearchDialog.tsx +6 -6
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +530 -299
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +229 -134
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +135 -108
- package/corpus/templates/calendar/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/button.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/card.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/collapsible.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/command.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/input.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/label.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/radio-group.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/select.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/separator.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/spinner.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/table.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/calendar/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/calendar/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/calendar/app/global.css +1 -0
- package/corpus/templates/calendar/app/hooks/use-event-drag.ts +63 -9
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +15 -27
- package/corpus/templates/calendar/app/hooks/use-grid-create-drag.ts +254 -0
- package/corpus/templates/calendar/app/hooks/use-mobile.ts +1 -0
- package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +287 -21
- package/corpus/templates/calendar/app/hooks/use-zoom-auth.ts +56 -19
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/calendar/app/i18n-data.ts +31 -0
- package/corpus/templates/calendar/app/lib/event-colors.ts +29 -8
- package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +11 -11
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +45 -50
- package/corpus/templates/calendar/app/pages/BookingPage.tsx +2 -2
- package/corpus/templates/calendar/app/pages/BookingsList.tsx +12 -12
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +438 -271
- package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +13 -13
- package/corpus/templates/calendar/app/pages/Settings.tsx +15 -29
- package/corpus/templates/calendar/app/root.tsx +19 -15
- package/corpus/templates/calendar/app/routes/event.tsx +2 -2
- package/corpus/templates/calendar/changelog/2026-07-06-calendar-now-loads-view-preferences-much-more-efficiently.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-06-connected-calendars-keep-their-own-color-when-you-customize-.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-06-create-events-from-the-command-menu-with-natural-phrases-lik.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-06-drag-across-empty-week-or-day-slots-to-create-an-event-with-.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-06-dragging-and-resizing-events-is-much-smoother-especially-on-.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-06-each-connected-google-account-can-now-have-its-own-calendar-.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-06-events-spanning-multiple-days-now-show-on-every-day-they-cov.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-06-google-calendar-now-finishes-connecting-correctly-after-sign.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-06-public-booking-pages-respond-faster.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-06-the-app-header-no-longer-shows-the-global-notifications-bell.md +6 -0
- package/corpus/templates/calendar/package.json +1 -0
- package/corpus/templates/calendar/server/handlers/bookings.ts +46 -52
- package/corpus/templates/calendar/server/handlers/google-auth.ts +21 -5
- package/corpus/templates/calendar/server/lib/google-calendar.ts +32 -3
- package/corpus/templates/calendar/shared/calendar-view-preferences.ts +66 -2
- package/corpus/templates/chat/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/chat/app/components/layout/Layout.tsx +6 -6
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +12 -12
- package/corpus/templates/chat/app/components/ui/button.tsx +1 -0
- package/corpus/templates/chat/app/components/ui/card.tsx +1 -0
- package/corpus/templates/chat/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/chat/app/components/ui/input.tsx +1 -0
- package/corpus/templates/chat/app/components/ui/label.tsx +1 -0
- package/corpus/templates/chat/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/chat/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/chat/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/chat/app/global.css +1 -0
- package/corpus/templates/chat/app/root.tsx +7 -4
- package/corpus/templates/chat/app/routes/settings.tsx +6 -21
- package/corpus/templates/clips/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +18 -4
- package/corpus/templates/clips/.env.example +8 -0
- package/corpus/templates/clips/actions/accept-invite.ts +3 -0
- package/corpus/templates/clips/actions/add-recording-to-space.ts +55 -27
- package/corpus/templates/clips/actions/add-vocabulary-term.ts +3 -1
- package/corpus/templates/clips/actions/archive-recording.ts +5 -12
- package/corpus/templates/clips/actions/cleanup-transcript.ts +5 -4
- package/corpus/templates/clips/actions/clear-edits.ts +42 -29
- package/corpus/templates/clips/actions/create-meeting.ts +87 -40
- package/corpus/templates/clips/actions/create-recording.ts +6 -3
- package/corpus/templates/clips/actions/delete-meeting.ts +25 -0
- package/corpus/templates/clips/actions/delete-recording-permanent.ts +3 -0
- package/corpus/templates/clips/actions/disconnect-calendar.ts +10 -4
- package/corpus/templates/clips/actions/export-insights-csv.ts +28 -8
- package/corpus/templates/clips/actions/finalize-meeting.ts +106 -33
- package/corpus/templates/clips/actions/finalize-recording.ts +201 -3
- package/corpus/templates/clips/actions/get-organization-insights.ts +51 -7
- package/corpus/templates/clips/actions/import-loom-recording.ts +17 -0
- package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +18 -1
- package/corpus/templates/clips/actions/lib/create-recording-schema.ts +7 -1
- package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +16 -0
- package/corpus/templates/clips/actions/list-clip-views.ts +60 -0
- package/corpus/templates/clips/actions/regenerate-summary.ts +10 -0
- package/corpus/templates/clips/actions/remove-vocabulary-term.ts +59 -0
- package/corpus/templates/clips/actions/request-transcript.ts +170 -25
- package/corpus/templates/clips/actions/restore-recording.ts +5 -12
- package/corpus/templates/clips/actions/save-browser-transcript.ts +13 -2
- package/corpus/templates/clips/actions/set-chapters.ts +6 -15
- package/corpus/templates/clips/actions/set-thumbnail.ts +77 -36
- package/corpus/templates/clips/actions/split-recording.ts +49 -35
- package/corpus/templates/clips/actions/stitch-recordings.ts +7 -4
- package/corpus/templates/clips/actions/stop-meeting-recording.ts +15 -1
- package/corpus/templates/clips/actions/sync-calendars.ts +10 -108
- package/corpus/templates/clips/actions/trash-recording.ts +43 -14
- package/corpus/templates/clips/actions/trim-recording.ts +48 -27
- package/corpus/templates/clips/actions/undo-edit.ts +49 -37
- package/corpus/templates/clips/app/components/capture-install-options.tsx +6 -6
- package/corpus/templates/clips/app/components/dictate/vocabulary-section.tsx +208 -0
- package/corpus/templates/clips/app/components/editable-recording-title.tsx +2 -2
- package/corpus/templates/clips/app/components/editor/chapters-editor.tsx +7 -7
- package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +26 -26
- package/corpus/templates/clips/app/components/editor/split-button.tsx +5 -4
- package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +9 -9
- package/corpus/templates/clips/app/components/editor/thumbnail-picker.tsx +11 -16
- package/corpus/templates/clips/app/components/editor/timeline.tsx +3 -3
- package/corpus/templates/clips/app/components/editor/transcript-editor.tsx +5 -5
- package/corpus/templates/clips/app/components/library/bulk-action-toolbar.tsx +4 -4
- package/corpus/templates/clips/app/components/library/create-space-dialog.tsx +8 -8
- package/corpus/templates/clips/app/components/library/empty-state.tsx +2 -1
- package/corpus/templates/clips/app/components/library/folder-tree.tsx +14 -14
- package/corpus/templates/clips/app/components/library/library-grid.tsx +24 -98
- package/corpus/templates/clips/app/components/library/library-layout.tsx +18 -19
- package/corpus/templates/clips/app/components/library/organization-switcher.tsx +11 -11
- package/corpus/templates/clips/app/components/library/recording-card.tsx +44 -50
- package/corpus/templates/clips/app/components/library/search-bar.tsx +5 -5
- package/corpus/templates/clips/app/components/library/sort-menu.tsx +4 -3
- package/corpus/templates/clips/app/components/library/tag-input.tsx +4 -4
- package/corpus/templates/clips/app/components/meetings/attendee-stack.tsx +2 -6
- package/corpus/templates/clips/app/components/meetings/auto-record-prompt.tsx +1 -1
- package/corpus/templates/clips/app/components/meetings/bullet-link.tsx +4 -4
- package/corpus/templates/clips/app/components/meetings/canvas-editor.tsx +5 -5
- package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +3 -3
- package/corpus/templates/clips/app/components/meetings/quick-ask-sidebar.tsx +17 -9
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +6 -11
- package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +215 -21
- package/corpus/templates/clips/app/components/player/access-password-prompt.tsx +3 -2
- package/corpus/templates/clips/app/components/player/comments-panel.tsx +14 -14
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +7 -6
- package/corpus/templates/clips/app/components/player/insights-panel.tsx +18 -6
- package/corpus/templates/clips/app/components/player/player-controls.tsx +42 -36
- package/corpus/templates/clips/app/components/player/reactions-tray.tsx +4 -4
- package/corpus/templates/clips/app/components/player/settings-panel.tsx +13 -12
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +80 -49
- package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +3 -2
- package/corpus/templates/clips/app/components/player/timestamped-comment-button.tsx +2 -2
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +7 -7
- package/corpus/templates/clips/app/components/player/video-player.tsx +182 -15
- package/corpus/templates/clips/app/components/recorder/camera-visualizer.tsx +1 -1
- package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +1 -1
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +49 -36
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +285 -24
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +6 -5
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +7 -6
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +10 -10
- package/corpus/templates/clips/app/components/sharing/viewed-by-popover.tsx +132 -0
- package/corpus/templates/clips/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/avatar.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/button.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/card.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/collapsible.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/context-menu.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/input.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/label.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/scroll-area.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/select.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/separator.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/slider.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/sonner.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/spinner.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/table.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/clips/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/clips/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +5 -9
- package/corpus/templates/clips/app/components/workspace/insights-hub.tsx +9 -19
- package/corpus/templates/clips/app/components/workspace/invite-dialog.tsx +10 -9
- package/corpus/templates/clips/app/components/workspace/members-list.tsx +11 -10
- package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +2 -1
- package/corpus/templates/clips/app/components/workspace/top-creators-table.tsx +3 -2
- package/corpus/templates/clips/app/components/workspace/top-videos-table.tsx +3 -2
- package/corpus/templates/clips/app/global.css +1 -0
- package/corpus/templates/clips/app/hooks/use-desktop-promo.ts +2 -1
- package/corpus/templates/clips/app/hooks/use-mobile.ts +1 -0
- package/corpus/templates/clips/app/hooks/use-player-shortcuts.ts +14 -10
- package/corpus/templates/clips/app/hooks/use-view-tracking.ts +14 -0
- package/corpus/templates/clips/app/i18n/ar-SA.ts +56 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +53 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +52 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +53 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +53 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +52 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +53 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +52 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +53 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +51 -0
- package/corpus/templates/clips/app/i18n/zh-TW.ts +51 -0
- package/corpus/templates/clips/app/lib/recorder-preferences.ts +4 -0
- package/corpus/templates/clips/app/lib/timestamp-mapping.ts +5 -0
- package/corpus/templates/clips/app/root.tsx +26 -23
- package/corpus/templates/clips/app/routes/_app.dictate.tsx +39 -23
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +134 -39
- package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +20 -20
- package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -3
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +92 -60
- package/corpus/templates/clips/app/routes/_app.settings.organization.tsx +8 -13
- package/corpus/templates/clips/app/routes/_app.spaces._index.tsx +1 -1
- package/corpus/templates/clips/app/routes/_app.trash.tsx +96 -31
- package/corpus/templates/clips/app/routes/bug-report.done.tsx +1 -1
- package/corpus/templates/clips/app/routes/bug-report.tsx +11 -11
- package/corpus/templates/clips/app/routes/download.tsx +2 -2
- package/corpus/templates/clips/app/routes/embed.$shareId.tsx +2 -1
- package/corpus/templates/clips/app/routes/invite.$token.tsx +8 -13
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +263 -251
- package/corpus/templates/clips/app/routes/record.tsx +28 -10
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +51 -29
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +71 -16
- package/corpus/templates/clips/changelog/2026-07-05-mobile-clip-pages-use-a-tighter-viewer-layout-with-inline-ac.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-ai-setup-now-switches-to-your-saved-provider-key-and-uses-fa.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-browsers-that-can-t-play-a-clip-s-video-format-now-see-a-cle.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-calendar-connections-no-longer-ask-you-to-reconnect-after-te.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-chrome-extension-recordings-keep-your-selected-microphone.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-clip-recordings-now-use-streaming-uploads-by-default.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-clip-titles-in-the-library-grid-now-open-the-clip-instead-of.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-desktop-countdown-now-dims-the-screen-before-recording.md +5 -0
- package/corpus/templates/clips/changelog/2026-07-06-desktop-popover-no-longer-drops-bluetooth-headphones-into-call-quality-mode.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-desktop-recorder-now-refreshes-cameras-and-mics-when-the-pop.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-desktop-recordings-keep-microphone-volume-clear-while-recor.md +5 -0
- package/corpus/templates/clips/changelog/2026-07-06-desktop-recordings-no-longer-get-stuck-optimizing-when-scree.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-dictation-history-from-the-desktop-app-now-appears-in-the-di.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-dictation-no-longer-overwrites-your-clipboard-and-messaging-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-double-tap-your-dictation-key-for-hands-free-dictation-tap-a.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-fixed-desktop-recordings-sometimes-uploading-without-audio-a.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-fixed-desktop-recordings-that-could-get-stuck-uploading-and-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-fixed-recordings-sometimes-capturing-from-the-wrong-micropho.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-hands-free-dictation-and-meeting-cleanup-recover-more-reliab.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-live-meetings-show-time-remaining-and-you-can-end-a-meeting-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-manage-your-personal-dictation-dictionary-from-the-dictate-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-meetings-interrupted-by-quitting-or-crashing-now-close-out-c.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-mic-only-desktop-recordings-now-use-the-selected-microphone-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-paste-your-last-dictation-anytime-with-cmd-ctrl-v-or-from-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-press-esc-to-cancel-a-dictation-in-progress.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-recordings-whose-video-file-contains-no-audio-track-no-longe.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-search-inside-meeting-transcripts.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-see-who-viewed-your-shared-clips-and-when-click-the-view-cou.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-share-links-for-agents-now-appear-as-soon-as-the-share-menu-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-share-popovers-now-stay-above-video-playback-controls.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-shared-clip-pages-now-recover-automatically-from-transient-v.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-the-desktop-app-now-walks-you-through-permissions-on-first-l.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-transcript-cleanup-preferences-now-save-per-user.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-transcripts-now-retry-automatically-after-transient-failures.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-06-video-compression-now-runs-in-the-background-without-blocking.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-07-native-microphone-recordings-now-reduce-steady-background-noise.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-07-recording-transcripts-now-line-up-with-the-video-start-afte.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +141 -48
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +383 -59
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +51 -10
- package/corpus/templates/clips/chrome-extension/src/popup.ts +57 -6
- package/corpus/templates/clips/desktop/package.json +1 -1
- package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +67 -0
- package/corpus/templates/clips/desktop/src/app.tsx +38 -13
- package/corpus/templates/clips/desktop/src/components/SourceRow.tsx +1 -0
- package/corpus/templates/clips/desktop/src/hooks/useMediaDevices.ts +88 -28
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +165 -55
- package/corpus/templates/clips/desktop/src/hooks/useMicMeter.ts +35 -10
- package/corpus/templates/clips/desktop/src/lib/backtrack.ts +12 -0
- package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +129 -34
- package/corpus/templates/clips/desktop/src/lib/media-device-selection.ts +157 -0
- package/corpus/templates/clips/desktop/src/lib/permissions.ts +2 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +109 -47
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +22 -2
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +29 -1
- package/corpus/templates/clips/desktop/src/lib/updater.ts +6 -0
- package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +372 -31
- package/corpus/templates/clips/desktop/src/overlays/finalizing.tsx +90 -0
- package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +4 -6
- package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +214 -9
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +37 -52
- package/corpus/templates/clips/desktop/src/styles.css +1 -1
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +7 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +371 -43
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +79 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1418 -260
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +188 -44
- package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +197 -8
- package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +64 -8
- package/corpus/templates/clips/desktop/src-tauri/src/state.rs +3 -2
- package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +10 -0
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +29 -8
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +136 -13
- package/corpus/templates/clips/desktop/vite.config.ts +3 -0
- package/corpus/templates/clips/package.json +1 -1
- package/corpus/templates/clips/server/db/schema.ts +26 -0
- package/corpus/templates/clips/server/jobs/builder-media-compression.ts +41 -0
- package/corpus/templates/clips/server/jobs/stale-meeting-sweeper.ts +294 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +976 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +107 -56
- package/corpus/templates/clips/server/lib/env-flags.ts +4 -0
- package/corpus/templates/clips/server/lib/google-calendar-client.ts +8 -1
- package/corpus/templates/clips/server/lib/media-worker.ts +153 -0
- package/corpus/templates/clips/server/lib/s3-upload-provider.ts +58 -17
- package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +17 -5
- package/corpus/templates/clips/server/lib/video-remux.ts +90 -0
- package/corpus/templates/clips/server/plugins/calendar-jobs.ts +4 -0
- package/corpus/templates/clips/server/plugins/db.ts +196 -0
- package/corpus/templates/clips/server/register-secrets.ts +78 -0
- package/corpus/templates/clips/server/routes/_agent-native/clips/user-prefs.get.ts +15 -0
- package/corpus/templates/clips/server/routes/_agent-native/clips/user-prefs.put.ts +27 -0
- package/corpus/templates/clips/server/routes/api/media-worker/callback.post.ts +88 -0
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +87 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +7 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +35 -10
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +24 -42
- package/corpus/templates/clips/server/routes/api/view-event.post.ts +29 -1
- package/corpus/templates/clips/shared/media-device-selection.ts +157 -0
- package/corpus/templates/clips/shared/media-worker-contract.ts +146 -0
- package/corpus/templates/content/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/content/.agents/skills/notion-integration/SKILL.md +67 -2
- package/corpus/templates/content/AGENTS.md +22 -14
- package/corpus/templates/content/actions/_local-file-documents.ts +90 -1
- package/corpus/templates/content/actions/_notion-action-utils.ts +6 -2
- package/corpus/templates/content/actions/delete-document.ts +8 -0
- package/corpus/templates/content/actions/remove-local-file-source.ts +100 -0
- package/corpus/templates/content/actions/sync-notion-comments.ts +174 -20
- package/corpus/templates/content/actions/update-document.ts +91 -5
- package/corpus/templates/content/app/blocks/SourceComponentBlock.tsx +31 -3
- package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +4 -4
- package/corpus/templates/content/app/components/EmptyState.tsx +1 -1
- package/corpus/templates/content/app/components/ThemeToggle.tsx +6 -6
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +5 -5
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +5 -5
- package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +1 -1
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +10 -14630
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +72 -51
- package/corpus/templates/content/app/components/editor/DocumentEditorSkeleton.tsx +36 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +29 -29
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +29 -44
- package/corpus/templates/content/app/components/editor/EmojiPicker.tsx +5 -4
- package/corpus/templates/content/app/components/editor/LinkHoverPreview.tsx +5 -4
- package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +1 -1
- package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +2 -2
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +5 -5
- package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +13 -13
- package/corpus/templates/content/app/components/editor/VersionHistoryPanel.tsx +10 -10
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +24 -7
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2856 -8389
- package/corpus/templates/content/app/components/editor/database/navigation-state.ts +36 -1
- package/corpus/templates/content/app/components/editor/database/settings.tsx +3224 -0
- package/corpus/templates/content/app/components/editor/database/shared.tsx +19 -19
- package/corpus/templates/content/app/components/editor/database/text.ts +23 -0
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +9 -8
- package/corpus/templates/content/app/components/editor/extensions/AudioBlock.tsx +19 -18
- package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +21 -20
- package/corpus/templates/content/app/components/editor/extensions/LocalMdxComponentNode.tsx +8 -8
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +9 -0
- package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +19 -18
- package/corpus/templates/content/app/components/layout/Header.tsx +1 -5
- package/corpus/templates/content/app/components/layout/Layout.tsx +25 -8
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +203 -55
- package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +22 -22
- package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +11 -11
- package/corpus/templates/content/app/components/sidebar/document-sidebar-sections.ts +109 -1
- package/corpus/templates/content/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/content/app/components/ui/button.tsx +1 -0
- package/corpus/templates/content/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/content/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/content/app/components/ui/input.tsx +1 -0
- package/corpus/templates/content/app/components/ui/label.tsx +1 -0
- package/corpus/templates/content/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/content/app/components/ui/separator.tsx +1 -0
- package/corpus/templates/content/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/content/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/content/app/components/ui/sonner.tsx +1 -0
- package/corpus/templates/content/app/components/ui/spinner.tsx +1 -0
- package/corpus/templates/content/app/components/ui/table.tsx +1 -0
- package/corpus/templates/content/app/components/ui/toaster.tsx +1 -0
- package/corpus/templates/content/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/content/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/content/app/global.css +1 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +89 -37
- package/corpus/templates/content/app/hooks/use-mobile.ts +1 -0
- package/corpus/templates/content/app/hooks/use-notion.ts +73 -5
- package/corpus/templates/content/app/i18n/zh-TW.ts +14 -0
- package/corpus/templates/content/app/i18n-data.ts +153 -0
- package/corpus/templates/content/app/root.tsx +35 -29
- package/corpus/templates/content/app/routes/_app._index.tsx +1 -1
- package/corpus/templates/content/app/routes/_app.local-files.tsx +268 -81
- package/corpus/templates/content/app/routes/_app.settings.tsx +4 -21
- package/corpus/templates/content/app/routes/p.$id.tsx +114 -9
- package/corpus/templates/content/changelog/2026-07-01-builder-source-components-show-preservation-and-review-states.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-document-headers-and-page-bodies-now-share-one-background-shade.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-export-submenus-stay-visible-outside-the-page-actions-menu.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-fixed-collaborative-documents-reapplying-content-before-save.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-local-files-now-remember-sidebar-collapse-state-and-include-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-notion-comment-sync-now-preserves-threads-replies-stay-attac.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-notion-sync-is-far-more-reliable-auto-sync-no-longer-overwri.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-opening-pages-now-highlights-the-sidebar-item-and-shows-the-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-presence-markers-in-the-editor-are-now-compact-instead-of-co.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-sidebar-collapse-now-keeps-the-same-background-shade-as-the-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-06-the-content-header-no-longer-shows-the-global-notifications-.md +6 -0
- package/corpus/templates/content/parity/matrix.md +24 -24
- package/corpus/templates/content/parity/matrix.ts +7 -3
- package/corpus/templates/content/server/db/index.ts +9 -0
- package/corpus/templates/content/server/db/schema.ts +15 -0
- package/corpus/templates/content/server/lib/notion-sync.ts +481 -39
- package/corpus/templates/content/server/lib/notion.ts +212 -48
- package/corpus/templates/content/server/plugins/db.ts +20 -0
- package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +90 -0
- package/corpus/templates/content/server/routes/api/documents/[id]/notion/resolve.post.ts +14 -2
- package/corpus/templates/content/server/routes/api/notion/callback.get.ts +54 -6
- package/corpus/templates/content/shared/agent-readable.ts +56 -0
- package/corpus/templates/content/shared/builder-mdx.ts +68 -12
- package/corpus/templates/content/shared/builder-source-component-registry.ts +177 -0
- package/corpus/templates/content/shared/nfm.ts +459 -66
- package/corpus/templates/content/shared/source-component-block.ts +72 -6
- package/corpus/templates/content/vite.config.ts +17 -1
- package/corpus/templates/design/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +21 -12
- package/corpus/templates/design/AGENTS.md +3 -2
- package/corpus/templates/design/actions/import-design-source.ts +8 -58
- package/corpus/templates/design/actions/run-design-audit.ts +127 -1
- package/corpus/templates/design/actions/take-design-screenshot.ts +673 -0
- package/corpus/templates/design/app/components/ThemeToggle.tsx +6 -6
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +11 -11
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +2 -2
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +16 -21
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +6 -97
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +7 -7
- package/corpus/templates/design/app/components/design/EditPanel.tsx +30 -30
- package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +9 -8
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +14 -14
- package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +6 -5
- package/corpus/templates/design/app/components/design/MotionDock.tsx +15 -15
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +3 -3
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +3 -3
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +13 -13
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +14 -14
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +6 -6
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +39 -9
- package/corpus/templates/design/app/components/design/code-workbench/ActivityBar.tsx +3 -3
- package/corpus/templates/design/app/components/design/code-workbench/SideBar.tsx +4 -7
- package/corpus/templates/design/app/components/design/code-workbench/editor/EditorTabs.tsx +6 -6
- package/corpus/templates/design/app/components/design/code-workbench/editor/MonacoHost.tsx +2 -1
- package/corpus/templates/design/app/components/design/code-workbench/explorer/FileTree.tsx +17 -17
- package/corpus/templates/design/app/components/design/code-workbench/search/SearchView.tsx +6 -6
- package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +3 -3
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +7 -7
- package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +3 -3
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +18 -18
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +6 -6
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +6 -6
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +7 -7
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +7 -7
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +14 -14
- package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +9 -9
- package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +6 -6
- package/corpus/templates/design/app/components/design-system/DesignSystemCard.tsx +3 -2
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +15 -18
- package/corpus/templates/design/app/components/layout/Header.tsx +2 -2
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +5 -5
- package/corpus/templates/design/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/design/app/components/ui/avatar.tsx +1 -0
- package/corpus/templates/design/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/design/app/components/ui/button.tsx +1 -0
- package/corpus/templates/design/app/components/ui/card.tsx +1 -0
- package/corpus/templates/design/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/design/app/components/ui/collapsible.tsx +1 -0
- package/corpus/templates/design/app/components/ui/context-menu.tsx +1 -0
- package/corpus/templates/design/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/design/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/design/app/components/ui/input.tsx +1 -0
- package/corpus/templates/design/app/components/ui/label.tsx +1 -0
- package/corpus/templates/design/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/design/app/components/ui/select.tsx +1 -0
- package/corpus/templates/design/app/components/ui/separator.tsx +1 -0
- package/corpus/templates/design/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/design/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/design/app/components/ui/slider.tsx +1 -0
- package/corpus/templates/design/app/components/ui/sonner.tsx +1 -0
- package/corpus/templates/design/app/components/ui/spinner.tsx +1 -0
- package/corpus/templates/design/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/design/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/design/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/design/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/design/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +7 -7
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +7 -7
- package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +3 -3
- package/corpus/templates/design/app/global.css +1 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +3 -4
- package/corpus/templates/design/app/i18n-data.ts +38 -35
- package/corpus/templates/design/app/pages/DesignEditor.tsx +47 -51
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +6 -6
- package/corpus/templates/design/app/pages/DesignSystems.tsx +31 -31
- package/corpus/templates/design/app/pages/Index.tsx +25 -29
- package/corpus/templates/design/app/pages/NotFound.tsx +2 -1
- package/corpus/templates/design/app/pages/Present.tsx +2 -1
- package/corpus/templates/design/app/pages/VisualEdit.tsx +2 -1
- package/corpus/templates/design/app/root.tsx +7 -4
- package/corpus/templates/design/app/routes/settings.tsx +22 -49
- package/corpus/templates/design/changelog/2026-07-05-code-sidebar-tabs-now-use-a-quieter-active-highlight-that-ma.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-fig-files-now-stay-on-the-builder-indexing-path-through-desi.md +6 -0
- package/corpus/templates/design/changelog/2026-07-06-mobile-layouts-no-longer-stack-two-top-headers.md +5 -0
- package/corpus/templates/design/changelog/2026-07-06-the-design-agent-now-visually-reviews-rendered-screenshots-o.md +6 -0
- package/corpus/templates/design/server/db/index.ts +8 -0
- package/corpus/templates/design/server/handlers/import-design-file.ts +4 -43
- package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +2 -2
- package/corpus/templates/design/server/lib/visible-clipboard-html.ts +32 -0
- package/corpus/templates/design/server/routes/[...page].get.ts +94 -1
- package/corpus/templates/design/server/routes/api/design-agent-context.json.get.ts +84 -0
- package/corpus/templates/design/shared/agent-readable.ts +2 -0
- package/corpus/templates/design/shared/design-review.ts +1 -0
- package/corpus/templates/dispatch/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/dispatch/actions/list-staged-datasets.ts +3 -0
- package/corpus/templates/dispatch/app/routes/settings.tsx +3 -17
- package/corpus/templates/forms/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/forms/app/components/CloudUpgrade.tsx +10 -10
- package/corpus/templates/forms/app/components/ThemeToggle.tsx +5 -5
- package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +11 -10
- package/corpus/templates/forms/app/components/builder/FieldRenderer.tsx +10 -13
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +17 -17
- package/corpus/templates/forms/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/button.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/calendar.tsx +1 -1
- package/corpus/templates/forms/app/components/ui/card.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/command.tsx +1 -1
- package/corpus/templates/forms/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/input.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/label.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/radio-group.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/scroll-area.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/select.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/separator.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/slider.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/table.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/forms/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/forms/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/forms/app/global.css +1 -0
- package/corpus/templates/forms/app/hooks/use-mobile.ts +1 -0
- package/corpus/templates/forms/app/hooks/use-navigation-state.ts +0 -1
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +24 -26
- package/corpus/templates/forms/app/pages/FormFillPage.tsx +2 -2
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +21 -21
- package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +11 -11
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +4 -4
- package/corpus/templates/forms/app/root.tsx +11 -8
- package/corpus/templates/forms/app/routes/_app.settings.tsx +6 -21
- package/corpus/templates/forms/app/routes/form-preview.tsx +3 -3
- package/corpus/templates/forms/changelog/2026-07-06-the-form-builder-header-no-longer-shows-the-global-notificat.md +6 -0
- package/corpus/templates/macros/app/components/AddExerciseDialog.tsx +1 -1
- package/corpus/templates/macros/app/components/AddMealDialog.tsx +1 -1
- package/corpus/templates/macros/app/components/AddWeightDialog.tsx +1 -1
- package/corpus/templates/macros/app/components/DailyProgress.tsx +7 -7
- package/corpus/templates/macros/app/components/WeeklyCaloriesChart.tsx +2 -1
- package/corpus/templates/macros/app/components/WeightCard.tsx +10 -10
- package/corpus/templates/macros/app/components/WeightTracker.tsx +1 -1
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +6 -10
- package/corpus/templates/macros/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/macros/app/components/ui/label.tsx +1 -0
- package/corpus/templates/macros/app/components/ui/select.tsx +1 -0
- package/corpus/templates/macros/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/macros/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/macros/app/components/ui/sonner.tsx +1 -0
- package/corpus/templates/macros/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/macros/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/macros/app/global.css +1 -0
- package/corpus/templates/macros/app/root.tsx +17 -14
- package/corpus/templates/macros/app/routes/_index.tsx +1 -1
- package/corpus/templates/macros/app/routes/analytics.tsx +8 -8
- package/corpus/templates/macros/app/routes/settings.tsx +4 -19
- package/corpus/templates/mail/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +67 -0
- package/corpus/templates/mail/.agents/skills/draft-queue/SKILL.md +35 -1
- package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +74 -33
- package/corpus/templates/mail/AGENTS.md +102 -16
- package/corpus/templates/mail/actions/archive-email.ts +67 -11
- package/corpus/templates/mail/actions/manage-snippets.ts +106 -0
- package/corpus/templates/mail/actions/mark-read.ts +45 -11
- package/corpus/templates/mail/actions/send-queued-drafts.ts +77 -42
- package/corpus/templates/mail/actions/star-email.ts +55 -10
- package/corpus/templates/mail/actions/trash-email.ts +48 -9
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +1 -1
- package/corpus/templates/mail/app/components/ThemeToggle.tsx +6 -6
- package/corpus/templates/mail/app/components/agent-engine-picker.tsx +10 -10
- package/corpus/templates/mail/app/components/email/CodeBlockLangPicker.tsx +4 -3
- package/corpus/templates/mail/app/components/email/ComposeBubbleToolbar.tsx +5 -5
- package/corpus/templates/mail/app/components/email/ComposeEditor.tsx +109 -10
- package/corpus/templates/mail/app/components/email/ComposeModal.tsx +44 -19
- package/corpus/templates/mail/app/components/email/ComposeSlashMenu.tsx +139 -17
- package/corpus/templates/mail/app/components/email/EmailList.tsx +522 -314
- package/corpus/templates/mail/app/components/email/EmailListItem.tsx +130 -45
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +9 -9
- package/corpus/templates/mail/app/components/email/InlineReplyComposer.tsx +35 -13
- package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +13 -13
- package/corpus/templates/mail/app/components/email/MobileActionBar.tsx +13 -13
- package/corpus/templates/mail/app/components/email/RecipientInput.tsx +53 -42
- package/corpus/templates/mail/app/components/email/SendLaterButton.tsx +5 -4
- package/corpus/templates/mail/app/components/email/SnoozeModal.tsx +1 -1
- package/corpus/templates/mail/app/components/email/SnoozePopover.tsx +6 -6
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +19 -23
- package/corpus/templates/mail/app/components/layout/HeaderActions.tsx +12 -4
- package/corpus/templates/mail/app/components/layout/SearchBar.tsx +138 -18
- package/corpus/templates/mail/app/components/settings/GmailFiltersSection.tsx +23 -23
- package/corpus/templates/mail/app/components/settings/SnippetsSection.tsx +303 -0
- package/corpus/templates/mail/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/mail/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/mail/app/components/ui/button.tsx +1 -0
- package/corpus/templates/mail/app/components/ui/calendar.tsx +1 -1
- package/corpus/templates/mail/app/components/ui/command.tsx +1 -1
- package/corpus/templates/mail/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/mail/app/components/ui/drawer.tsx +1 -0
- package/corpus/templates/mail/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/mail/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/mail/app/components/ui/select.tsx +1 -0
- package/corpus/templates/mail/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/mail/app/components/ui/spinner.tsx +1 -0
- package/corpus/templates/mail/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/mail/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/mail/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/mail/app/global.css +20 -3
- package/corpus/templates/mail/app/hooks/use-emails.ts +187 -0
- package/corpus/templates/mail/app/hooks/use-snippets.ts +103 -0
- package/corpus/templates/mail/app/i18n/ar-SA.ts +17 -0
- package/corpus/templates/mail/app/i18n/de-DE.ts +18 -0
- package/corpus/templates/mail/app/i18n/en-US.ts +17 -0
- package/corpus/templates/mail/app/i18n/es-ES.ts +17 -0
- package/corpus/templates/mail/app/i18n/fr-FR.ts +18 -0
- package/corpus/templates/mail/app/i18n/hi-IN.ts +17 -0
- package/corpus/templates/mail/app/i18n/ja-JP.ts +18 -0
- package/corpus/templates/mail/app/i18n/ko-KR.ts +17 -0
- package/corpus/templates/mail/app/i18n/pt-BR.ts +17 -0
- package/corpus/templates/mail/app/i18n/zh-CN.ts +16 -0
- package/corpus/templates/mail/app/i18n/zh-TW.ts +16 -0
- package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +14 -14
- package/corpus/templates/mail/app/pages/InboxPage.tsx +3 -3
- package/corpus/templates/mail/app/pages/NotFound.tsx +2 -1
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +64 -66
- package/corpus/templates/mail/app/root.tsx +21 -18
- package/corpus/templates/mail/app/routes/_index.tsx +2 -1
- package/corpus/templates/mail/app/routes/email.tsx +2 -2
- package/corpus/templates/mail/changelog/2026-07-06-a-queued-draft-can-no-longer-be-sent-twice-by-simultaneous-s.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-06-bulk-archive-trash-star-and-mark-read-now-complete-in-one-fa.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-06-dark-mode-sidebars-and-notifications-now-use-the-softer-gray.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-06-inbox-automations-pick-up-new-mail-faster.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-06-inbox-lists-stay-fast-as-scheduled-and-snoozed-mail-accumula.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-06-long-inboxes-scroll-smoothly-and-stay-fast-as-more-mail-load.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-06-paste-or-drop-images-straight-into-the-composer-to-send-them.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-06-save-reusable-snippets-and-insert-them-from-the-compose-slas.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-06-scrolling-older-mail-keeps-loading-correctly-when-no-gmail-a.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-06-search-shows-instant-matches-from-already-loaded-mail-while-.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-06-sends-now-fail-with-a-clear-error-instead-of-quietly-missing.md +6 -0
- package/corpus/templates/mail/changelog/2026-07-06-the-mail-header-no-longer-shows-the-global-notifications-bel.md +6 -0
- package/corpus/templates/mail/package.json +1 -0
- package/corpus/templates/mail/server/db/schema.ts +11 -0
- package/corpus/templates/mail/server/handlers/emails.ts +20 -1
- package/corpus/templates/mail/server/lib/automation-engine.ts +53 -25
- package/corpus/templates/mail/server/lib/google-auth.ts +140 -9
- package/corpus/templates/mail/server/lib/jobs.ts +22 -6
- package/corpus/templates/mail/server/lib/outgoing-email.ts +11 -2
- package/corpus/templates/mail/server/lib/queued-drafts.ts +145 -4
- package/corpus/templates/mail/server/plugins/db.ts +29 -0
- package/corpus/templates/plan/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/plan/app/components/editor/PlanDocumentEditor.tsx +42 -3
- package/corpus/templates/plan/app/components/layout/Layout.tsx +5 -4
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +20 -20
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +3 -2
- package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +2 -2
- package/corpus/templates/plan/app/components/plan/GuestModeBanner.tsx +2 -1
- package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +8 -8
- package/corpus/templates/plan/app/components/plan/PlanContentRenderer.tsx +6 -1
- package/corpus/templates/plan/app/components/plan/PlanImageViewer.tsx +8 -12
- package/corpus/templates/plan/app/components/plan/PlanMarkdownEditor.tsx +6 -1
- package/corpus/templates/plan/app/components/plan/PlanVisualSurface.tsx +3 -3
- package/corpus/templates/plan/app/components/plan/planBlocks.tsx +4 -4
- package/corpus/templates/plan/app/components/plan/wireframe/kit/rough.tsx +61 -27
- package/corpus/templates/plan/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/avatar.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/button.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/card.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/collapsible.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/input.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/label.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/scroll-area.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/select.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/separator.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/sonner.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/spinner.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/toggle-group.tsx +1 -0
- package/corpus/templates/plan/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/plan/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/plan/app/global.css +13 -0
- package/corpus/templates/plan/app/hooks/use-plan-presence.ts +11 -1
- package/corpus/templates/plan/app/lib/plan-access-prompt.ts +74 -0
- package/corpus/templates/plan/app/lib/plan-annotation-runtime.ts +1435 -0
- package/corpus/templates/plan/app/lib/plan-comment-editor-helpers.ts +142 -0
- package/corpus/templates/plan/app/lib/plan-local-bridge.ts +376 -0
- package/corpus/templates/plan/app/lib/plan-native-anchors.ts +682 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +216 -2747
- package/corpus/templates/plan/app/root.tsx +13 -10
- package/corpus/templates/plan/app/routes/_index.tsx +1 -2
- package/corpus/templates/plan/app/routes/local-plans.$slug.tsx +1 -1
- package/corpus/templates/plan/app/routes/plans.$id.tsx +1 -2
- package/corpus/templates/plan/app/routes/plans.tsx +1 -2
- package/corpus/templates/plan/app/routes/recaps.$id.tsx +1 -2
- package/corpus/templates/plan/app/routes/recaps.tsx +1 -2
- package/corpus/templates/plan/app/routes/settings.tsx +7 -21
- package/corpus/templates/plan/changelog/2026-07-05-plan-pages-open-at-the-top-unless-a-shared-link-targets-a-sp.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-06-fixed-collaborative-plan-editing-so-content-no-longer-duplic.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-06-fixed-visual-reports-using-excessive-cpu-while-rendering-ske.md +6 -0
- package/corpus/templates/plan/server/db/index.ts +12 -0
- package/corpus/templates/plan/server/plans.ts +25 -1
- package/corpus/templates/plan/server/routes/[...page].get.ts +107 -1
- package/corpus/templates/plan/server/routes/api/plan-agent-context.json.get.ts +65 -0
- package/corpus/templates/plan/shared/agent-readable.ts +2 -0
- package/corpus/templates/slides/.agents/skills/actions/SKILL.md +55 -0
- package/corpus/templates/slides/app/components/ThemeToggle.tsx +6 -6
- package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +5 -5
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +7 -7
- package/corpus/templates/slides/app/components/deck/ExcalidrawSlide.tsx +2 -1
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +3 -2
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +15 -14
- package/corpus/templates/slides/app/components/editor/AnimationsPanel.tsx +7 -7
- package/corpus/templates/slides/app/components/editor/BlockBubbleMenu.tsx +5 -5
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +6 -6
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +14 -14
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +10 -9
- package/corpus/templates/slides/app/components/editor/GenerateSlidesDialog.tsx +6 -5
- package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +12 -12
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +4 -3
- package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +5 -4
- package/corpus/templates/slides/app/components/editor/ImportButton.tsx +4 -3
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +2 -1
- package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +10 -10
- package/corpus/templates/slides/app/components/editor/SlideBubbleMenu.tsx +10 -10
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +6 -6
- package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +2 -1
- package/corpus/templates/slides/app/components/editor/TweaksPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/layout/Header.tsx +2 -2
- package/corpus/templates/slides/app/components/layout/Sidebar.tsx +5 -5
- package/corpus/templates/slides/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/button.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/card.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/input.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/label.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/scroll-area.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/select.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/separator.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/spinner.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/toast.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/toaster.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/toggle-group.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/toolkit-provider.tsx +12 -0
- package/corpus/templates/slides/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/slides/app/components/visual-editor/CanvasCommentPins.tsx +6 -6
- package/corpus/templates/slides/app/components/visual-editor/DrawOverlay.tsx +7 -7
- package/corpus/templates/slides/app/global.css +1 -0
- package/corpus/templates/slides/app/hooks/use-toast.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +3 -3
- package/corpus/templates/slides/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +14 -17
- package/corpus/templates/slides/app/root.tsx +13 -10
- package/corpus/templates/slides/app/routes/p.$id.tsx +126 -12
- package/corpus/templates/slides/app/routes/settings.tsx +6 -21
- package/corpus/templates/slides/app/routes/share.$token.tsx +55 -8
- package/corpus/templates/slides/changelog/2026-07-06-mobile-layouts-no-longer-stack-two-top-headers.md +5 -0
- package/corpus/templates/slides/server/db/index.ts +9 -0
- package/corpus/templates/slides/server/handlers/uploads.ts +7 -2
- package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
- package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +97 -0
- package/corpus/templates/slides/shared/agent-readable.ts +2 -0
- package/dist/agent/durable-background.d.ts +29 -0
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +46 -0
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.js +7 -2
- package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
- package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
- package/dist/agent/engine/anthropic-engine.js +28 -2
- package/dist/agent/engine/anthropic-engine.js.map +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +1 -1
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/agent/engine/output-tokens.d.ts +3 -3
- package/dist/agent/engine/output-tokens.d.ts.map +1 -1
- package/dist/agent/engine/output-tokens.js +16 -14
- package/dist/agent/engine/output-tokens.js.map +1 -1
- package/dist/agent/engine/registry.d.ts +3 -3
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +28 -25
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/agent/engine/translate-ai-sdk.d.ts +14 -2
- package/dist/agent/engine/translate-ai-sdk.d.ts.map +1 -1
- package/dist/agent/engine/translate-ai-sdk.js +33 -18
- package/dist/agent/engine/translate-ai-sdk.js.map +1 -1
- package/dist/agent/engine/translate-anthropic.d.ts.map +1 -1
- package/dist/agent/engine/translate-anthropic.js +42 -1
- package/dist/agent/engine/translate-anthropic.js.map +1 -1
- package/dist/agent/engine/types.d.ts +15 -0
- package/dist/agent/engine/types.d.ts.map +1 -1
- package/dist/agent/engine/types.js.map +1 -1
- package/dist/agent/model-config.d.ts +8 -0
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +86 -0
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/production-agent.d.ts +91 -5
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +389 -180
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/runtime-context.d.ts +29 -0
- package/dist/agent/runtime-context.d.ts.map +1 -1
- package/dist/agent/runtime-context.js +40 -11
- package/dist/agent/runtime-context.js.map +1 -1
- package/dist/agent/tool-result-images.d.ts +49 -0
- package/dist/agent/tool-result-images.d.ts.map +1 -0
- package/dist/agent/tool-result-images.js +141 -0
- package/dist/agent/tool-result-images.js.map +1 -0
- package/dist/brand-kit/fig/index.d.ts.map +1 -1
- package/dist/brand-kit/fig/index.js +6 -3
- package/dist/brand-kit/fig/index.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +66 -17
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +72 -40
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/KeepTabOpenNotice.d.ts +55 -0
- package/dist/client/KeepTabOpenNotice.d.ts.map +1 -0
- package/dist/client/KeepTabOpenNotice.js +85 -0
- package/dist/client/KeepTabOpenNotice.js.map +1 -0
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +2 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts +7 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +7 -2
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/chat/run-recovery.d.ts.map +1 -1
- package/dist/client/chat/run-recovery.js +7 -1
- package/dist/client/chat/run-recovery.js.map +1 -1
- package/dist/client/clipboard.d.ts.map +1 -1
- package/dist/client/clipboard.js +5 -6
- package/dist/client/clipboard.js.map +1 -1
- package/dist/client/components/AgentPresenceChip.d.ts +1 -11
- package/dist/client/components/AgentPresenceChip.d.ts.map +1 -1
- package/dist/client/components/AgentPresenceChip.js +1 -41
- package/dist/client/components/AgentPresenceChip.js.map +1 -1
- package/dist/client/components/LiveCursorOverlay.d.ts +1 -44
- package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
- package/dist/client/components/LiveCursorOverlay.js +1 -123
- package/dist/client/components/LiveCursorOverlay.js.map +1 -1
- package/dist/client/components/PresenceBar.d.ts +1 -26
- package/dist/client/components/PresenceBar.d.ts.map +1 -1
- package/dist/client/components/PresenceBar.js +1 -164
- package/dist/client/components/PresenceBar.js.map +1 -1
- package/dist/client/components/RecentEditHighlights.d.ts +1 -42
- package/dist/client/components/RecentEditHighlights.d.ts.map +1 -1
- package/dist/client/components/RecentEditHighlights.js +1 -145
- package/dist/client/components/RecentEditHighlights.js.map +1 -1
- package/dist/client/components/RemoteSelectionRings.d.ts +1 -51
- package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -1
- package/dist/client/components/RemoteSelectionRings.js +1 -129
- package/dist/client/components/RemoteSelectionRings.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +4 -0
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +74 -26
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/db-admin/SqlEditor.d.ts.map +1 -1
- package/dist/client/db-admin/SqlEditor.js +13 -3
- package/dist/client/db-admin/SqlEditor.js.map +1 -1
- package/dist/client/dynamic-suggestions.d.ts.map +1 -1
- package/dist/client/dynamic-suggestions.js +7 -1
- package/dist/client/dynamic-suggestions.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +7 -7
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionsListPage.js +5 -7
- package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js +1 -1
- package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
- package/dist/client/host-bridge.d.ts +2 -0
- package/dist/client/host-bridge.d.ts.map +1 -1
- package/dist/client/host-bridge.js +26 -10
- package/dist/client/host-bridge.js.map +1 -1
- package/dist/client/i18n.d.ts +1 -0
- package/dist/client/i18n.d.ts.map +1 -1
- package/dist/client/i18n.js +3 -1
- package/dist/client/i18n.js.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +12 -31
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +3 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.js +2 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts +3 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.js +6 -2
- package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +103 -9
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
- package/dist/client/settings/SettingsTabsPage.js +5 -5
- package/dist/client/settings/SettingsTabsPage.js.map +1 -1
- package/dist/client/settings/index.d.ts +1 -1
- package/dist/client/settings/index.d.ts.map +1 -1
- package/dist/client/settings/index.js +1 -1
- package/dist/client/settings/index.js.map +1 -1
- package/dist/client/sharing/VisibilityBadge.d.ts +1 -10
- package/dist/client/sharing/VisibilityBadge.d.ts.map +1 -1
- package/dist/client/sharing/VisibilityBadge.js +1 -18
- package/dist/client/sharing/VisibilityBadge.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +8 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +3 -2
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/coding-tools/run-code.d.ts +36 -0
- package/dist/coding-tools/run-code.d.ts.map +1 -1
- package/dist/coding-tools/run-code.js +333 -62
- package/dist/coding-tools/run-code.js.map +1 -1
- package/dist/coding-tools/sandbox/background.d.ts +141 -0
- package/dist/coding-tools/sandbox/background.d.ts.map +1 -0
- package/dist/coding-tools/sandbox/background.js +279 -0
- package/dist/coding-tools/sandbox/background.js.map +1 -0
- package/dist/coding-tools/sandbox/executions-store.d.ts +138 -0
- package/dist/coding-tools/sandbox/executions-store.d.ts.map +1 -0
- package/dist/coding-tools/sandbox/executions-store.js +380 -0
- package/dist/coding-tools/sandbox/executions-store.js.map +1 -0
- package/dist/coding-tools/sandbox/index.d.ts +22 -13
- package/dist/coding-tools/sandbox/index.d.ts.map +1 -1
- package/dist/coding-tools/sandbox/index.js +31 -13
- package/dist/coding-tools/sandbox/index.js.map +1 -1
- package/dist/coding-tools/sandbox/local-child-process-adapter.d.ts.map +1 -1
- package/dist/coding-tools/sandbox/local-child-process-adapter.js +61 -4
- package/dist/coding-tools/sandbox/local-child-process-adapter.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/client.d.ts +16 -12
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +744 -496
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/presence.d.ts +2 -20
- package/dist/collab/presence.d.ts.map +1 -1
- package/dist/collab/presence.js +3 -0
- package/dist/collab/presence.js.map +1 -1
- package/dist/collab/recent-edits.d.ts +2 -37
- package/dist/collab/recent-edits.d.ts.map +1 -1
- package/dist/collab/recent-edits.js +2 -9
- package/dist/collab/recent-edits.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/data-programs/actions.d.ts +19 -0
- package/dist/data-programs/actions.d.ts.map +1 -0
- package/dist/data-programs/actions.js +279 -0
- package/dist/data-programs/actions.js.map +1 -0
- package/dist/data-programs/contract.d.ts +70 -0
- package/dist/data-programs/contract.d.ts.map +1 -0
- package/dist/data-programs/contract.js +225 -0
- package/dist/data-programs/contract.js.map +1 -0
- package/dist/data-programs/execute.d.ts +96 -0
- package/dist/data-programs/execute.d.ts.map +1 -0
- package/dist/data-programs/execute.js +454 -0
- package/dist/data-programs/execute.js.map +1 -0
- package/dist/data-programs/index.d.ts +11 -0
- package/dist/data-programs/index.d.ts.map +1 -0
- package/dist/data-programs/index.js +11 -0
- package/dist/data-programs/index.js.map +1 -0
- package/dist/data-programs/schema.d.ts +517 -0
- package/dist/data-programs/schema.d.ts.map +1 -0
- package/dist/data-programs/schema.js +138 -0
- package/dist/data-programs/schema.js.map +1 -0
- package/dist/data-programs/store.d.ts +158 -0
- package/dist/data-programs/store.d.ts.map +1 -0
- package/dist/data-programs/store.js +493 -0
- package/dist/data-programs/store.js.map +1 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +34 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/extensions/actions.js +2 -2
- package/dist/extensions/actions.js.map +1 -1
- package/dist/file-upload/builder.d.ts.map +1 -1
- package/dist/file-upload/builder.js +9 -0
- package/dist/file-upload/builder.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +21 -10
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/local-artifacts/index.js +1 -1
- package/dist/local-artifacts/index.js.map +1 -1
- package/dist/localization/default-messages.d.ts +0 -1
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +1 -2
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp-client/index.d.ts +9 -0
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +23 -0
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/onboarding/default-steps.d.ts.map +1 -1
- package/dist/onboarding/default-steps.js +2 -2
- package/dist/onboarding/default-steps.js.map +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +35 -3
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.js +4 -8
- package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
- package/dist/scripts/agent-engines/test-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/test-agent-engine.js +7 -2
- package/dist/scripts/agent-engines/test-agent-engine.js.map +1 -1
- package/dist/scripts/db/tool-mode.js +3 -3
- package/dist/scripts/db/tool-mode.js.map +1 -1
- package/dist/scripts/resources/migrate-learnings.d.ts +5 -1
- package/dist/scripts/resources/migrate-learnings.d.ts.map +1 -1
- package/dist/scripts/resources/migrate-learnings.js +8 -10
- package/dist/scripts/resources/migrate-learnings.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +4 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +8 -2
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +20 -6
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +138 -59
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-discovery.d.ts.map +1 -1
- package/dist/server/agent-discovery.js +62 -8
- package/dist/server/agent-discovery.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +75 -3
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-design-systems.js +2 -2
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +100 -20
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/credential-provider.d.ts +6 -3
- package/dist/server/credential-provider.d.ts.map +1 -1
- package/dist/server/credential-provider.js +25 -8
- package/dist/server/credential-provider.js.map +1 -1
- package/dist/server/csrf.d.ts.map +1 -1
- package/dist/server/csrf.js +3 -0
- package/dist/server/csrf.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/oauth-helpers.d.ts.map +1 -1
- package/dist/server/oauth-helpers.js +9 -1
- package/dist/server/oauth-helpers.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +80 -48
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/server/prompts/shared-rules.js +2 -2
- package/dist/server/prompts/shared-rules.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts.map +1 -1
- package/dist/server/transcribe-voice.js +77 -15
- package/dist/server/transcribe-voice.js.map +1 -1
- package/dist/shared/agent-readable-resource.d.ts +29 -0
- package/dist/shared/agent-readable-resource.d.ts.map +1 -0
- package/dist/shared/agent-readable-resource.js +60 -0
- package/dist/shared/agent-readable-resource.js.map +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +1 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/sharing/actions/create-agent-resource-link.d.ts +13 -0
- package/dist/sharing/actions/create-agent-resource-link.d.ts.map +1 -0
- package/dist/sharing/actions/create-agent-resource-link.js +72 -0
- package/dist/sharing/actions/create-agent-resource-link.js.map +1 -0
- package/dist/sharing/registry.d.ts +17 -0
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/dist/styles/agent-native.css +57 -45
- package/dist/templates/default/.agents/skills/actions/SKILL.md +55 -0
- package/dist/templates/default/AGENTS.md +11 -14
- package/dist/templates/default/actions/navigate.ts +1 -1
- package/dist/templates/default/app/i18n/ar-SA.ts +0 -1
- package/dist/templates/default/app/i18n/de-DE.ts +0 -1
- package/dist/templates/default/app/i18n/en-US.ts +0 -1
- package/dist/templates/default/app/i18n/es-ES.ts +0 -1
- package/dist/templates/default/app/i18n/fr-FR.ts +0 -1
- package/dist/templates/default/app/i18n/hi-IN.ts +0 -1
- package/dist/templates/default/app/i18n/ja-JP.ts +0 -1
- package/dist/templates/default/app/i18n/ko-KR.ts +0 -1
- package/dist/templates/default/app/i18n/pt-BR.ts +0 -1
- package/dist/templates/default/app/i18n/zh-CN.ts +0 -1
- package/dist/templates/default/app/i18n/zh-TW.ts +0 -1
- package/dist/templates/default/app/routes/settings.tsx +22 -33
- package/dist/templates/headless/.agents/skills/agent-native-docs/SKILL.md +20 -7
- package/dist/templates/headless/AGENTS.md +9 -6
- package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +55 -0
- package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +92 -0
- package/dist/templates/workspace-core/.agents/skills/data-programs/SKILL.md +128 -0
- package/dist/templates/workspace-core/.agents/skills/harness-agents/SKILL.md +10 -4
- package/dist/vite/agents-bundle-plugin.d.ts.map +1 -1
- package/dist/vite/agents-bundle-plugin.js +26 -1
- package/dist/vite/agents-bundle-plugin.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +12 -0
- package/dist/vite/client.js.map +1 -1
- package/docs/content/agent-native-toolkit.mdx +103 -0
- package/docs/content/cloneable-saas.mdx +19 -19
- package/docs/content/components.mdx +16 -4
- package/docs/content/data-programs.mdx +329 -0
- package/docs/content/deployment.mdx +9 -9
- package/docs/content/extensions.mdx +8 -7
- package/docs/content/generative-ui.mdx +5 -3
- package/docs/content/key-concepts.mdx +6 -9
- package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +103 -0
- package/docs/content/locales/ar-SA/cloneable-saas.mdx +19 -19
- package/docs/content/locales/ar-SA/components.mdx +12 -2
- package/docs/content/locales/ar-SA/data-programs.mdx +329 -0
- package/docs/content/locales/ar-SA/deployment.mdx +8 -9
- package/docs/content/locales/ar-SA/extensions.mdx +6 -4
- package/docs/content/locales/ar-SA/key-concepts.mdx +7 -9
- package/docs/content/locales/ar-SA/real-time-collaboration.mdx +9 -10
- package/docs/content/locales/ar-SA/sharing.mdx +15 -7
- package/docs/content/locales/de-DE/agent-native-toolkit.mdx +103 -0
- package/docs/content/locales/de-DE/cloneable-saas.mdx +19 -19
- package/docs/content/locales/de-DE/components.mdx +12 -2
- package/docs/content/locales/de-DE/data-programs.mdx +329 -0
- package/docs/content/locales/de-DE/deployment.mdx +8 -9
- package/docs/content/locales/de-DE/extensions.mdx +6 -4
- package/docs/content/locales/de-DE/key-concepts.mdx +8 -10
- package/docs/content/locales/de-DE/real-time-collaboration.mdx +9 -10
- package/docs/content/locales/de-DE/sharing.mdx +15 -7
- package/docs/content/locales/es-ES/agent-native-toolkit.mdx +103 -0
- package/docs/content/locales/es-ES/cloneable-saas.mdx +19 -19
- package/docs/content/locales/es-ES/components.mdx +12 -2
- package/docs/content/locales/es-ES/data-programs.mdx +329 -0
- package/docs/content/locales/es-ES/deployment.mdx +8 -9
- package/docs/content/locales/es-ES/extensions.mdx +6 -4
- package/docs/content/locales/es-ES/key-concepts.mdx +8 -10
- package/docs/content/locales/es-ES/real-time-collaboration.mdx +9 -10
- package/docs/content/locales/es-ES/sharing.mdx +15 -7
- package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +103 -0
- package/docs/content/locales/fr-FR/cloneable-saas.mdx +19 -19
- package/docs/content/locales/fr-FR/components.mdx +12 -2
- package/docs/content/locales/fr-FR/data-programs.mdx +329 -0
- package/docs/content/locales/fr-FR/deployment.mdx +8 -9
- package/docs/content/locales/fr-FR/extensions.mdx +6 -4
- package/docs/content/locales/fr-FR/key-concepts.mdx +8 -10
- package/docs/content/locales/fr-FR/real-time-collaboration.mdx +9 -10
- package/docs/content/locales/fr-FR/sharing.mdx +15 -7
- package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +103 -0
- package/docs/content/locales/hi-IN/cloneable-saas.mdx +19 -19
- package/docs/content/locales/hi-IN/components.mdx +20 -10
- package/docs/content/locales/hi-IN/data-programs.mdx +329 -0
- package/docs/content/locales/hi-IN/deployment.mdx +8 -9
- package/docs/content/locales/hi-IN/extensions.mdx +6 -4
- package/docs/content/locales/hi-IN/key-concepts.mdx +8 -10
- package/docs/content/locales/hi-IN/real-time-collaboration.mdx +9 -10
- package/docs/content/locales/hi-IN/sharing.mdx +15 -7
- package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +103 -0
- package/docs/content/locales/ja-JP/cloneable-saas.mdx +19 -19
- package/docs/content/locales/ja-JP/components.mdx +12 -2
- package/docs/content/locales/ja-JP/data-programs.mdx +329 -0
- package/docs/content/locales/ja-JP/deployment.mdx +8 -9
- package/docs/content/locales/ja-JP/extensions.mdx +6 -4
- package/docs/content/locales/ja-JP/key-concepts.mdx +9 -11
- package/docs/content/locales/ja-JP/real-time-collaboration.mdx +9 -10
- package/docs/content/locales/ja-JP/sharing.mdx +15 -7
- package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +103 -0
- package/docs/content/locales/ko-KR/cloneable-saas.mdx +19 -19
- package/docs/content/locales/ko-KR/components.mdx +20 -10
- package/docs/content/locales/ko-KR/data-programs.mdx +329 -0
- package/docs/content/locales/ko-KR/deployment.mdx +8 -9
- package/docs/content/locales/ko-KR/extensions.mdx +16 -14
- package/docs/content/locales/ko-KR/key-concepts.mdx +9 -11
- package/docs/content/locales/ko-KR/real-time-collaboration.mdx +9 -10
- package/docs/content/locales/ko-KR/sharing.mdx +15 -7
- package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +103 -0
- package/docs/content/locales/pt-BR/cloneable-saas.mdx +19 -19
- package/docs/content/locales/pt-BR/components.mdx +12 -2
- package/docs/content/locales/pt-BR/data-programs.mdx +329 -0
- package/docs/content/locales/pt-BR/deployment.mdx +8 -9
- package/docs/content/locales/pt-BR/extensions.mdx +6 -4
- package/docs/content/locales/pt-BR/key-concepts.mdx +8 -11
- package/docs/content/locales/pt-BR/real-time-collaboration.mdx +9 -10
- package/docs/content/locales/pt-BR/sharing.mdx +15 -7
- package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +103 -0
- package/docs/content/locales/zh-CN/cloneable-saas.mdx +19 -19
- package/docs/content/locales/zh-CN/components.mdx +20 -10
- package/docs/content/locales/zh-CN/data-programs.mdx +329 -0
- package/docs/content/locales/zh-CN/deployment.mdx +7 -9
- package/docs/content/locales/zh-CN/extensions.mdx +16 -14
- package/docs/content/locales/zh-CN/key-concepts.mdx +7 -11
- package/docs/content/locales/zh-CN/real-time-collaboration.mdx +10 -11
- package/docs/content/locales/zh-CN/sharing.mdx +15 -7
- package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +103 -0
- package/docs/content/locales/zh-TW/cloneable-saas.mdx +19 -19
- package/docs/content/locales/zh-TW/components.mdx +20 -10
- package/docs/content/locales/zh-TW/data-programs.mdx +329 -0
- package/docs/content/locales/zh-TW/deployment.mdx +7 -9
- package/docs/content/locales/zh-TW/extensions.mdx +16 -14
- package/docs/content/locales/zh-TW/key-concepts.mdx +7 -11
- package/docs/content/locales/zh-TW/real-time-collaboration.mdx +10 -11
- package/docs/content/locales/zh-TW/sharing.mdx +15 -7
- package/docs/content/real-time-collaboration.mdx +10 -11
- package/docs/content/sharing.mdx +39 -7
- package/docs/content/toolkit-app-adapters.mdx +46 -0
- package/docs/content/toolkit-collaboration-ui.mdx +48 -0
- package/docs/content/toolkit-sharing-ui.mdx +33 -0
- package/docs/content/toolkit-shell-hooks.mdx +27 -0
- package/docs/content/toolkit-ui.mdx +69 -0
- package/package.json +7 -2
- package/src/templates/default/.agents/skills/actions/SKILL.md +55 -0
- package/src/templates/default/AGENTS.md +11 -14
- package/src/templates/default/actions/navigate.spec.ts +53 -0
- package/src/templates/default/actions/navigate.ts +1 -1
- package/src/templates/default/app/i18n/ar-SA.ts +0 -1
- package/src/templates/default/app/i18n/de-DE.ts +0 -1
- package/src/templates/default/app/i18n/en-US.ts +0 -1
- package/src/templates/default/app/i18n/es-ES.ts +0 -1
- package/src/templates/default/app/i18n/fr-FR.ts +0 -1
- package/src/templates/default/app/i18n/hi-IN.ts +0 -1
- package/src/templates/default/app/i18n/ja-JP.ts +0 -1
- package/src/templates/default/app/i18n/ko-KR.ts +0 -1
- package/src/templates/default/app/i18n/pt-BR.ts +0 -1
- package/src/templates/default/app/i18n/zh-CN.ts +0 -1
- package/src/templates/default/app/i18n/zh-TW.ts +0 -1
- package/src/templates/default/app/routes/settings.tsx +22 -33
- package/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +20 -7
- package/src/templates/headless/AGENTS.md +9 -6
- package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +55 -0
- package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +92 -0
- package/src/templates/workspace-core/.agents/skills/data-programs/SKILL.md +128 -0
- package/src/templates/workspace-core/.agents/skills/harness-agents/SKILL.md +10 -4
- package/corpus/templates/design/server/lib/figma-import/clipboard.ts +0 -122
- package/corpus/templates/design/server/lib/figma-import/decode.ts +0 -455
- package/corpus/templates/design/server/lib/figma-import/processor.ts +0 -116
- package/corpus/templates/design/server/lib/figma-import/render-html.ts +0 -397
- package/corpus/templates/design/server/lib/figma-import/types.ts +0 -60
|
@@ -4,7 +4,7 @@ use std::fs::File;
|
|
|
4
4
|
use std::io::{ErrorKind, Read};
|
|
5
5
|
use std::path::{Path, PathBuf};
|
|
6
6
|
use std::process::{Child, Command, Stdio};
|
|
7
|
-
use std::sync::{Arc, Condvar, Mutex};
|
|
7
|
+
use std::sync::{Arc, Condvar, Mutex, OnceLock};
|
|
8
8
|
use std::time::{Duration, Instant};
|
|
9
9
|
use tauri::{AppHandle, Emitter, Manager, State};
|
|
10
10
|
|
|
@@ -26,7 +26,7 @@ use screencapturekit::stream::{
|
|
|
26
26
|
configuration::SCStreamConfiguration, content_filter::SCContentFilter,
|
|
27
27
|
output_type::SCStreamOutputType, sc_stream::SCStream,
|
|
28
28
|
};
|
|
29
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
29
|
+
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
30
30
|
|
|
31
31
|
pub(crate) const QUICKTIME_RECORDING_MIME_TYPE: &str = "video/quicktime";
|
|
32
32
|
pub(crate) const MP4_RECORDING_MIME_TYPE: &str = "video/mp4";
|
|
@@ -45,6 +45,8 @@ const TRANSCODE_RATE_LIMIT_OVERHEAD_KBPS: f64 = 64.0;
|
|
|
45
45
|
const TRANSCODE_FRAME_RATE_LIMIT: u32 = 30;
|
|
46
46
|
const NORMALIZED_AUDIO_BITRATE_KBPS: u32 = 160;
|
|
47
47
|
const AUDIO_LOUDNESS_FILTER: &str = "loudnorm=I=-16:TP=-1.5:LRA=11";
|
|
48
|
+
const AUDIO_SIGNAL_MIN_MEAN_VOLUME_DB: f64 = -60.0;
|
|
49
|
+
const AUDIO_SIGNAL_MIN_MAX_VOLUME_DB: f64 = -30.0;
|
|
48
50
|
// When the mic is captured alongside system audio, ScreenCaptureKit lays the
|
|
49
51
|
// two sources out as the left/right channels of a single stereo track, which
|
|
50
52
|
// plays back stuck on one speaker. Force the input to stereo (mono sources
|
|
@@ -54,25 +56,36 @@ const AUDIO_LOUDNESS_FILTER: &str = "loudnorm=I=-16:TP=-1.5:LRA=11";
|
|
|
54
56
|
// stereo content that must not be flattened.
|
|
55
57
|
const AUDIO_DOWNMIX_FILTER: &str =
|
|
56
58
|
"aformat=channel_layouts=stereo,pan=stereo|FL=0.5*FL+0.5*FR|FR=0.5*FL+0.5*FR";
|
|
59
|
+
// Mild FFT denoise for native mic captures. Browser recordings already request
|
|
60
|
+
// WebRTC noise suppression; ScreenCaptureKit/screencapture mic capture does
|
|
61
|
+
// not, so reduce steady broadband room/mic hiss during the existing optimize
|
|
62
|
+
// step. Keep this conservative to avoid watery speech artifacts.
|
|
63
|
+
const AUDIO_DENOISE_FILTER: &str = "afftdn=nr=10:nf=-50:tn=1";
|
|
57
64
|
// loudnorm operates internally at 192 kHz and emits at 192 kHz; without an
|
|
58
65
|
// explicit output rate the AAC track ends up at 192 kHz and plays back slow.
|
|
59
66
|
const AUDIO_OUTPUT_SAMPLE_RATE: u32 = 48000;
|
|
60
67
|
|
|
61
68
|
// Loudness normalization, optionally preceded by the centered-stereo downmix
|
|
62
|
-
// that repairs the mic+system L/R split
|
|
63
|
-
// so loudnorm's 192 kHz output is
|
|
64
|
-
|
|
69
|
+
// that repairs the mic+system L/R split and denoise for native mic captures.
|
|
70
|
+
// Pair with `-ar AUDIO_OUTPUT_SAMPLE_RATE` so loudnorm's 192 kHz output is
|
|
71
|
+
// resampled back.
|
|
72
|
+
fn audio_filter_chain(downmix: bool, denoise: bool) -> String {
|
|
73
|
+
let mut filters = Vec::new();
|
|
65
74
|
if downmix {
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
75
|
+
filters.push(AUDIO_DOWNMIX_FILTER);
|
|
76
|
+
}
|
|
77
|
+
if denoise {
|
|
78
|
+
filters.push(AUDIO_DENOISE_FILTER);
|
|
69
79
|
}
|
|
80
|
+
filters.push(AUDIO_LOUDNESS_FILTER);
|
|
81
|
+
filters.join(",")
|
|
70
82
|
}
|
|
71
83
|
const NATIVE_CAPTURE_MAX_LONG_EDGE: u32 = 1280;
|
|
72
84
|
const NATIVE_CAPTURE_FPS: u32 = 24;
|
|
73
85
|
const AVCONVERT_PATH: &str = "/usr/bin/avconvert";
|
|
74
86
|
const AVCONVERT_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
|
75
87
|
const FFMPEG_TIMEOUT: Duration = Duration::from_secs(8 * 60);
|
|
88
|
+
const FFMPEG_AUDIO_PROBE_TIMEOUT: Duration = Duration::from_secs(90);
|
|
76
89
|
const FFMPEG_CANDIDATE_PATHS: &[&str] = &[
|
|
77
90
|
"ffmpeg",
|
|
78
91
|
"/opt/homebrew/bin/ffmpeg",
|
|
@@ -95,6 +108,28 @@ const DISK_MONITOR_CRITICAL_BYTES: u64 = 250 * 1024 * 1024;
|
|
|
95
108
|
// How often the background monitor checks free space.
|
|
96
109
|
const DISK_MONITOR_INTERVAL_SECS: u64 = 30;
|
|
97
110
|
|
|
111
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
112
|
+
enum NativeUploadMode {
|
|
113
|
+
Buffered,
|
|
114
|
+
Streaming,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
impl NativeUploadMode {
|
|
118
|
+
fn from_option(value: Option<String>) -> Self {
|
|
119
|
+
match value.as_deref() {
|
|
120
|
+
Some("streaming") => Self::Streaming,
|
|
121
|
+
_ => Self::Buffered,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fn label(self) -> &'static str {
|
|
126
|
+
match self {
|
|
127
|
+
Self::Buffered => "buffered",
|
|
128
|
+
Self::Streaming => "streaming",
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
98
133
|
#[derive(Default)]
|
|
99
134
|
pub struct NativeFullscreenRecordingState {
|
|
100
135
|
inner: Mutex<Option<NativeFullscreenSession>>,
|
|
@@ -187,9 +222,31 @@ pub(crate) enum NativeFullscreenBackend {
|
|
|
187
222
|
/// silent second while ScreenCaptureKit's mic pipeline spins up.
|
|
188
223
|
/// `None` when the recording has no microphone input.
|
|
189
224
|
mic_ready: Option<Arc<AtomicBool>>,
|
|
225
|
+
/// Number of microphone sample buffers observed before/after attach.
|
|
226
|
+
/// This is diagnostic only; it lets the tray log distinguish "SCK
|
|
227
|
+
/// never saw mic samples" from "samples existed but encoded silent".
|
|
228
|
+
mic_sample_count: Option<Arc<AtomicU64>>,
|
|
190
229
|
},
|
|
191
230
|
}
|
|
192
231
|
|
|
232
|
+
/// Safety net for the `screencapture` fallback: if a session carrying a live
|
|
233
|
+
/// `screencapture` child is ever dropped without going through
|
|
234
|
+
/// `stop_native_recording`/`stop_screencapture` (app quit, crash unwind, or an
|
|
235
|
+
/// error path that discards the session), make sure the child process doesn't
|
|
236
|
+
/// keep recording and writing to disk after we've lost track of it. This is a
|
|
237
|
+
/// best-effort hard kill (no SIGINT grace period) since a `Drop` impl is not
|
|
238
|
+
/// the place to block on graceful finalization.
|
|
239
|
+
impl Drop for NativeFullscreenBackend {
|
|
240
|
+
fn drop(&mut self) {
|
|
241
|
+
if let NativeFullscreenBackend::Screencapture { child } = self {
|
|
242
|
+
if matches!(child.try_wait(), Ok(None)) {
|
|
243
|
+
let _ = child.kill();
|
|
244
|
+
let _ = child.wait();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
193
250
|
/// `SCRecordingOutput` finalizes the MP4 *asynchronously*: after
|
|
194
251
|
/// `remove_recording_output()` / `stop_capture()` it still has to flush its
|
|
195
252
|
/// last buffered sample fragment and write the `moov` atom, then it calls
|
|
@@ -258,11 +315,13 @@ impl SCRecordingOutputDelegate for FinishDelegate {
|
|
|
258
315
|
}
|
|
259
316
|
}
|
|
260
317
|
|
|
318
|
+
#[derive(Clone)]
|
|
261
319
|
struct PreparedRecordingFile {
|
|
262
320
|
path: PathBuf,
|
|
263
321
|
mime_type: String,
|
|
264
322
|
bytes: u64,
|
|
265
323
|
temporary: bool,
|
|
324
|
+
locally_transcoded: bool,
|
|
266
325
|
}
|
|
267
326
|
|
|
268
327
|
pub(crate) fn format_mb(bytes: u64) -> String {
|
|
@@ -370,12 +429,93 @@ fn emit_native_upload_progress(
|
|
|
370
429
|
);
|
|
371
430
|
}
|
|
372
431
|
|
|
432
|
+
fn clear_recording_active(app: &AppHandle) {
|
|
433
|
+
let mut changed = false;
|
|
434
|
+
if let Some(state) = app.try_state::<crate::state::RecordingActive>() {
|
|
435
|
+
if let Ok(mut active) = state.0.lock() {
|
|
436
|
+
if *active {
|
|
437
|
+
*active = false;
|
|
438
|
+
changed = true;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if changed {
|
|
443
|
+
crate::tray::rebuild_tray_menu(app);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
static LAST_NATIVE_UPLOAD_FINISHED: OnceLock<Mutex<Option<NativeUploadFinishedPayload>>> =
|
|
448
|
+
OnceLock::new();
|
|
449
|
+
static CLAIMED_NATIVE_UPLOAD_OPEN: OnceLock<Mutex<Option<String>>> = OnceLock::new();
|
|
450
|
+
|
|
451
|
+
fn last_native_upload_finished() -> &'static Mutex<Option<NativeUploadFinishedPayload>> {
|
|
452
|
+
LAST_NATIVE_UPLOAD_FINISHED.get_or_init(|| Mutex::new(None))
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
fn claimed_native_upload_open() -> &'static Mutex<Option<String>> {
|
|
456
|
+
CLAIMED_NATIVE_UPLOAD_OPEN.get_or_init(|| Mutex::new(None))
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
fn reset_native_upload_completion_state() {
|
|
460
|
+
if let Ok(mut last) = last_native_upload_finished().lock() {
|
|
461
|
+
*last = None;
|
|
462
|
+
}
|
|
463
|
+
if let Ok(mut claimed) = claimed_native_upload_open().lock() {
|
|
464
|
+
*claimed = None;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
#[tauri::command]
|
|
469
|
+
pub fn native_fullscreen_take_upload_finished() -> Option<NativeUploadFinishedPayload> {
|
|
470
|
+
last_native_upload_finished()
|
|
471
|
+
.lock()
|
|
472
|
+
.ok()
|
|
473
|
+
.and_then(|mut last| last.take())
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
#[tauri::command]
|
|
477
|
+
pub fn native_fullscreen_claim_upload_open(recording_id: String) -> bool {
|
|
478
|
+
let Ok(mut claimed) = claimed_native_upload_open().lock() else {
|
|
479
|
+
return true;
|
|
480
|
+
};
|
|
481
|
+
if claimed.as_deref() == Some(recording_id.as_str()) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
*claimed = Some(recording_id);
|
|
485
|
+
true
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
fn emit_native_upload_finished(
|
|
489
|
+
app: &AppHandle,
|
|
490
|
+
server_url: &str,
|
|
491
|
+
recording_id: &str,
|
|
492
|
+
ok: bool,
|
|
493
|
+
error: Option<String>,
|
|
494
|
+
local_file_path: Option<&Path>,
|
|
495
|
+
) {
|
|
496
|
+
clear_recording_active(app);
|
|
497
|
+
let base = server_url.trim_end_matches('/');
|
|
498
|
+
let payload = NativeUploadFinishedPayload {
|
|
499
|
+
recording_id: recording_id.to_string(),
|
|
500
|
+
ok,
|
|
501
|
+
view_url: format!("{base}/r/{recording_id}"),
|
|
502
|
+
error,
|
|
503
|
+
local_file_path: local_file_path.map(|path| path.to_string_lossy().to_string()),
|
|
504
|
+
};
|
|
505
|
+
if let Ok(mut last) = last_native_upload_finished().lock() {
|
|
506
|
+
*last = Some(payload.clone());
|
|
507
|
+
}
|
|
508
|
+
let _ = app.emit("clips:native-upload-finished", payload);
|
|
509
|
+
}
|
|
510
|
+
|
|
373
511
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
374
512
|
#[serde(rename_all = "camelCase")]
|
|
375
513
|
struct SavedNativeRecording {
|
|
376
514
|
recording_id: String,
|
|
377
515
|
server_url: String,
|
|
378
516
|
file_path: PathBuf,
|
|
517
|
+
#[serde(default)]
|
|
518
|
+
segment_paths: Vec<PathBuf>,
|
|
379
519
|
mime_type: String,
|
|
380
520
|
duration_ms: u128,
|
|
381
521
|
width: Option<u32>,
|
|
@@ -435,6 +575,16 @@ pub struct NativeFullscreenUploadResult {
|
|
|
435
575
|
bytes: u64,
|
|
436
576
|
}
|
|
437
577
|
|
|
578
|
+
#[derive(Serialize, Clone)]
|
|
579
|
+
#[serde(rename_all = "camelCase")]
|
|
580
|
+
pub struct NativeUploadFinishedPayload {
|
|
581
|
+
recording_id: String,
|
|
582
|
+
ok: bool,
|
|
583
|
+
view_url: String,
|
|
584
|
+
error: Option<String>,
|
|
585
|
+
local_file_path: Option<String>,
|
|
586
|
+
}
|
|
587
|
+
|
|
438
588
|
#[derive(Serialize)]
|
|
439
589
|
#[serde(rename_all = "camelCase")]
|
|
440
590
|
pub struct NativeLocalRecordingFile {
|
|
@@ -521,72 +671,53 @@ fn start_native_session_locked(
|
|
|
521
671
|
defer_recording_output: bool,
|
|
522
672
|
) -> Result<NativeFullscreenStartInfo, String> {
|
|
523
673
|
let safe_id = sanitize_recording_id(recording_id);
|
|
674
|
+
reset_native_upload_completion_state();
|
|
524
675
|
let has_specific_mic = mic_device_id
|
|
525
676
|
.as_deref()
|
|
526
677
|
.is_some_and(|v| !v.trim().is_empty())
|
|
527
678
|
|| mic_device_label
|
|
528
679
|
.as_deref()
|
|
529
680
|
.is_some_and(|v| !v.trim().is_empty());
|
|
530
|
-
let session =
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
681
|
+
let session = match start_screencapturekit_recording(
|
|
682
|
+
app,
|
|
683
|
+
&safe_id,
|
|
684
|
+
include_audio,
|
|
685
|
+
capture_system_audio,
|
|
686
|
+
mic_device_id.as_deref(),
|
|
687
|
+
mic_device_label.as_deref(),
|
|
688
|
+
capture_region,
|
|
689
|
+
defer_recording_output,
|
|
690
|
+
) {
|
|
691
|
+
Ok(session) => session,
|
|
692
|
+
Err(sck_err) => {
|
|
693
|
+
if defer_recording_output {
|
|
694
|
+
return Err(sck_err);
|
|
695
|
+
}
|
|
696
|
+
if include_audio {
|
|
697
|
+
let mic_description = if has_specific_mic {
|
|
698
|
+
"the selected microphone"
|
|
699
|
+
} else {
|
|
700
|
+
"the resolved default microphone"
|
|
701
|
+
};
|
|
702
|
+
return Err(format!(
|
|
703
|
+
"ScreenCaptureKit recording failed before it could use {mic_description} ({sck_err}). Clips did not fall back to macOS screencapture because that would ignore the requested input."
|
|
704
|
+
));
|
|
705
|
+
}
|
|
542
706
|
eprintln!(
|
|
543
|
-
"[clips-tray]
|
|
707
|
+
"[clips-tray] ScreenCaptureKit recording unavailable; falling back to screencapture: {sck_err}"
|
|
544
708
|
);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
app,
|
|
556
|
-
&safe_id,
|
|
557
|
-
include_audio,
|
|
558
|
-
capture_system_audio,
|
|
559
|
-
mic_device_id.as_deref(),
|
|
560
|
-
mic_device_label.as_deref(),
|
|
561
|
-
capture_region,
|
|
562
|
-
defer_recording_output,
|
|
563
|
-
) {
|
|
564
|
-
Ok(session) => session,
|
|
565
|
-
Err(sck_err) => {
|
|
566
|
-
if defer_recording_output {
|
|
567
|
-
return Err(sck_err);
|
|
568
|
-
}
|
|
569
|
-
if include_audio && !capture_system_audio && has_specific_mic {
|
|
570
|
-
return Err(format!(
|
|
571
|
-
"ScreenCaptureKit recording failed before it could use the selected microphone ({sck_err}). Clips did not fall back to macOS screencapture because that would ignore your selected input."
|
|
572
|
-
));
|
|
573
|
-
}
|
|
574
|
-
eprintln!(
|
|
575
|
-
"[clips-tray] ScreenCaptureKit recording unavailable; falling back to screencapture: {sck_err}"
|
|
576
|
-
);
|
|
577
|
-
start_screencapture_recording(
|
|
578
|
-
app,
|
|
579
|
-
&safe_id,
|
|
580
|
-
include_audio,
|
|
581
|
-
capture_system_audio,
|
|
582
|
-
capture_region,
|
|
709
|
+
start_screencapture_recording(
|
|
710
|
+
app,
|
|
711
|
+
&safe_id,
|
|
712
|
+
include_audio,
|
|
713
|
+
capture_system_audio,
|
|
714
|
+
capture_region,
|
|
715
|
+
)
|
|
716
|
+
.map_err(|fallback_err| {
|
|
717
|
+
format!(
|
|
718
|
+
"ScreenCaptureKit recording failed ({sck_err}); screencapture fallback failed ({fallback_err})"
|
|
583
719
|
)
|
|
584
|
-
|
|
585
|
-
format!(
|
|
586
|
-
"ScreenCaptureKit recording failed ({sck_err}); screencapture fallback failed ({fallback_err})"
|
|
587
|
-
)
|
|
588
|
-
})?
|
|
589
|
-
}
|
|
720
|
+
})?
|
|
590
721
|
}
|
|
591
722
|
};
|
|
592
723
|
let width = session.width;
|
|
@@ -726,21 +857,34 @@ pub async fn native_fullscreen_recording_begin(
|
|
|
726
857
|
);
|
|
727
858
|
}
|
|
728
859
|
|
|
729
|
-
// Grab the mic-ready
|
|
730
|
-
let
|
|
860
|
+
// Grab the mic-ready diagnostics without holding the lock during the wait.
|
|
861
|
+
let mic_state = {
|
|
731
862
|
let guard = state.inner.lock().map_err(|e| e.to_string())?;
|
|
732
863
|
guard.as_ref().and_then(|s| match s.backend.as_ref() {
|
|
733
|
-
Some(NativeFullscreenBackend::ScreenCaptureKit {
|
|
734
|
-
mic_ready
|
|
735
|
-
|
|
864
|
+
Some(NativeFullscreenBackend::ScreenCaptureKit {
|
|
865
|
+
mic_ready,
|
|
866
|
+
mic_sample_count,
|
|
867
|
+
..
|
|
868
|
+
}) => mic_ready
|
|
869
|
+
.as_ref()
|
|
870
|
+
.map(|ready| (Arc::clone(ready), mic_sample_count.clone())),
|
|
736
871
|
_ => None,
|
|
737
872
|
})
|
|
738
873
|
};
|
|
739
|
-
|
|
874
|
+
let mut mic_ready_before_attach: Option<bool> = None;
|
|
875
|
+
let mut mic_samples_before_attach: Option<u64> = None;
|
|
876
|
+
let mut mic_warm_wait_ms: Option<u128> = None;
|
|
877
|
+
if let Some((ready, sample_count)) = mic_state {
|
|
878
|
+
let wait_started = Instant::now();
|
|
740
879
|
let deadline = Instant::now() + Duration::from_millis(MIC_WARM_TIMEOUT_MS);
|
|
741
880
|
while !ready.load(Ordering::Relaxed) && Instant::now() < deadline {
|
|
742
|
-
|
|
881
|
+
tokio::time::sleep(Duration::from_millis(15)).await;
|
|
743
882
|
}
|
|
883
|
+
mic_ready_before_attach = Some(ready.load(Ordering::Relaxed));
|
|
884
|
+
mic_samples_before_attach = sample_count
|
|
885
|
+
.as_ref()
|
|
886
|
+
.map(|samples| samples.load(Ordering::Relaxed));
|
|
887
|
+
mic_warm_wait_ms = Some(wait_started.elapsed().as_millis());
|
|
744
888
|
}
|
|
745
889
|
|
|
746
890
|
let mut guard = state.inner.lock().map_err(|e| e.to_string())?;
|
|
@@ -757,6 +901,20 @@ pub async fn native_fullscreen_recording_begin(
|
|
|
757
901
|
.add_recording_output(recording)
|
|
758
902
|
.map_err(|e| format!("add recording output failed: {e:?}"))?;
|
|
759
903
|
}
|
|
904
|
+
if mic_ready_before_attach.is_some() || mic_samples_before_attach.is_some() {
|
|
905
|
+
eprintln!(
|
|
906
|
+
"[clips-tray] ScreenCaptureKit recording output attached: mic_ready_before_attach={} mic_samples_before_attach={} mic_warm_wait_ms={}",
|
|
907
|
+
mic_ready_before_attach
|
|
908
|
+
.map(|ready| ready.to_string())
|
|
909
|
+
.unwrap_or_else(|| "n/a".to_string()),
|
|
910
|
+
mic_samples_before_attach
|
|
911
|
+
.map(|samples| samples.to_string())
|
|
912
|
+
.unwrap_or_else(|| "n/a".to_string()),
|
|
913
|
+
mic_warm_wait_ms
|
|
914
|
+
.map(|ms| ms.to_string())
|
|
915
|
+
.unwrap_or_else(|| "n/a".to_string())
|
|
916
|
+
);
|
|
917
|
+
}
|
|
760
918
|
// Rebaseline the duration clock: the warm phase ran during the
|
|
761
919
|
// countdown, so `started_at` (set at warm time) is several seconds
|
|
762
920
|
// early. The clip's first written frame is now, so measure from now.
|
|
@@ -781,9 +939,11 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
781
939
|
recording_id: String,
|
|
782
940
|
auth_token: Option<String>,
|
|
783
941
|
cookie: Option<String>,
|
|
942
|
+
upload_mode: Option<String>,
|
|
784
943
|
has_audio: bool,
|
|
785
944
|
has_camera: bool,
|
|
786
945
|
) -> Result<NativeFullscreenUploadResult, String> {
|
|
946
|
+
let upload_mode = NativeUploadMode::from_option(upload_mode);
|
|
787
947
|
emit_native_upload_progress(&app, "finalizing", "Optimizing clip", None, None);
|
|
788
948
|
// The recorder's ScreenCaptureKit stream is now fully stopped and its moov
|
|
789
949
|
// atom is written (or has definitively failed). Signal the UI so it can tear
|
|
@@ -812,6 +972,40 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
812
972
|
// corner through the (multi-second) finalize + upload phase, reading
|
|
813
973
|
// as "still recording" while the bottom-left card says "processing".
|
|
814
974
|
let _ = crate::clips::close_bubble(app.clone()).await;
|
|
975
|
+
clear_recording_active(&app);
|
|
976
|
+
|
|
977
|
+
if multi_segment {
|
|
978
|
+
if let Err(merge_err) = &consolidate_outcome {
|
|
979
|
+
let mut saved = saved_recording_from_segments(
|
|
980
|
+
&session,
|
|
981
|
+
&server_url,
|
|
982
|
+
&recording_id,
|
|
983
|
+
duration_ms,
|
|
984
|
+
has_audio,
|
|
985
|
+
has_camera,
|
|
986
|
+
)?;
|
|
987
|
+
saved.last_error = Some(match &stop_outcome {
|
|
988
|
+
Err(stop_err) => {
|
|
989
|
+
format!("{stop_err}. Segment consolidation failed: {merge_err}")
|
|
990
|
+
}
|
|
991
|
+
Ok(()) => merge_err.clone(),
|
|
992
|
+
});
|
|
993
|
+
write_saved_recording_metadata(&app, &saved)?;
|
|
994
|
+
emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
|
|
995
|
+
let error = format!(
|
|
996
|
+
"{merge_err}. The raw clip segments were saved locally and can be retried from the Clips menu."
|
|
997
|
+
);
|
|
998
|
+
emit_native_upload_finished(
|
|
999
|
+
&app,
|
|
1000
|
+
&server_url,
|
|
1001
|
+
&recording_id,
|
|
1002
|
+
false,
|
|
1003
|
+
Some(error.clone()),
|
|
1004
|
+
Some(&saved.file_path),
|
|
1005
|
+
);
|
|
1006
|
+
return Err(error);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
815
1009
|
|
|
816
1010
|
let mut saved = saved_recording_from_session(
|
|
817
1011
|
&session,
|
|
@@ -850,7 +1044,16 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
850
1044
|
} else {
|
|
851
1045
|
"The clip was saved locally and can be retried from the Clips menu."
|
|
852
1046
|
};
|
|
853
|
-
|
|
1047
|
+
let error = format!("{stop_err}. {suffix}");
|
|
1048
|
+
emit_native_upload_finished(
|
|
1049
|
+
&app,
|
|
1050
|
+
&server_url,
|
|
1051
|
+
&recording_id,
|
|
1052
|
+
false,
|
|
1053
|
+
Some(error.clone()),
|
|
1054
|
+
Some(&saved.file_path),
|
|
1055
|
+
);
|
|
1056
|
+
return Err(error);
|
|
854
1057
|
}
|
|
855
1058
|
Some(true) => {
|
|
856
1059
|
eprintln!(
|
|
@@ -874,29 +1077,29 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
874
1077
|
eprintln!("[clips-tray] recording missing moov after Ok stop outcome (likely finalize timeout) — saving as retryable, skipping upload");
|
|
875
1078
|
write_saved_recording_metadata(&app, &saved)?;
|
|
876
1079
|
emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
|
|
877
|
-
|
|
878
|
-
"Recorded MP4 is missing playback metadata. Please retry the recording.".to_string()
|
|
1080
|
+
let error =
|
|
1081
|
+
"Recorded MP4 is missing playback metadata. Please retry the recording.".to_string();
|
|
1082
|
+
emit_native_upload_finished(
|
|
1083
|
+
&app,
|
|
1084
|
+
&server_url,
|
|
1085
|
+
&recording_id,
|
|
1086
|
+
false,
|
|
1087
|
+
Some(error.clone()),
|
|
1088
|
+
Some(&saved.file_path),
|
|
879
1089
|
);
|
|
880
|
-
|
|
881
|
-
saved.last_error = Some(merge_err.clone());
|
|
1090
|
+
return Err(error);
|
|
882
1091
|
}
|
|
883
1092
|
write_saved_recording_metadata(&app, &saved)?;
|
|
884
1093
|
emit_native_upload_progress(&app, "preparing", "Optimizing clip", None, None);
|
|
885
|
-
if multi_segment {
|
|
886
|
-
if let Err(merge_err) = consolidate_outcome {
|
|
887
|
-
return Err(format!(
|
|
888
|
-
"{merge_err}. The clip segments were saved locally and can be retried from the Clips menu."
|
|
889
|
-
));
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
1094
|
|
|
893
1095
|
let result = upload_recording_file(
|
|
894
1096
|
&app,
|
|
895
1097
|
&session,
|
|
896
|
-
server_url,
|
|
897
|
-
recording_id,
|
|
1098
|
+
server_url.clone(),
|
|
1099
|
+
recording_id.clone(),
|
|
898
1100
|
auth_token.unwrap_or_default(),
|
|
899
1101
|
cookie.unwrap_or_default(),
|
|
1102
|
+
upload_mode,
|
|
900
1103
|
duration_ms,
|
|
901
1104
|
has_audio,
|
|
902
1105
|
has_camera,
|
|
@@ -906,6 +1109,7 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
906
1109
|
match result {
|
|
907
1110
|
Ok(result) => {
|
|
908
1111
|
clear_saved_recording_after_success(&app, &saved);
|
|
1112
|
+
emit_native_upload_finished(&app, &server_url, &recording_id, true, None, None);
|
|
909
1113
|
Ok(result)
|
|
910
1114
|
}
|
|
911
1115
|
Err(err) => {
|
|
@@ -917,9 +1121,18 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
917
1121
|
}
|
|
918
1122
|
let _ = write_saved_recording_metadata(&app, &saved);
|
|
919
1123
|
emit_native_upload_progress(&app, "failed", "Upload paused", None, None);
|
|
920
|
-
|
|
1124
|
+
let error = format!(
|
|
921
1125
|
"{err}. The clip was saved locally and can be retried from the Clips menu."
|
|
922
|
-
)
|
|
1126
|
+
);
|
|
1127
|
+
emit_native_upload_finished(
|
|
1128
|
+
&app,
|
|
1129
|
+
&server_url,
|
|
1130
|
+
&recording_id,
|
|
1131
|
+
false,
|
|
1132
|
+
Some(error.clone()),
|
|
1133
|
+
Some(&saved.file_path),
|
|
1134
|
+
);
|
|
1135
|
+
Err(error)
|
|
923
1136
|
}
|
|
924
1137
|
}
|
|
925
1138
|
}
|
|
@@ -1013,6 +1226,26 @@ pub async fn native_fullscreen_capture_thumbnail(
|
|
|
1013
1226
|
Ok(())
|
|
1014
1227
|
}
|
|
1015
1228
|
|
|
1229
|
+
/// Called from the app's exit path (tray Quit / Cmd+Q) so a live `screencapture`
|
|
1230
|
+
/// fallback process doesn't survive the app quitting. `app.exit()` triggers
|
|
1231
|
+
/// `std::process::exit` under the hood, which does not run Rust destructors,
|
|
1232
|
+
/// so this must run explicitly before exit rather than relying solely on
|
|
1233
|
+
/// `NativeFullscreenBackend`'s `Drop` impl. Synchronous and best-effort: no
|
|
1234
|
+
/// finalize/upload, just make sure nothing keeps recording after we're gone.
|
|
1235
|
+
pub(crate) fn kill_active_screencapture_child(state: &NativeFullscreenRecordingState) {
|
|
1236
|
+
let Ok(mut guard) = state.inner.lock() else {
|
|
1237
|
+
return;
|
|
1238
|
+
};
|
|
1239
|
+
if let Some(session) = guard.as_mut() {
|
|
1240
|
+
if let Some(NativeFullscreenBackend::Screencapture { child }) = session.backend.as_mut() {
|
|
1241
|
+
if matches!(child.try_wait(), Ok(None)) {
|
|
1242
|
+
let _ = child.kill();
|
|
1243
|
+
let _ = child.wait();
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1016
1249
|
#[tauri::command]
|
|
1017
1250
|
pub async fn native_fullscreen_recording_cancel(
|
|
1018
1251
|
state: State<'_, NativeFullscreenRecordingState>,
|
|
@@ -1049,7 +1282,11 @@ pub async fn native_fullscreen_recording_pause(
|
|
|
1049
1282
|
session.paused_at = Some(Instant::now());
|
|
1050
1283
|
return Ok(());
|
|
1051
1284
|
}
|
|
1052
|
-
finalize_active_backend(session, true)
|
|
1285
|
+
let stop_outcome = finalize_active_backend(session, true);
|
|
1286
|
+
if let Err(err) = &stop_outcome {
|
|
1287
|
+
eprintln!("[clips-tray] pause finalize reported an error: {err}");
|
|
1288
|
+
}
|
|
1289
|
+
recover_from_unusable_current_segment(session, "pause", true);
|
|
1053
1290
|
session.paused_at = Some(Instant::now());
|
|
1054
1291
|
Ok(())
|
|
1055
1292
|
}
|
|
@@ -1435,6 +1672,19 @@ fn start_segment_backend(
|
|
|
1435
1672
|
) {
|
|
1436
1673
|
Ok((backend, w, h)) => return Ok((backend, w, h)),
|
|
1437
1674
|
Err(sck_err) => {
|
|
1675
|
+
if include_audio {
|
|
1676
|
+
let mic_description = if mic_device_id
|
|
1677
|
+
.is_some_and(|value| !value.trim().is_empty())
|
|
1678
|
+
|| mic_device_label.is_some_and(|value| !value.trim().is_empty())
|
|
1679
|
+
{
|
|
1680
|
+
"the selected microphone"
|
|
1681
|
+
} else {
|
|
1682
|
+
"the resolved default microphone"
|
|
1683
|
+
};
|
|
1684
|
+
return Err(format!(
|
|
1685
|
+
"ScreenCaptureKit resume failed before it could use {mic_description} ({sck_err}). Clips did not fall back to macOS screencapture because that would ignore the requested input."
|
|
1686
|
+
));
|
|
1687
|
+
}
|
|
1438
1688
|
eprintln!(
|
|
1439
1689
|
"[clips-tray] ScreenCaptureKit resume failed; falling back to screencapture: {sck_err}"
|
|
1440
1690
|
);
|
|
@@ -1514,12 +1764,7 @@ pub(crate) fn start_screencapturekit_backend_at(
|
|
|
1514
1764
|
} else {
|
|
1515
1765
|
filter_builder.build()
|
|
1516
1766
|
};
|
|
1517
|
-
let capture_microphone_in_recording =
|
|
1518
|
-
if include_audio {
|
|
1519
|
-
eprintln!(
|
|
1520
|
-
"[clips-tray] ScreenCaptureKit microphone recording disabled; microphone audio stays out of SCRecordingOutput to avoid macOS 15 finalize timeouts"
|
|
1521
|
-
);
|
|
1522
|
-
}
|
|
1767
|
+
let capture_microphone_in_recording = include_audio;
|
|
1523
1768
|
let selected_mic = if capture_microphone_in_recording {
|
|
1524
1769
|
resolve_microphone_capture_device(mic_device_id, mic_device_label)?
|
|
1525
1770
|
} else {
|
|
@@ -1533,8 +1778,8 @@ pub(crate) fn start_screencapturekit_backend_at(
|
|
|
1533
1778
|
.with_queue_depth(8)
|
|
1534
1779
|
.with_shows_cursor(true)
|
|
1535
1780
|
// Mic and system audio are independent toggles. SCK delivers them as
|
|
1536
|
-
// separate inputs
|
|
1537
|
-
//
|
|
1781
|
+
// separate inputs so recordings can include both the user's mic and
|
|
1782
|
+
// system audio.
|
|
1538
1783
|
.with_captures_audio(capture_system_audio)
|
|
1539
1784
|
.with_captures_microphone(capture_microphone_in_recording)
|
|
1540
1785
|
.with_excludes_current_process_audio(true)
|
|
@@ -1569,20 +1814,23 @@ pub(crate) fn start_screencapturekit_backend_at(
|
|
|
1569
1814
|
let mut stream = SCStream::new(&filter, &config);
|
|
1570
1815
|
// Observe the microphone stream so `begin` can wait for the first sample
|
|
1571
1816
|
// before attaching the recording output (avoids the silent-mic head).
|
|
1572
|
-
let mic_ready = if capture_microphone_in_recording {
|
|
1817
|
+
let (mic_ready, mic_sample_count) = if capture_microphone_in_recording {
|
|
1573
1818
|
let flag = Arc::new(AtomicBool::new(false));
|
|
1819
|
+
let sample_count = Arc::new(AtomicU64::new(0));
|
|
1574
1820
|
let flag_cb = Arc::clone(&flag);
|
|
1821
|
+
let sample_count_cb = Arc::clone(&sample_count);
|
|
1575
1822
|
stream.add_output_handler(
|
|
1576
1823
|
move |_sample, of_type| {
|
|
1577
1824
|
if matches!(of_type, SCStreamOutputType::Microphone) {
|
|
1825
|
+
sample_count_cb.fetch_add(1, Ordering::Relaxed);
|
|
1578
1826
|
flag_cb.store(true, Ordering::Relaxed);
|
|
1579
1827
|
}
|
|
1580
1828
|
},
|
|
1581
1829
|
SCStreamOutputType::Microphone,
|
|
1582
1830
|
);
|
|
1583
|
-
Some(flag)
|
|
1831
|
+
(Some(flag), Some(sample_count))
|
|
1584
1832
|
} else {
|
|
1585
|
-
None
|
|
1833
|
+
(None, None)
|
|
1586
1834
|
};
|
|
1587
1835
|
if !defer_recording_output {
|
|
1588
1836
|
stream
|
|
@@ -1603,6 +1851,7 @@ pub(crate) fn start_screencapturekit_backend_at(
|
|
|
1603
1851
|
recording,
|
|
1604
1852
|
finish,
|
|
1605
1853
|
mic_ready,
|
|
1854
|
+
mic_sample_count,
|
|
1606
1855
|
},
|
|
1607
1856
|
Some(width),
|
|
1608
1857
|
Some(height),
|
|
@@ -1756,7 +2005,7 @@ pub async fn native_fullscreen_pending_uploads(
|
|
|
1756
2005
|
let Ok(saved) = read_saved_recording_metadata_path(&path) else {
|
|
1757
2006
|
continue;
|
|
1758
2007
|
};
|
|
1759
|
-
if saved
|
|
2008
|
+
if saved_recording_has_local_artifact(&saved) {
|
|
1760
2009
|
pending.push(PendingNativeRecording::from(&saved));
|
|
1761
2010
|
} else {
|
|
1762
2011
|
let _ = std::fs::remove_file(path);
|
|
@@ -1857,6 +2106,10 @@ fn describe_recording_path(path: &Path) -> String {
|
|
|
1857
2106
|
)
|
|
1858
2107
|
}
|
|
1859
2108
|
|
|
2109
|
+
fn saved_recording_has_local_artifact(saved: &SavedNativeRecording) -> bool {
|
|
2110
|
+
saved.file_path.exists() || saved.segment_paths.iter().any(|path| path.exists())
|
|
2111
|
+
}
|
|
2112
|
+
|
|
1860
2113
|
fn pending_uploads_dir(app: &AppHandle) -> Result<PathBuf, String> {
|
|
1861
2114
|
let dir = app
|
|
1862
2115
|
.path()
|
|
@@ -1938,6 +2191,37 @@ fn names_match(a: &str, b: &str) -> bool {
|
|
|
1938
2191
|
short.iter().all(|token| long.contains(token))
|
|
1939
2192
|
}
|
|
1940
2193
|
|
|
2194
|
+
#[cfg(target_os = "macos")]
|
|
2195
|
+
fn is_built_in_input_name(value: &str) -> bool {
|
|
2196
|
+
let value = normalize_audio_device_name(value);
|
|
2197
|
+
value.contains("macbook")
|
|
2198
|
+
|| value.contains("built in")
|
|
2199
|
+
|| value.contains("builtin")
|
|
2200
|
+
|| value.contains("internal microphone")
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
#[cfg(target_os = "macos")]
|
|
2204
|
+
fn is_phone_input_name(value: &str) -> bool {
|
|
2205
|
+
let value = normalize_audio_device_name(value);
|
|
2206
|
+
value.contains("iphone")
|
|
2207
|
+
|| value.contains("ipad")
|
|
2208
|
+
|| value.contains("continuity")
|
|
2209
|
+
|| value.contains("phone microphone")
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
#[cfg(target_os = "macos")]
|
|
2213
|
+
fn preferred_default_microphone_device(devices: &[AudioInputDevice]) -> Option<AudioInputDevice> {
|
|
2214
|
+
devices
|
|
2215
|
+
.iter()
|
|
2216
|
+
.find(|device| is_built_in_input_name(&device.name))
|
|
2217
|
+
.or_else(|| {
|
|
2218
|
+
devices
|
|
2219
|
+
.iter()
|
|
2220
|
+
.find(|device| !is_phone_input_name(&device.name))
|
|
2221
|
+
})
|
|
2222
|
+
.cloned()
|
|
2223
|
+
}
|
|
2224
|
+
|
|
1941
2225
|
#[cfg(target_os = "macos")]
|
|
1942
2226
|
fn resolve_microphone_capture_device(
|
|
1943
2227
|
device_id: Option<&str>,
|
|
@@ -1948,11 +2232,6 @@ fn resolve_microphone_capture_device(
|
|
|
1948
2232
|
.map(str::trim)
|
|
1949
2233
|
.filter(|value| !value.is_empty());
|
|
1950
2234
|
|
|
1951
|
-
if device_id.is_none() && device_label.is_none() {
|
|
1952
|
-
eprintln!("[clips-tray] audio input devices not provided");
|
|
1953
|
-
return Ok(None);
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
2235
|
let devices = AudioInputDevice::list();
|
|
1957
2236
|
devices.iter().for_each(|device| {
|
|
1958
2237
|
eprintln!(
|
|
@@ -1961,6 +2240,18 @@ fn resolve_microphone_capture_device(
|
|
|
1961
2240
|
);
|
|
1962
2241
|
});
|
|
1963
2242
|
|
|
2243
|
+
if device_id.is_none() && device_label.is_none() {
|
|
2244
|
+
let resolved = preferred_default_microphone_device(&devices);
|
|
2245
|
+
eprintln!(
|
|
2246
|
+
"[clips-tray] mic resolve: no explicit input provided -> {}",
|
|
2247
|
+
match &resolved {
|
|
2248
|
+
Some(device) => format!("using {} ({})", device.name, device.id),
|
|
2249
|
+
None => "using macOS default input".to_string(),
|
|
2250
|
+
}
|
|
2251
|
+
);
|
|
2252
|
+
return Ok(resolved);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
1964
2255
|
let resolved = device_id
|
|
1965
2256
|
.and_then(|id| devices.iter().find(|device| device.id == id))
|
|
1966
2257
|
.or_else(|| {
|
|
@@ -2187,9 +2478,70 @@ fn saved_recording_from_session(
|
|
|
2187
2478
|
has_audio: bool,
|
|
2188
2479
|
has_camera: bool,
|
|
2189
2480
|
) -> Result<SavedNativeRecording, String> {
|
|
2190
|
-
|
|
2481
|
+
saved_recording_from_path(
|
|
2482
|
+
session,
|
|
2483
|
+
&session.path,
|
|
2484
|
+
Vec::new(),
|
|
2485
|
+
server_url,
|
|
2486
|
+
recording_id,
|
|
2487
|
+
duration_ms,
|
|
2488
|
+
has_audio,
|
|
2489
|
+
has_camera,
|
|
2490
|
+
)
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
fn saved_recording_from_segments(
|
|
2494
|
+
session: &NativeFullscreenSession,
|
|
2495
|
+
server_url: &str,
|
|
2496
|
+
recording_id: &str,
|
|
2497
|
+
duration_ms: u128,
|
|
2498
|
+
has_audio: bool,
|
|
2499
|
+
has_camera: bool,
|
|
2500
|
+
) -> Result<SavedNativeRecording, String> {
|
|
2501
|
+
let segment_paths: Vec<PathBuf> = session
|
|
2502
|
+
.segments
|
|
2503
|
+
.iter()
|
|
2504
|
+
.filter(|path| path.exists())
|
|
2505
|
+
.cloned()
|
|
2506
|
+
.collect();
|
|
2507
|
+
let fallback_path = segment_paths
|
|
2508
|
+
.iter()
|
|
2509
|
+
.find(|path| playable_recording_file(path, session.mime_type))
|
|
2510
|
+
.or_else(|| {
|
|
2511
|
+
segment_paths.iter().find(|path| {
|
|
2512
|
+
std::fs::metadata(path)
|
|
2513
|
+
.map(|meta| meta.len() > 0)
|
|
2514
|
+
.unwrap_or(false)
|
|
2515
|
+
})
|
|
2516
|
+
})
|
|
2517
|
+
.cloned()
|
|
2518
|
+
.ok_or_else(|| "No local recording segment survived consolidation failure.".to_string())?;
|
|
2519
|
+
|
|
2520
|
+
saved_recording_from_path(
|
|
2521
|
+
session,
|
|
2522
|
+
&fallback_path,
|
|
2523
|
+
segment_paths,
|
|
2524
|
+
server_url,
|
|
2525
|
+
recording_id,
|
|
2526
|
+
duration_ms,
|
|
2527
|
+
has_audio,
|
|
2528
|
+
has_camera,
|
|
2529
|
+
)
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
fn saved_recording_from_path(
|
|
2533
|
+
session: &NativeFullscreenSession,
|
|
2534
|
+
file_path: &Path,
|
|
2535
|
+
segment_paths: Vec<PathBuf>,
|
|
2536
|
+
server_url: &str,
|
|
2537
|
+
recording_id: &str,
|
|
2538
|
+
duration_ms: u128,
|
|
2539
|
+
has_audio: bool,
|
|
2540
|
+
has_camera: bool,
|
|
2541
|
+
) -> Result<SavedNativeRecording, String> {
|
|
2542
|
+
let bytes = std::fs::metadata(file_path)
|
|
2191
2543
|
.map_err(|e| {
|
|
2192
|
-
let diag = describe_recording_path(
|
|
2544
|
+
let diag = describe_recording_path(file_path);
|
|
2193
2545
|
eprintln!(
|
|
2194
2546
|
"[clips-tray] native recording file missing at save: {e}; backend={}, segments={}, {diag}",
|
|
2195
2547
|
session.mime_type,
|
|
@@ -2201,7 +2553,7 @@ fn saved_recording_from_session(
|
|
|
2201
2553
|
if bytes == 0 {
|
|
2202
2554
|
eprintln!(
|
|
2203
2555
|
"[clips-tray] native recording empty at save: {}",
|
|
2204
|
-
describe_recording_path(
|
|
2556
|
+
describe_recording_path(file_path)
|
|
2205
2557
|
);
|
|
2206
2558
|
return Err("Native recording produced an empty file.".into());
|
|
2207
2559
|
}
|
|
@@ -2209,7 +2561,8 @@ fn saved_recording_from_session(
|
|
|
2209
2561
|
Ok(SavedNativeRecording {
|
|
2210
2562
|
recording_id: recording_id.to_string(),
|
|
2211
2563
|
server_url: server_url.trim_end_matches('/').to_string(),
|
|
2212
|
-
file_path:
|
|
2564
|
+
file_path: file_path.to_path_buf(),
|
|
2565
|
+
segment_paths,
|
|
2213
2566
|
mime_type: session.mime_type.to_string(),
|
|
2214
2567
|
duration_ms,
|
|
2215
2568
|
width: session.width,
|
|
@@ -2261,6 +2614,11 @@ fn remove_saved_file(path: &Path, label: &str) -> Result<(), String> {
|
|
|
2261
2614
|
|
|
2262
2615
|
fn clear_saved_recording(app: &AppHandle, saved: &SavedNativeRecording) -> Result<(), String> {
|
|
2263
2616
|
remove_saved_file(&saved.file_path, "pending recording file")?;
|
|
2617
|
+
for segment_path in &saved.segment_paths {
|
|
2618
|
+
if segment_path != &saved.file_path {
|
|
2619
|
+
remove_saved_file(segment_path, "pending recording segment")?;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2264
2622
|
let path = saved_recording_metadata_path(app, &saved.recording_id)?;
|
|
2265
2623
|
remove_saved_file(&path, "pending recording metadata")
|
|
2266
2624
|
}
|
|
@@ -2391,7 +2749,7 @@ fn start_screencapturekit_recording(
|
|
|
2391
2749
|
safe_id: safe_id.to_string(),
|
|
2392
2750
|
include_audio,
|
|
2393
2751
|
capture_system_audio,
|
|
2394
|
-
mic_captured_in_file:
|
|
2752
|
+
mic_captured_in_file: include_audio,
|
|
2395
2753
|
mic_device_id: mic_device_id.map(str::to_string),
|
|
2396
2754
|
mic_device_label: mic_device_label.map(str::to_string),
|
|
2397
2755
|
segment_counter: 0,
|
|
@@ -2589,44 +2947,23 @@ pub(crate) fn stop_native_recording(
|
|
|
2589
2947
|
match backend {
|
|
2590
2948
|
NativeFullscreenBackend::Screencapture { child } => stop_screencapture(child),
|
|
2591
2949
|
#[cfg(target_os = "macos")]
|
|
2592
|
-
NativeFullscreenBackend::ScreenCaptureKit {
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
//
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
let remove_result = stream
|
|
2603
|
-
.remove_recording_output(recording)
|
|
2604
|
-
.map_err(|e| format!("ScreenCaptureKit recording finalize failed: {e:?}"));
|
|
2605
|
-
let remove_result = match remove_result {
|
|
2606
|
-
Ok(v) => Ok(v),
|
|
2607
|
-
Err(first_err) => {
|
|
2608
|
-
eprintln!("[clips-tray] waiting 200ms for SCK frame buffer to drain before remove_recording_output retry");
|
|
2609
|
-
std::thread::sleep(Duration::from_millis(200));
|
|
2610
|
-
stream.remove_recording_output(recording).map_err(|e| {
|
|
2611
|
-
format!(
|
|
2612
|
-
"ScreenCaptureKit recording finalize failed (retry): {e:?}; first attempt: {first_err}"
|
|
2613
|
-
)
|
|
2614
|
-
})
|
|
2615
|
-
}
|
|
2616
|
-
};
|
|
2617
|
-
// remove_recording_output()/stop_capture() only *trigger* the
|
|
2618
|
-
// async finalize; the MP4 isn't complete until the delegate's
|
|
2619
|
-
// recording_did_finish fires. Prefer waiting after removal while
|
|
2620
|
-
// the stream is alive, then stop the stream after the output has
|
|
2621
|
-
// had a chance to flush.
|
|
2622
|
-
let remove_ok = remove_result.is_ok();
|
|
2950
|
+
NativeFullscreenBackend::ScreenCaptureKit { stream, finish, .. } => {
|
|
2951
|
+
// `remove_recording_output()` looks like the clean stop path, but
|
|
2952
|
+
// on real machines it can block synchronously forever when the
|
|
2953
|
+
// underlying SCStream connection is interrupted. `stop_capture()`
|
|
2954
|
+
// returns control to us, then the delegate callback is bounded by
|
|
2955
|
+
// `SCK_FINALIZE_TIMEOUT`; the moov/audio guards below decide
|
|
2956
|
+
// whether the resulting file is uploadable or recoverable.
|
|
2957
|
+
let stop_result = stream
|
|
2958
|
+
.stop_capture()
|
|
2959
|
+
.map_err(|e| format!("ScreenCaptureKit stop failed: {e:?}"));
|
|
2623
2960
|
let mut waited_for_finalize = false;
|
|
2624
|
-
let
|
|
2961
|
+
let finalize_outcome = if wait_for_finalize {
|
|
2625
2962
|
waited_for_finalize = true;
|
|
2626
2963
|
let outcome = finish.wait(SCK_FINALIZE_TIMEOUT);
|
|
2627
2964
|
if outcome.is_none() {
|
|
2628
2965
|
eprintln!(
|
|
2629
|
-
"[clips-tray] SCRecordingOutput finalize callback did not fire within {}s after
|
|
2966
|
+
"[clips-tray] SCRecordingOutput finalize callback did not fire within {}s after stop_capture; saving file as-is",
|
|
2630
2967
|
SCK_FINALIZE_TIMEOUT.as_secs()
|
|
2631
2968
|
);
|
|
2632
2969
|
}
|
|
@@ -2635,21 +2972,6 @@ pub(crate) fn stop_native_recording(
|
|
|
2635
2972
|
None
|
|
2636
2973
|
};
|
|
2637
2974
|
|
|
2638
|
-
let stop_result = stream
|
|
2639
|
-
.stop_capture()
|
|
2640
|
-
.map_err(|e| format!("ScreenCaptureKit stop failed: {e:?}"));
|
|
2641
|
-
|
|
2642
|
-
if wait_for_finalize && !waited_for_finalize && stop_result.is_ok() {
|
|
2643
|
-
waited_for_finalize = true;
|
|
2644
|
-
finalize_outcome = finish.wait(SCK_FINALIZE_TIMEOUT);
|
|
2645
|
-
if finalize_outcome.is_none() {
|
|
2646
|
-
eprintln!(
|
|
2647
|
-
"[clips-tray] SCRecordingOutput finalize callback did not fire within {}s after stop_capture; saving file as-is",
|
|
2648
|
-
SCK_FINALIZE_TIMEOUT.as_secs()
|
|
2649
|
-
);
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
2975
|
// Check the delegate outcome BEFORE returning on stop_result. When
|
|
2654
2976
|
// stop_capture() fails AND recording_did_fail fires, callers must see
|
|
2655
2977
|
// the "finalization callback failed" prefix to correctly identify
|
|
@@ -2670,25 +2992,10 @@ pub(crate) fn stop_native_recording(
|
|
|
2670
2992
|
);
|
|
2671
2993
|
}
|
|
2672
2994
|
|
|
2673
|
-
let remove_recoverable = matches!(finalize_outcome.as_ref(), Some(Ok(())))
|
|
2674
|
-
|| (waited_for_finalize && finalize_outcome.is_none());
|
|
2675
|
-
match remove_result {
|
|
2676
|
-
Ok(()) => {}
|
|
2677
|
-
Err(remove_err) => {
|
|
2678
|
-
if remove_recoverable {
|
|
2679
|
-
eprintln!(
|
|
2680
|
-
"[clips-tray] ScreenCaptureKit recording output removal reported an error after finalize completed or timed out; continuing upload: {remove_err}"
|
|
2681
|
-
);
|
|
2682
|
-
} else {
|
|
2683
|
-
return Err(remove_err);
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
2995
|
if let Err(stop_err) = stop_result {
|
|
2689
|
-
if
|
|
2996
|
+
if matches!(finalize_outcome.as_ref(), Some(Ok(()))) {
|
|
2690
2997
|
eprintln!(
|
|
2691
|
-
"[clips-tray] ScreenCaptureKit stop_capture reported an error after
|
|
2998
|
+
"[clips-tray] ScreenCaptureKit stop_capture reported an error after finalize completed; continuing upload: {stop_err}"
|
|
2692
2999
|
);
|
|
2693
3000
|
} else {
|
|
2694
3001
|
return Err(stop_err);
|
|
@@ -2743,6 +3050,7 @@ async fn upload_recording_file(
|
|
|
2743
3050
|
recording_id: String,
|
|
2744
3051
|
auth_token: String,
|
|
2745
3052
|
cookie: String,
|
|
3053
|
+
upload_mode: NativeUploadMode,
|
|
2746
3054
|
duration_ms: u128,
|
|
2747
3055
|
has_audio: bool,
|
|
2748
3056
|
has_camera: bool,
|
|
@@ -2764,6 +3072,7 @@ async fn upload_recording_file(
|
|
|
2764
3072
|
recording_id,
|
|
2765
3073
|
auth_token,
|
|
2766
3074
|
cookie,
|
|
3075
|
+
upload_mode,
|
|
2767
3076
|
duration_ms,
|
|
2768
3077
|
session.width,
|
|
2769
3078
|
session.height,
|
|
@@ -2784,23 +3093,15 @@ async fn upload_saved_recording_file(
|
|
|
2784
3093
|
auth_token: String,
|
|
2785
3094
|
cookie: String,
|
|
2786
3095
|
) -> Result<NativeFullscreenUploadResult, String> {
|
|
2787
|
-
let prepared =
|
|
2788
|
-
|
|
2789
|
-
&saved.file_path,
|
|
2790
|
-
&saved.mime_type,
|
|
2791
|
-
saved.width,
|
|
2792
|
-
saved.height,
|
|
2793
|
-
Some(saved.duration_ms),
|
|
2794
|
-
saved.has_audio,
|
|
2795
|
-
saved.mic_captured,
|
|
2796
|
-
)?;
|
|
2797
|
-
let upload_result = upload_prepared_recording_file(
|
|
3096
|
+
let (prepared, retry_combined_path) = prepare_saved_recording_file(app, saved)?;
|
|
3097
|
+
let upload_result = upload_prepared_recording_file(
|
|
2798
3098
|
app,
|
|
2799
3099
|
&prepared,
|
|
2800
3100
|
server_url,
|
|
2801
3101
|
saved.recording_id.clone(),
|
|
2802
3102
|
auth_token,
|
|
2803
3103
|
cookie,
|
|
3104
|
+
NativeUploadMode::Buffered,
|
|
2804
3105
|
saved.duration_ms,
|
|
2805
3106
|
saved.width,
|
|
2806
3107
|
saved.height,
|
|
@@ -2811,9 +3112,64 @@ async fn upload_saved_recording_file(
|
|
|
2811
3112
|
if prepared.temporary {
|
|
2812
3113
|
let _ = std::fs::remove_file(&prepared.path);
|
|
2813
3114
|
}
|
|
3115
|
+
if let Some(path) = retry_combined_path {
|
|
3116
|
+
if path != prepared.path || !prepared.temporary {
|
|
3117
|
+
let _ = std::fs::remove_file(path);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
2814
3120
|
upload_result
|
|
2815
3121
|
}
|
|
2816
3122
|
|
|
3123
|
+
fn prepare_saved_recording_file(
|
|
3124
|
+
app: &AppHandle,
|
|
3125
|
+
saved: &SavedNativeRecording,
|
|
3126
|
+
) -> Result<(PreparedRecordingFile, Option<PathBuf>), String> {
|
|
3127
|
+
let retry_combined_path = if saved.segment_paths.len() > 1 {
|
|
3128
|
+
let output = retry_combined_recording_path(saved);
|
|
3129
|
+
let _ = std::fs::remove_file(&output);
|
|
3130
|
+
concat_saved_recording_segments(&saved.segment_paths, &output)?;
|
|
3131
|
+
Some(output)
|
|
3132
|
+
} else {
|
|
3133
|
+
None
|
|
3134
|
+
};
|
|
3135
|
+
let source_path = retry_combined_path
|
|
3136
|
+
.as_ref()
|
|
3137
|
+
.unwrap_or(&saved.file_path)
|
|
3138
|
+
.to_path_buf();
|
|
3139
|
+
let prepared = prepare_recording_file(
|
|
3140
|
+
app,
|
|
3141
|
+
&source_path,
|
|
3142
|
+
&saved.mime_type,
|
|
3143
|
+
saved.width,
|
|
3144
|
+
saved.height,
|
|
3145
|
+
Some(saved.duration_ms),
|
|
3146
|
+
saved.has_audio,
|
|
3147
|
+
saved.mic_captured,
|
|
3148
|
+
)?;
|
|
3149
|
+
Ok((prepared, retry_combined_path))
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
fn retry_combined_recording_path(saved: &SavedNativeRecording) -> PathBuf {
|
|
3153
|
+
let stem = saved
|
|
3154
|
+
.file_path
|
|
3155
|
+
.file_stem()
|
|
3156
|
+
.and_then(|value| value.to_str())
|
|
3157
|
+
.unwrap_or("recording");
|
|
3158
|
+
saved
|
|
3159
|
+
.file_path
|
|
3160
|
+
.with_file_name(format!("{stem}-retry-combined.mp4"))
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
#[cfg(target_os = "macos")]
|
|
3164
|
+
fn concat_saved_recording_segments(segments: &[PathBuf], output: &Path) -> Result<(), String> {
|
|
3165
|
+
concat_mp4_segments(segments, output)
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
#[cfg(not(target_os = "macos"))]
|
|
3169
|
+
fn concat_saved_recording_segments(_segments: &[PathBuf], _output: &Path) -> Result<(), String> {
|
|
3170
|
+
Err("Segment concat is only available on macOS.".into())
|
|
3171
|
+
}
|
|
3172
|
+
|
|
2817
3173
|
async fn upload_prepared_recording_file(
|
|
2818
3174
|
app: &AppHandle,
|
|
2819
3175
|
prepared: &PreparedRecordingFile,
|
|
@@ -2821,6 +3177,7 @@ async fn upload_prepared_recording_file(
|
|
|
2821
3177
|
recording_id: String,
|
|
2822
3178
|
auth_token: String,
|
|
2823
3179
|
cookie: String,
|
|
3180
|
+
upload_mode: NativeUploadMode,
|
|
2824
3181
|
duration_ms: u128,
|
|
2825
3182
|
width: Option<u32>,
|
|
2826
3183
|
height: Option<u32>,
|
|
@@ -2828,9 +3185,21 @@ async fn upload_prepared_recording_file(
|
|
|
2828
3185
|
has_camera: bool,
|
|
2829
3186
|
) -> Result<NativeFullscreenUploadResult, String> {
|
|
2830
3187
|
let total_bytes = prepared.bytes;
|
|
2831
|
-
let
|
|
2832
|
-
|
|
3188
|
+
let total_bytes_usize = usize::try_from(total_bytes)
|
|
3189
|
+
.map_err(|_| "Native recording is too large to upload on this system.".to_string())?;
|
|
3190
|
+
let total_chunks = (total_bytes_usize + UPLOAD_CHUNK_BYTES - 1) / UPLOAD_CHUNK_BYTES;
|
|
3191
|
+
let streaming_full_chunks = total_bytes_usize / UPLOAD_CHUNK_BYTES;
|
|
3192
|
+
let streaming_remainder = total_bytes_usize % UPLOAD_CHUNK_BYTES;
|
|
3193
|
+
let total_posts = if upload_mode == NativeUploadMode::Streaming {
|
|
3194
|
+
streaming_full_chunks + 1
|
|
3195
|
+
} else {
|
|
3196
|
+
total_chunks + 1
|
|
3197
|
+
};
|
|
2833
3198
|
emit_native_upload_progress(app, "uploading", "Uploading clip", None, Some(0.0));
|
|
3199
|
+
eprintln!(
|
|
3200
|
+
"[clips-tray] native upload starting recording={recording_id} mode={} bytes={total_bytes} posts={total_posts}",
|
|
3201
|
+
upload_mode.label()
|
|
3202
|
+
);
|
|
2834
3203
|
let client = reqwest::Client::builder()
|
|
2835
3204
|
.timeout(Duration::from_secs(180))
|
|
2836
3205
|
.build()
|
|
@@ -2838,68 +3207,144 @@ async fn upload_prepared_recording_file(
|
|
|
2838
3207
|
let mut file =
|
|
2839
3208
|
File::open(&prepared.path).map_err(|e| format!("native recording open failed: {e}"))?;
|
|
2840
3209
|
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
3210
|
+
if upload_mode == NativeUploadMode::Streaming {
|
|
3211
|
+
// Resumable providers require every non-final body to be aligned. The
|
|
3212
|
+
// final body may be the unaligned tail; if the file is exactly aligned,
|
|
3213
|
+
// an empty final request closes the session.
|
|
3214
|
+
for index in 0..streaming_full_chunks {
|
|
3215
|
+
let mut buffer = vec![0_u8; UPLOAD_CHUNK_BYTES];
|
|
3216
|
+
file.read_exact(&mut buffer)
|
|
3217
|
+
.map_err(|e| format!("native recording read failed: {e}"))?;
|
|
3218
|
+
send_upload_post(
|
|
3219
|
+
&client,
|
|
3220
|
+
&server_url,
|
|
3221
|
+
&recording_id,
|
|
3222
|
+
&auth_token,
|
|
3223
|
+
&cookie,
|
|
3224
|
+
index,
|
|
3225
|
+
total_posts,
|
|
3226
|
+
false,
|
|
3227
|
+
None,
|
|
3228
|
+
&prepared.mime_type,
|
|
3229
|
+
width,
|
|
3230
|
+
height,
|
|
3231
|
+
has_audio,
|
|
3232
|
+
has_camera,
|
|
3233
|
+
upload_mode,
|
|
3234
|
+
false,
|
|
3235
|
+
buffer,
|
|
3236
|
+
)
|
|
3237
|
+
.await?;
|
|
3238
|
+
emit_native_upload_progress(
|
|
3239
|
+
app,
|
|
3240
|
+
"uploading",
|
|
3241
|
+
"Uploading clip",
|
|
3242
|
+
None,
|
|
3243
|
+
Some((index + 1) as f32 / total_posts as f32),
|
|
3244
|
+
);
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
let mut final_body = vec![0_u8; streaming_remainder];
|
|
3248
|
+
if streaming_remainder > 0 {
|
|
3249
|
+
file.read_exact(&mut final_body)
|
|
3250
|
+
.map_err(|e| format!("native recording read failed: {e}"))?;
|
|
2848
3251
|
}
|
|
2849
|
-
|
|
3252
|
+
|
|
3253
|
+
emit_native_upload_progress(
|
|
3254
|
+
app,
|
|
3255
|
+
"processing",
|
|
3256
|
+
"Uploading clip",
|
|
3257
|
+
None,
|
|
3258
|
+
Some(streaming_full_chunks as f32 / total_posts as f32),
|
|
3259
|
+
);
|
|
2850
3260
|
send_upload_post(
|
|
2851
3261
|
&client,
|
|
2852
3262
|
&server_url,
|
|
2853
3263
|
&recording_id,
|
|
2854
3264
|
&auth_token,
|
|
2855
3265
|
&cookie,
|
|
2856
|
-
|
|
3266
|
+
streaming_full_chunks,
|
|
2857
3267
|
total_posts,
|
|
2858
|
-
|
|
2859
|
-
|
|
3268
|
+
true,
|
|
3269
|
+
Some(duration_ms),
|
|
2860
3270
|
&prepared.mime_type,
|
|
2861
3271
|
width,
|
|
2862
3272
|
height,
|
|
2863
3273
|
has_audio,
|
|
2864
3274
|
has_camera,
|
|
2865
|
-
|
|
3275
|
+
upload_mode,
|
|
3276
|
+
prepared.locally_transcoded,
|
|
3277
|
+
final_body,
|
|
2866
3278
|
)
|
|
2867
3279
|
.await?;
|
|
3280
|
+
} else {
|
|
3281
|
+
for index in 0..total_chunks {
|
|
3282
|
+
let mut buffer = vec![0_u8; UPLOAD_CHUNK_BYTES];
|
|
3283
|
+
let read = file
|
|
3284
|
+
.read(&mut buffer)
|
|
3285
|
+
.map_err(|e| format!("native recording read failed: {e}"))?;
|
|
3286
|
+
if read == 0 {
|
|
3287
|
+
return Err("Native recording ended before all chunks were read.".into());
|
|
3288
|
+
}
|
|
3289
|
+
buffer.truncate(read);
|
|
3290
|
+
send_upload_post(
|
|
3291
|
+
&client,
|
|
3292
|
+
&server_url,
|
|
3293
|
+
&recording_id,
|
|
3294
|
+
&auth_token,
|
|
3295
|
+
&cookie,
|
|
3296
|
+
index,
|
|
3297
|
+
total_posts,
|
|
3298
|
+
false,
|
|
3299
|
+
None,
|
|
3300
|
+
&prepared.mime_type,
|
|
3301
|
+
width,
|
|
3302
|
+
height,
|
|
3303
|
+
has_audio,
|
|
3304
|
+
has_camera,
|
|
3305
|
+
upload_mode,
|
|
3306
|
+
false,
|
|
3307
|
+
buffer,
|
|
3308
|
+
)
|
|
3309
|
+
.await?;
|
|
3310
|
+
emit_native_upload_progress(
|
|
3311
|
+
app,
|
|
3312
|
+
"uploading",
|
|
3313
|
+
"Uploading clip",
|
|
3314
|
+
None,
|
|
3315
|
+
Some((index + 1) as f32 / total_posts as f32),
|
|
3316
|
+
);
|
|
3317
|
+
}
|
|
3318
|
+
|
|
2868
3319
|
emit_native_upload_progress(
|
|
2869
3320
|
app,
|
|
2870
|
-
"
|
|
3321
|
+
"processing",
|
|
2871
3322
|
"Uploading clip",
|
|
2872
3323
|
None,
|
|
2873
|
-
Some(
|
|
3324
|
+
Some(total_chunks as f32 / total_posts as f32),
|
|
2874
3325
|
);
|
|
3326
|
+
send_upload_post(
|
|
3327
|
+
&client,
|
|
3328
|
+
&server_url,
|
|
3329
|
+
&recording_id,
|
|
3330
|
+
&auth_token,
|
|
3331
|
+
&cookie,
|
|
3332
|
+
total_chunks,
|
|
3333
|
+
total_posts,
|
|
3334
|
+
true,
|
|
3335
|
+
Some(duration_ms),
|
|
3336
|
+
&prepared.mime_type,
|
|
3337
|
+
width,
|
|
3338
|
+
height,
|
|
3339
|
+
has_audio,
|
|
3340
|
+
has_camera,
|
|
3341
|
+
upload_mode,
|
|
3342
|
+
prepared.locally_transcoded,
|
|
3343
|
+
Vec::new(),
|
|
3344
|
+
)
|
|
3345
|
+
.await?;
|
|
2875
3346
|
}
|
|
2876
3347
|
|
|
2877
|
-
emit_native_upload_progress(
|
|
2878
|
-
app,
|
|
2879
|
-
"processing",
|
|
2880
|
-
"Uploading clip",
|
|
2881
|
-
None,
|
|
2882
|
-
Some(total_chunks as f32 / total_posts as f32),
|
|
2883
|
-
);
|
|
2884
|
-
send_upload_post(
|
|
2885
|
-
&client,
|
|
2886
|
-
&server_url,
|
|
2887
|
-
&recording_id,
|
|
2888
|
-
&auth_token,
|
|
2889
|
-
&cookie,
|
|
2890
|
-
total_chunks,
|
|
2891
|
-
total_posts,
|
|
2892
|
-
true,
|
|
2893
|
-
Some(duration_ms),
|
|
2894
|
-
&prepared.mime_type,
|
|
2895
|
-
width,
|
|
2896
|
-
height,
|
|
2897
|
-
has_audio,
|
|
2898
|
-
has_camera,
|
|
2899
|
-
Vec::new(),
|
|
2900
|
-
)
|
|
2901
|
-
.await?;
|
|
2902
|
-
|
|
2903
3348
|
emit_native_upload_progress(app, "opening", "Uploading clip", None, Some(1.0));
|
|
2904
3349
|
Ok(NativeFullscreenUploadResult {
|
|
2905
3350
|
recording_id,
|
|
@@ -3016,8 +3461,11 @@ async fn send_upload_post(
|
|
|
3016
3461
|
height: Option<u32>,
|
|
3017
3462
|
has_audio: bool,
|
|
3018
3463
|
has_camera: bool,
|
|
3464
|
+
upload_mode: NativeUploadMode,
|
|
3465
|
+
locally_transcoded: bool,
|
|
3019
3466
|
body: Vec<u8>,
|
|
3020
3467
|
) -> Result<(), String> {
|
|
3468
|
+
let body_len = body.len();
|
|
3021
3469
|
let url = upload_url(
|
|
3022
3470
|
server_url,
|
|
3023
3471
|
recording_id,
|
|
@@ -3030,7 +3478,12 @@ async fn send_upload_post(
|
|
|
3030
3478
|
height,
|
|
3031
3479
|
has_audio,
|
|
3032
3480
|
has_camera,
|
|
3481
|
+
locally_transcoded,
|
|
3033
3482
|
)?;
|
|
3483
|
+
eprintln!(
|
|
3484
|
+
"[clips-tray] native upload post start recording={recording_id} mode={} index={index}/{total} final={is_final} bytes={body_len}",
|
|
3485
|
+
upload_mode.label()
|
|
3486
|
+
);
|
|
3034
3487
|
let mut request = client
|
|
3035
3488
|
.post(url)
|
|
3036
3489
|
.header("Content-Type", mime_type)
|
|
@@ -3057,6 +3510,10 @@ async fn send_upload_post(
|
|
|
3057
3510
|
body.chars().take(400).collect::<String>()
|
|
3058
3511
|
));
|
|
3059
3512
|
}
|
|
3513
|
+
eprintln!(
|
|
3514
|
+
"[clips-tray] native upload post ok recording={recording_id} mode={} index={index}/{total} final={is_final}",
|
|
3515
|
+
upload_mode.label()
|
|
3516
|
+
);
|
|
3060
3517
|
Ok(())
|
|
3061
3518
|
}
|
|
3062
3519
|
|
|
@@ -3072,6 +3529,7 @@ fn upload_url(
|
|
|
3072
3529
|
height: Option<u32>,
|
|
3073
3530
|
has_audio: bool,
|
|
3074
3531
|
has_camera: bool,
|
|
3532
|
+
locally_transcoded: bool,
|
|
3075
3533
|
) -> Result<String, String> {
|
|
3076
3534
|
let base = server_url.trim_end_matches('/');
|
|
3077
3535
|
let mut url = url::Url::parse(&format!("{base}/api/uploads/{recording_id}/chunk"))
|
|
@@ -3094,6 +3552,9 @@ fn upload_url(
|
|
|
3094
3552
|
if let Some(height) = height {
|
|
3095
3553
|
query.append_pair("height", &height.to_string());
|
|
3096
3554
|
}
|
|
3555
|
+
if is_final && locally_transcoded {
|
|
3556
|
+
query.append_pair("locallyTranscoded", "1");
|
|
3557
|
+
}
|
|
3097
3558
|
}
|
|
3098
3559
|
Ok(url.to_string())
|
|
3099
3560
|
}
|
|
@@ -3172,6 +3633,235 @@ pub(crate) fn mp4_has_moov(path: &Path) -> Option<bool> {
|
|
|
3172
3633
|
}
|
|
3173
3634
|
}
|
|
3174
3635
|
|
|
3636
|
+
/// Scan an MP4/QuickTime file for a `soun` (audio) handler nested under the
|
|
3637
|
+
/// top-level `moov` box (i.e. `moov > trak > mdia > hdlr`). Used to verify
|
|
3638
|
+
/// that ffmpeg/avconvert actually preserved an audio track rather than
|
|
3639
|
+
/// silently dropping it — `-map 0:a?` and similar optional maps exit 0 and
|
|
3640
|
+
/// produce a valid, smaller, video-only MP4 when the source audio stream
|
|
3641
|
+
/// can't be mapped, so a successful exit status alone can't be trusted when
|
|
3642
|
+
/// audio is expected. Returns `Some(true)`/`Some(false)` once `moov` has
|
|
3643
|
+
/// been located and scanned, or `None` when the file could not be read or
|
|
3644
|
+
/// `moov` could not be found/parsed (transient I/O error or unexpected
|
|
3645
|
+
/// structure — callers must not treat this as a definite "no audio").
|
|
3646
|
+
pub(crate) fn mp4_has_audio_track(path: &Path) -> Option<bool> {
|
|
3647
|
+
use std::io::{ErrorKind, Read, Seek, SeekFrom};
|
|
3648
|
+
let mut f = match std::fs::File::open(path) {
|
|
3649
|
+
Ok(f) => f,
|
|
3650
|
+
Err(e) => {
|
|
3651
|
+
eprintln!("[clips-tray] mp4_has_audio_track: could not open file for scan: {e}");
|
|
3652
|
+
return None;
|
|
3653
|
+
}
|
|
3654
|
+
};
|
|
3655
|
+
|
|
3656
|
+
// Walk the top-level boxes (mirrors `mp4_has_moov`) until `moov` is
|
|
3657
|
+
// found, then read its entire body into memory. `moov` holds only
|
|
3658
|
+
// metadata (no sample data, which lives in `mdat`), so even for large
|
|
3659
|
+
// recordings it is at most a few hundred KB — safe to buffer fully.
|
|
3660
|
+
let moov = loop {
|
|
3661
|
+
let mut buf = [0u8; 8];
|
|
3662
|
+
match f.read_exact(&mut buf) {
|
|
3663
|
+
Ok(()) => {}
|
|
3664
|
+
Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Some(false),
|
|
3665
|
+
Err(_) => return None,
|
|
3666
|
+
}
|
|
3667
|
+
let box_size_raw = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
|
|
3668
|
+
let box_type = &buf[4..8];
|
|
3669
|
+
let body_size: u64 = match box_size_raw {
|
|
3670
|
+
0 => return Some(false), // box extends to EOF — can't be moov and something after it
|
|
3671
|
+
1 => {
|
|
3672
|
+
let mut ext = [0u8; 8];
|
|
3673
|
+
match f.read_exact(&mut ext) {
|
|
3674
|
+
Ok(()) => {}
|
|
3675
|
+
Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Some(false),
|
|
3676
|
+
Err(_) => return None,
|
|
3677
|
+
}
|
|
3678
|
+
u64::from_be_bytes(ext).saturating_sub(16)
|
|
3679
|
+
}
|
|
3680
|
+
n if n < 8 => return Some(false),
|
|
3681
|
+
n => (n as u64).saturating_sub(8),
|
|
3682
|
+
};
|
|
3683
|
+
if box_type == b"moov" {
|
|
3684
|
+
if body_size > 64 * 1024 * 1024 {
|
|
3685
|
+
// Implausibly large metadata box — bail rather than buffer
|
|
3686
|
+
// tens of MB; treat as unparseable rather than "no audio".
|
|
3687
|
+
eprintln!("[clips-tray] mp4_has_audio_track: moov box implausibly large ({body_size} bytes)");
|
|
3688
|
+
return None;
|
|
3689
|
+
}
|
|
3690
|
+
let mut body = vec![0u8; body_size as usize];
|
|
3691
|
+
if f.read_exact(&mut body).is_err() {
|
|
3692
|
+
return None;
|
|
3693
|
+
}
|
|
3694
|
+
break body;
|
|
3695
|
+
}
|
|
3696
|
+
let offset = match i64::try_from(body_size) {
|
|
3697
|
+
Ok(v) => v,
|
|
3698
|
+
Err(_) => return Some(false),
|
|
3699
|
+
};
|
|
3700
|
+
if f.seek(SeekFrom::Current(offset)).is_err() {
|
|
3701
|
+
return None;
|
|
3702
|
+
}
|
|
3703
|
+
};
|
|
3704
|
+
|
|
3705
|
+
// Linear scan for `hdlr` boxes within the buffered moov body. hdlr
|
|
3706
|
+
// layout: size(4) + type(4) + version(1) + flags(3) + pre_defined(4) +
|
|
3707
|
+
// handler_type(4) — so the 4-byte handler type sits 16 bytes after the
|
|
3708
|
+
// box header starts (8 bytes header + 8 bytes version/flags/pre_defined).
|
|
3709
|
+
let mut i = 0usize;
|
|
3710
|
+
while i + 8 <= moov.len() {
|
|
3711
|
+
let box_type = &moov[i + 4..i + 8];
|
|
3712
|
+
if box_type == b"hdlr" && i + 20 <= moov.len() {
|
|
3713
|
+
if &moov[i + 16..i + 20] == b"soun" {
|
|
3714
|
+
return Some(true);
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
// Advance by one byte at a time rather than by parsed box size:
|
|
3718
|
+
// `hdlr`/`mdia`/`trak` box sizes aren't otherwise tracked here, and
|
|
3719
|
+
// scanning byte-by-byte for the 4-byte `hdlr` tag is simple, safe
|
|
3720
|
+
// (can't run past the buffer), and cheap given moov's small size.
|
|
3721
|
+
i += 1;
|
|
3722
|
+
}
|
|
3723
|
+
Some(false)
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
#[derive(Clone, Copy, Debug)]
|
|
3727
|
+
struct AudioSignalProbe {
|
|
3728
|
+
mean_volume_db: Option<f64>,
|
|
3729
|
+
max_volume_db: Option<f64>,
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
impl AudioSignalProbe {
|
|
3733
|
+
fn has_audible_signal(self) -> bool {
|
|
3734
|
+
let peak_ok = self
|
|
3735
|
+
.max_volume_db
|
|
3736
|
+
.map(|value| value.is_finite() && value >= AUDIO_SIGNAL_MIN_MAX_VOLUME_DB)
|
|
3737
|
+
.unwrap_or(false);
|
|
3738
|
+
let mean_ok = self
|
|
3739
|
+
.mean_volume_db
|
|
3740
|
+
.map(|value| value.is_finite() && value >= AUDIO_SIGNAL_MIN_MEAN_VOLUME_DB)
|
|
3741
|
+
.unwrap_or(true);
|
|
3742
|
+
peak_ok && mean_ok
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
fn summary(self) -> String {
|
|
3746
|
+
fn fmt(value: Option<f64>) -> String {
|
|
3747
|
+
match value {
|
|
3748
|
+
Some(value) if value.is_finite() => format!("{value:.1} dB"),
|
|
3749
|
+
Some(value) if value.is_infinite() && value.is_sign_negative() => "-inf dB".into(),
|
|
3750
|
+
Some(_) => "non-finite".into(),
|
|
3751
|
+
None => "unknown".into(),
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
format!(
|
|
3755
|
+
"mean_volume={} max_volume={} mean_floor={:.1} dB peak_floor={:.1} dB",
|
|
3756
|
+
fmt(self.mean_volume_db),
|
|
3757
|
+
fmt(self.max_volume_db),
|
|
3758
|
+
AUDIO_SIGNAL_MIN_MEAN_VOLUME_DB,
|
|
3759
|
+
AUDIO_SIGNAL_MIN_MAX_VOLUME_DB
|
|
3760
|
+
)
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
fn parse_ffmpeg_volume_db(stderr: &str, label: &str) -> Option<f64> {
|
|
3765
|
+
stderr.lines().rev().find_map(|line| {
|
|
3766
|
+
let (_, value) = line.split_once(label)?;
|
|
3767
|
+
let value = value.trim().strip_suffix(" dB").unwrap_or(value.trim());
|
|
3768
|
+
if value == "-inf" {
|
|
3769
|
+
Some(f64::NEG_INFINITY)
|
|
3770
|
+
} else {
|
|
3771
|
+
value.parse::<f64>().ok()
|
|
3772
|
+
}
|
|
3773
|
+
})
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
fn audio_signal_probe_with_ffmpeg(
|
|
3777
|
+
ffmpeg_path: &str,
|
|
3778
|
+
source: &Path,
|
|
3779
|
+
) -> Result<AudioSignalProbe, String> {
|
|
3780
|
+
let child = Command::new(ffmpeg_path)
|
|
3781
|
+
.arg("-hide_banner")
|
|
3782
|
+
.arg("-nostdin")
|
|
3783
|
+
.arg("-i")
|
|
3784
|
+
.arg(source)
|
|
3785
|
+
.arg("-map")
|
|
3786
|
+
.arg("0:a:0")
|
|
3787
|
+
.arg("-af")
|
|
3788
|
+
.arg("volumedetect")
|
|
3789
|
+
.arg("-f")
|
|
3790
|
+
.arg("null")
|
|
3791
|
+
.arg("-")
|
|
3792
|
+
.stdout(Stdio::null())
|
|
3793
|
+
.stderr(Stdio::piped())
|
|
3794
|
+
.spawn()
|
|
3795
|
+
.map_err(|e| format!("ffmpeg audio probe spawn failed: {e}"))?;
|
|
3796
|
+
let stderr = wait_for_child_collect_stderr(
|
|
3797
|
+
child,
|
|
3798
|
+
FFMPEG_AUDIO_PROBE_TIMEOUT,
|
|
3799
|
+
"ffmpeg",
|
|
3800
|
+
"checking recording audio",
|
|
3801
|
+
)?;
|
|
3802
|
+
Ok(AudioSignalProbe {
|
|
3803
|
+
mean_volume_db: parse_ffmpeg_volume_db(&stderr, "mean_volume:"),
|
|
3804
|
+
max_volume_db: parse_ffmpeg_volume_db(&stderr, "max_volume:"),
|
|
3805
|
+
})
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
fn describe_audio_signal_probe(probe: Result<AudioSignalProbe, String>) -> String {
|
|
3809
|
+
match probe {
|
|
3810
|
+
Ok(probe) => probe.summary(),
|
|
3811
|
+
Err(err) => format!("probe failed: {err}"),
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
fn verify_prepared_audio_signal(
|
|
3816
|
+
ffmpeg_path: &str,
|
|
3817
|
+
candidate: &Path,
|
|
3818
|
+
original: &PreparedRecordingFile,
|
|
3819
|
+
source_path: &Path,
|
|
3820
|
+
label: &str,
|
|
3821
|
+
) -> Result<Option<PreparedRecordingFile>, String> {
|
|
3822
|
+
let candidate_probe = match audio_signal_probe_with_ffmpeg(ffmpeg_path, candidate) {
|
|
3823
|
+
Ok(probe) => probe,
|
|
3824
|
+
Err(err) => {
|
|
3825
|
+
eprintln!(
|
|
3826
|
+
"[clips-tray] audio signal probe failed for {label}; accepting candidate: {err}"
|
|
3827
|
+
);
|
|
3828
|
+
return Ok(None);
|
|
3829
|
+
}
|
|
3830
|
+
};
|
|
3831
|
+
if candidate_probe.has_audible_signal() {
|
|
3832
|
+
eprintln!(
|
|
3833
|
+
"[clips-tray] audio signal probe ok for {label}: {}",
|
|
3834
|
+
candidate_probe.summary()
|
|
3835
|
+
);
|
|
3836
|
+
return Ok(None);
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
let source_probe = audio_signal_probe_with_ffmpeg(ffmpeg_path, source_path);
|
|
3840
|
+
let source_has_signal = source_probe
|
|
3841
|
+
.as_ref()
|
|
3842
|
+
.ok()
|
|
3843
|
+
.copied()
|
|
3844
|
+
.map(AudioSignalProbe::has_audible_signal)
|
|
3845
|
+
.unwrap_or(false);
|
|
3846
|
+
let source_summary = describe_audio_signal_probe(source_probe);
|
|
3847
|
+
if source_has_signal {
|
|
3848
|
+
eprintln!(
|
|
3849
|
+
"[clips-tray] AUDIO QUIET AFTER PREPARE: {label} is effectively silent ({}) but original has signal ({source_summary}) — uploading original instead",
|
|
3850
|
+
candidate_probe.summary()
|
|
3851
|
+
);
|
|
3852
|
+
return Ok(Some(original.clone()));
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
eprintln!(
|
|
3856
|
+
"[clips-tray] AUDIO CAPTURE TOO QUIET: prepared {label} has an audio track but no usable signal ({}) and original was not usable either ({source_summary})",
|
|
3857
|
+
candidate_probe.summary()
|
|
3858
|
+
);
|
|
3859
|
+
Err(format!(
|
|
3860
|
+
"Recorded file contains an audio track, but the captured audio is too quiet to publish ({})",
|
|
3861
|
+
candidate_probe.summary()
|
|
3862
|
+
))
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3175
3865
|
fn prepare_recording_file(
|
|
3176
3866
|
app: &AppHandle,
|
|
3177
3867
|
path: &Path,
|
|
@@ -3180,7 +3870,7 @@ fn prepare_recording_file(
|
|
|
3180
3870
|
height: Option<u32>,
|
|
3181
3871
|
duration_ms: Option<u128>,
|
|
3182
3872
|
has_audio: bool,
|
|
3183
|
-
|
|
3873
|
+
mic_captured_audio: bool,
|
|
3184
3874
|
) -> Result<PreparedRecordingFile, String> {
|
|
3185
3875
|
let metadata = std::fs::metadata(path).map_err(|e| {
|
|
3186
3876
|
let diag = describe_recording_path(path);
|
|
@@ -3216,10 +3906,18 @@ fn prepare_recording_file(
|
|
|
3216
3906
|
mime_type: mime_type.to_string(),
|
|
3217
3907
|
bytes: source_bytes,
|
|
3218
3908
|
temporary: false,
|
|
3909
|
+
locally_transcoded: false,
|
|
3219
3910
|
};
|
|
3220
3911
|
|
|
3221
3912
|
let mut smallest_attempt_bytes: Option<u64> = None;
|
|
3222
3913
|
let ffmpeg_path = resolve_ffmpeg_path();
|
|
3914
|
+
if has_audio {
|
|
3915
|
+
if let Some(ffmpeg_path) = ffmpeg_path.as_deref() {
|
|
3916
|
+
let raw_summary =
|
|
3917
|
+
describe_audio_signal_probe(audio_signal_probe_with_ffmpeg(ffmpeg_path, path));
|
|
3918
|
+
eprintln!("[clips-tray] raw recording audio signal before prepare: {raw_summary}");
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3223
3921
|
|
|
3224
3922
|
if !COMPRESSION_ENABLED || source_bytes < TRANSCODE_THRESHOLD_BYTES {
|
|
3225
3923
|
if has_audio {
|
|
@@ -3230,15 +3928,42 @@ fn prepare_recording_file(
|
|
|
3230
3928
|
ffmpeg_path,
|
|
3231
3929
|
path,
|
|
3232
3930
|
&normalized_path,
|
|
3233
|
-
|
|
3931
|
+
mic_captured_audio,
|
|
3932
|
+
mic_captured_audio,
|
|
3234
3933
|
) {
|
|
3235
3934
|
Ok(()) => {
|
|
3236
3935
|
let normalized_bytes = std::fs::metadata(&normalized_path)
|
|
3237
3936
|
.map_err(|e| format!("normalized recording file missing: {e}"))?
|
|
3238
3937
|
.len();
|
|
3239
3938
|
if normalized_bytes > 0 && normalized_bytes <= max_upload_bytes() {
|
|
3939
|
+
if mp4_has_audio_track(&normalized_path) == Some(false) {
|
|
3940
|
+
let _ = std::fs::remove_file(&normalized_path);
|
|
3941
|
+
eprintln!(
|
|
3942
|
+
"[clips-tray] AUDIO LOST: ffmpeg audio normalization dropped the audio track \
|
|
3943
|
+
(source had audio, normalized output did not) — uploading original instead of \
|
|
3944
|
+
a silent smaller file"
|
|
3945
|
+
);
|
|
3946
|
+
return Ok(original);
|
|
3947
|
+
}
|
|
3948
|
+
match verify_prepared_audio_signal(
|
|
3949
|
+
ffmpeg_path,
|
|
3950
|
+
&normalized_path,
|
|
3951
|
+
&original,
|
|
3952
|
+
path,
|
|
3953
|
+
"audio-normalized recording",
|
|
3954
|
+
) {
|
|
3955
|
+
Ok(Some(fallback)) => {
|
|
3956
|
+
let _ = std::fs::remove_file(&normalized_path);
|
|
3957
|
+
return Ok(fallback);
|
|
3958
|
+
}
|
|
3959
|
+
Ok(None) => {}
|
|
3960
|
+
Err(err) => {
|
|
3961
|
+
let _ = std::fs::remove_file(&normalized_path);
|
|
3962
|
+
return Err(err);
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3240
3965
|
eprintln!(
|
|
3241
|
-
"[clips-tray] native recording audio
|
|
3966
|
+
"[clips-tray] native recording audio optimized with ffmpeg: {} -> {} bytes",
|
|
3242
3967
|
source_bytes, normalized_bytes
|
|
3243
3968
|
);
|
|
3244
3969
|
return Ok(PreparedRecordingFile {
|
|
@@ -3246,6 +3971,7 @@ fn prepare_recording_file(
|
|
|
3246
3971
|
mime_type: MP4_RECORDING_MIME_TYPE.to_string(),
|
|
3247
3972
|
bytes: normalized_bytes,
|
|
3248
3973
|
temporary: true,
|
|
3974
|
+
locally_transcoded: false,
|
|
3249
3975
|
});
|
|
3250
3976
|
}
|
|
3251
3977
|
let _ = std::fs::remove_file(&normalized_path);
|
|
@@ -3268,7 +3994,7 @@ fn prepare_recording_file(
|
|
|
3268
3994
|
return Ok(original);
|
|
3269
3995
|
}
|
|
3270
3996
|
|
|
3271
|
-
if let Some(ffmpeg_path) = ffmpeg_path {
|
|
3997
|
+
if let Some(ffmpeg_path) = ffmpeg_path.as_deref() {
|
|
3272
3998
|
let presets = ffmpeg_transcode_presets(width, height, source_bytes, duration_ms);
|
|
3273
3999
|
for (index, preset) in presets.iter().enumerate() {
|
|
3274
4000
|
emit_native_upload_progress(
|
|
@@ -3281,7 +4007,7 @@ fn prepare_recording_file(
|
|
|
3281
4007
|
let compressed_path = compressed_recording_path(path);
|
|
3282
4008
|
let _ = std::fs::remove_file(&compressed_path);
|
|
3283
4009
|
match transcode_with_ffmpeg(
|
|
3284
|
-
|
|
4010
|
+
ffmpeg_path,
|
|
3285
4011
|
path,
|
|
3286
4012
|
&compressed_path,
|
|
3287
4013
|
preset,
|
|
@@ -3289,7 +4015,8 @@ fn prepare_recording_file(
|
|
|
3289
4015
|
height,
|
|
3290
4016
|
duration_ms,
|
|
3291
4017
|
has_audio,
|
|
3292
|
-
|
|
4018
|
+
mic_captured_audio,
|
|
4019
|
+
mic_captured_audio,
|
|
3293
4020
|
) {
|
|
3294
4021
|
Ok(()) => {
|
|
3295
4022
|
let compressed_bytes = std::fs::metadata(&compressed_path)
|
|
@@ -3303,6 +4030,43 @@ fn prepare_recording_file(
|
|
|
3303
4030
|
);
|
|
3304
4031
|
continue;
|
|
3305
4032
|
}
|
|
4033
|
+
if has_audio && mp4_has_audio_track(&compressed_path) == Some(false) {
|
|
4034
|
+
let _ = std::fs::remove_file(&compressed_path);
|
|
4035
|
+
eprintln!(
|
|
4036
|
+
"[clips-tray] AUDIO LOST: ffmpeg {} dropped the audio track (source had \
|
|
4037
|
+
audio, compressed output did not) — rejecting this preset rather than \
|
|
4038
|
+
uploading a silent smaller file",
|
|
4039
|
+
preset.label
|
|
4040
|
+
);
|
|
4041
|
+
continue;
|
|
4042
|
+
}
|
|
4043
|
+
if has_audio {
|
|
4044
|
+
match verify_prepared_audio_signal(
|
|
4045
|
+
ffmpeg_path,
|
|
4046
|
+
&compressed_path,
|
|
4047
|
+
&original,
|
|
4048
|
+
path,
|
|
4049
|
+
preset.label,
|
|
4050
|
+
) {
|
|
4051
|
+
Ok(Some(fallback)) => {
|
|
4052
|
+
let _ = std::fs::remove_file(&compressed_path);
|
|
4053
|
+
if fallback.bytes <= max_upload_bytes() {
|
|
4054
|
+
return Ok(fallback);
|
|
4055
|
+
}
|
|
4056
|
+
eprintln!(
|
|
4057
|
+
"[clips-tray] original recording has usable audio but is too large to upload without compression ({}); rejecting silent compressed preset {}",
|
|
4058
|
+
format_mb(fallback.bytes),
|
|
4059
|
+
preset.label
|
|
4060
|
+
);
|
|
4061
|
+
continue;
|
|
4062
|
+
}
|
|
4063
|
+
Ok(None) => {}
|
|
4064
|
+
Err(err) => {
|
|
4065
|
+
let _ = std::fs::remove_file(&compressed_path);
|
|
4066
|
+
return Err(err);
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
3306
4070
|
smallest_attempt_bytes = Some(
|
|
3307
4071
|
smallest_attempt_bytes
|
|
3308
4072
|
.map(|smallest| smallest.min(compressed_bytes))
|
|
@@ -3348,6 +4112,7 @@ fn prepare_recording_file(
|
|
|
3348
4112
|
mime_type: MP4_RECORDING_MIME_TYPE.to_string(),
|
|
3349
4113
|
bytes: compressed_bytes,
|
|
3350
4114
|
temporary: true,
|
|
4115
|
+
locally_transcoded: true,
|
|
3351
4116
|
});
|
|
3352
4117
|
}
|
|
3353
4118
|
Err(err) => {
|
|
@@ -3385,6 +4150,44 @@ fn prepare_recording_file(
|
|
|
3385
4150
|
eprintln!("[clips-tray] avconvert produced an empty file with {preset}");
|
|
3386
4151
|
continue;
|
|
3387
4152
|
}
|
|
4153
|
+
if has_audio && mp4_has_audio_track(&compressed_path) == Some(false) {
|
|
4154
|
+
let _ = std::fs::remove_file(&compressed_path);
|
|
4155
|
+
eprintln!(
|
|
4156
|
+
"[clips-tray] AUDIO LOST: avconvert {preset} dropped the audio track \
|
|
4157
|
+
(source had audio, compressed output did not) — rejecting this preset \
|
|
4158
|
+
rather than uploading a silent smaller file"
|
|
4159
|
+
);
|
|
4160
|
+
continue;
|
|
4161
|
+
}
|
|
4162
|
+
if has_audio {
|
|
4163
|
+
if let Some(ffmpeg_probe_path) = ffmpeg_path.as_deref() {
|
|
4164
|
+
match verify_prepared_audio_signal(
|
|
4165
|
+
ffmpeg_probe_path,
|
|
4166
|
+
&compressed_path,
|
|
4167
|
+
&original,
|
|
4168
|
+
path,
|
|
4169
|
+
preset,
|
|
4170
|
+
) {
|
|
4171
|
+
Ok(Some(fallback)) => {
|
|
4172
|
+
let _ = std::fs::remove_file(&compressed_path);
|
|
4173
|
+
if fallback.bytes <= max_upload_bytes() {
|
|
4174
|
+
return Ok(fallback);
|
|
4175
|
+
}
|
|
4176
|
+
eprintln!(
|
|
4177
|
+
"[clips-tray] original recording has usable audio but is too large to upload without compression ({}); rejecting silent avconvert preset {}",
|
|
4178
|
+
format_mb(fallback.bytes),
|
|
4179
|
+
preset
|
|
4180
|
+
);
|
|
4181
|
+
continue;
|
|
4182
|
+
}
|
|
4183
|
+
Ok(None) => {}
|
|
4184
|
+
Err(err) => {
|
|
4185
|
+
let _ = std::fs::remove_file(&compressed_path);
|
|
4186
|
+
return Err(err);
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
3388
4191
|
smallest_attempt_bytes = Some(
|
|
3389
4192
|
smallest_attempt_bytes
|
|
3390
4193
|
.map(|smallest| smallest.min(compressed_bytes))
|
|
@@ -3430,6 +4233,7 @@ fn prepare_recording_file(
|
|
|
3430
4233
|
mime_type: MP4_RECORDING_MIME_TYPE.to_string(),
|
|
3431
4234
|
bytes: compressed_bytes,
|
|
3432
4235
|
temporary: true,
|
|
4236
|
+
locally_transcoded: true,
|
|
3433
4237
|
});
|
|
3434
4238
|
}
|
|
3435
4239
|
Err(err) => {
|
|
@@ -3486,7 +4290,7 @@ struct FfmpegTranscodePreset {
|
|
|
3486
4290
|
}
|
|
3487
4291
|
|
|
3488
4292
|
/// Maximum total bytes a recording upload may be. Overridable per-deployment
|
|
3489
|
-
/// with the `CLIPS_MAX_UPLOAD_BYTES` env var; falls back to
|
|
4293
|
+
/// with the `CLIPS_MAX_UPLOAD_BYTES` env var; falls back to `DEFAULT_MAX_UPLOAD_BYTES` (2 GB).
|
|
3490
4294
|
fn max_upload_bytes() -> u64 {
|
|
3491
4295
|
std::env::var("CLIPS_MAX_UPLOAD_BYTES")
|
|
3492
4296
|
.ok()
|
|
@@ -3609,6 +4413,7 @@ fn normalize_audio_with_ffmpeg(
|
|
|
3609
4413
|
source: &Path,
|
|
3610
4414
|
output: &Path,
|
|
3611
4415
|
downmix_audio: bool,
|
|
4416
|
+
denoise_audio: bool,
|
|
3612
4417
|
) -> Result<(), String> {
|
|
3613
4418
|
let audio_bitrate = format!("{NORMALIZED_AUDIO_BITRATE_KBPS}k");
|
|
3614
4419
|
let mut command = Command::new(ffmpeg_path);
|
|
@@ -3630,7 +4435,7 @@ fn normalize_audio_with_ffmpeg(
|
|
|
3630
4435
|
.arg("-b:a")
|
|
3631
4436
|
.arg(audio_bitrate)
|
|
3632
4437
|
.arg("-af")
|
|
3633
|
-
.arg(audio_filter_chain(downmix_audio))
|
|
4438
|
+
.arg(audio_filter_chain(downmix_audio, denoise_audio))
|
|
3634
4439
|
.arg("-ac")
|
|
3635
4440
|
.arg("2")
|
|
3636
4441
|
.arg("-ar")
|
|
@@ -3694,6 +4499,7 @@ fn transcode_with_ffmpeg(
|
|
|
3694
4499
|
duration_ms: Option<u128>,
|
|
3695
4500
|
normalize_audio: bool,
|
|
3696
4501
|
downmix_audio: bool,
|
|
4502
|
+
denoise_audio: bool,
|
|
3697
4503
|
) -> Result<(), String> {
|
|
3698
4504
|
let mut command = Command::new(ffmpeg_path);
|
|
3699
4505
|
command
|
|
@@ -3717,7 +4523,9 @@ fn transcode_with_ffmpeg(
|
|
|
3717
4523
|
}
|
|
3718
4524
|
|
|
3719
4525
|
if normalize_audio {
|
|
3720
|
-
command
|
|
4526
|
+
command
|
|
4527
|
+
.arg("-af")
|
|
4528
|
+
.arg(audio_filter_chain(downmix_audio, denoise_audio));
|
|
3721
4529
|
}
|
|
3722
4530
|
|
|
3723
4531
|
let duration_rate_limit =
|
|
@@ -3790,19 +4598,25 @@ fn transcode_with_avconvert(source: &Path, output: &Path, preset: &str) -> Resul
|
|
|
3790
4598
|
}
|
|
3791
4599
|
|
|
3792
4600
|
fn wait_for_transcode_child(
|
|
3793
|
-
|
|
4601
|
+
child: Child,
|
|
3794
4602
|
timeout: Duration,
|
|
3795
4603
|
tool_name: &str,
|
|
3796
4604
|
) -> Result<(), String> {
|
|
4605
|
+
wait_for_child_collect_stderr(child, timeout, tool_name, "compressing recording").map(|_| ())
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
fn wait_for_child_collect_stderr(
|
|
4609
|
+
mut child: Child,
|
|
4610
|
+
timeout: Duration,
|
|
4611
|
+
tool_name: &str,
|
|
4612
|
+
action: &str,
|
|
4613
|
+
) -> Result<String, String> {
|
|
3797
4614
|
let deadline = Instant::now() + timeout;
|
|
3798
4615
|
loop {
|
|
3799
4616
|
if let Some(status) = child
|
|
3800
4617
|
.try_wait()
|
|
3801
4618
|
.map_err(|e| format!("{tool_name} wait failed: {e}"))?
|
|
3802
4619
|
{
|
|
3803
|
-
if status.success() {
|
|
3804
|
-
return Ok(());
|
|
3805
|
-
}
|
|
3806
4620
|
let mut stderr = String::new();
|
|
3807
4621
|
if let Some(mut pipe) = child.stderr.take() {
|
|
3808
4622
|
let _ = pipe.read_to_string(&mut stderr);
|
|
@@ -3816,12 +4630,16 @@ fn wait_for_transcode_child(
|
|
|
3816
4630
|
.rev()
|
|
3817
4631
|
.collect::<Vec<_>>()
|
|
3818
4632
|
.join("\n");
|
|
3819
|
-
return
|
|
4633
|
+
return if status.success() {
|
|
4634
|
+
Ok(stderr)
|
|
4635
|
+
} else {
|
|
4636
|
+
Err(format!("{tool_name} exited with {status}: {}", tail.trim()))
|
|
4637
|
+
};
|
|
3820
4638
|
}
|
|
3821
4639
|
if Instant::now() >= deadline {
|
|
3822
4640
|
let _ = child.kill();
|
|
3823
4641
|
let _ = child.wait();
|
|
3824
|
-
return Err(format!("{tool_name} timed out while
|
|
4642
|
+
return Err(format!("{tool_name} timed out while {action}"));
|
|
3825
4643
|
}
|
|
3826
4644
|
std::thread::sleep(Duration::from_millis(250));
|
|
3827
4645
|
}
|
|
@@ -3834,6 +4652,26 @@ fn wait_for_transcode_child(
|
|
|
3834
4652
|
/// re-encoding, so concat is roughly disk-IO bound. Called from
|
|
3835
4653
|
/// `consolidate_segments_into_path` after every segment has been
|
|
3836
4654
|
/// finalized by `stop_native_recording(_, wait_for_finalize=true)`.
|
|
4655
|
+
fn validate_recording_segment_file(path: &Path) -> Result<(), String> {
|
|
4656
|
+
match std::fs::metadata(path) {
|
|
4657
|
+
Ok(meta) if meta.len() > 0 => {}
|
|
4658
|
+
Ok(_) => return Err(format!("recording segment is empty: {}", path.display())),
|
|
4659
|
+
Err(err) => {
|
|
4660
|
+
return Err(format!(
|
|
4661
|
+
"recording segment is missing or unreadable: {} ({err})",
|
|
4662
|
+
path.display()
|
|
4663
|
+
));
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
if mp4_has_moov(path) == Some(false) {
|
|
4667
|
+
return Err(format!(
|
|
4668
|
+
"recording segment is missing playback metadata: {}",
|
|
4669
|
+
path.display()
|
|
4670
|
+
));
|
|
4671
|
+
}
|
|
4672
|
+
Ok(())
|
|
4673
|
+
}
|
|
4674
|
+
|
|
3837
4675
|
#[cfg(target_os = "macos")]
|
|
3838
4676
|
fn concat_mp4_segments(segments: &[PathBuf], output: &Path) -> Result<(), String> {
|
|
3839
4677
|
use std::ffi::CString;
|
|
@@ -3995,18 +4833,7 @@ fn concat_mp4_segments(segments: &[PathBuf], output: &Path) -> Result<(), String
|
|
|
3995
4833
|
let mut appended_any = false;
|
|
3996
4834
|
|
|
3997
4835
|
for path in segments {
|
|
3998
|
-
|
|
3999
|
-
// that fired before the segment captured a single sample).
|
|
4000
|
-
match std::fs::metadata(path) {
|
|
4001
|
-
Ok(meta) if meta.len() > 0 => {}
|
|
4002
|
-
_ => {
|
|
4003
|
-
eprintln!(
|
|
4004
|
-
"[clips-tray] concat: skipping empty/missing segment {}",
|
|
4005
|
-
path.display()
|
|
4006
|
-
);
|
|
4007
|
-
continue;
|
|
4008
|
-
}
|
|
4009
|
-
}
|
|
4836
|
+
validate_recording_segment_file(path)?;
|
|
4010
4837
|
let url = file_url(path)
|
|
4011
4838
|
.ok_or_else(|| format!("could not build NSURL for {}", path.display()))?;
|
|
4012
4839
|
let asset: *mut AnyObject = msg_send![asset_cls, URLAssetWithURL: &*url, options: std::ptr::null::<AnyObject>()];
|
|
@@ -4018,11 +4845,10 @@ fn concat_mp4_segments(segments: &[PathBuf], output: &Path) -> Result<(), String
|
|
|
4018
4845
|
}
|
|
4019
4846
|
let duration: CMTime = msg_send![asset, duration];
|
|
4020
4847
|
if duration.flags & 1 == 0 || duration.timescale == 0 || duration.value <= 0 {
|
|
4021
|
-
|
|
4022
|
-
"
|
|
4848
|
+
return Err(format!(
|
|
4849
|
+
"recording segment has invalid duration: {}",
|
|
4023
4850
|
path.display()
|
|
4024
|
-
);
|
|
4025
|
-
continue;
|
|
4851
|
+
));
|
|
4026
4852
|
}
|
|
4027
4853
|
let range = CMTimeRange {
|
|
4028
4854
|
start: CM_TIME_ZERO,
|
|
@@ -4127,3 +4953,335 @@ fn concat_mp4_segments(segments: &[PathBuf], output: &Path) -> Result<(), String
|
|
|
4127
4953
|
|
|
4128
4954
|
Ok(())
|
|
4129
4955
|
}
|
|
4956
|
+
|
|
4957
|
+
#[cfg(test)]
|
|
4958
|
+
mod audio_track_probe_tests {
|
|
4959
|
+
use super::{
|
|
4960
|
+
audio_filter_chain, mp4_has_audio_track, parse_ffmpeg_volume_db, AudioSignalProbe,
|
|
4961
|
+
AUDIO_DENOISE_FILTER, AUDIO_DOWNMIX_FILTER, AUDIO_LOUDNESS_FILTER,
|
|
4962
|
+
};
|
|
4963
|
+
use std::io::Write;
|
|
4964
|
+
|
|
4965
|
+
/// Append an ISO BMFF box: 4-byte big-endian size (header + body) then
|
|
4966
|
+
/// the 4-byte type tag, then the raw body bytes.
|
|
4967
|
+
fn push_box(buf: &mut Vec<u8>, box_type: &[u8; 4], body: &[u8]) {
|
|
4968
|
+
let size = (8 + body.len()) as u32;
|
|
4969
|
+
buf.extend_from_slice(&size.to_be_bytes());
|
|
4970
|
+
buf.extend_from_slice(box_type);
|
|
4971
|
+
buf.extend_from_slice(body);
|
|
4972
|
+
}
|
|
4973
|
+
|
|
4974
|
+
/// Build a minimal `hdlr` box body for the given handler type (e.g.
|
|
4975
|
+
/// `soun` or `vide`): version(1) + flags(3) + pre_defined(4) +
|
|
4976
|
+
/// handler_type(4), zero-padded further like a real hdlr's trailing
|
|
4977
|
+
/// name/reserved fields.
|
|
4978
|
+
fn hdlr_body(handler_type: &[u8; 4]) -> Vec<u8> {
|
|
4979
|
+
let mut body = vec![0u8; 8]; // version+flags+pre_defined
|
|
4980
|
+
body.extend_from_slice(handler_type);
|
|
4981
|
+
body.extend_from_slice(&[0u8; 4]); // trailing reserved/name padding
|
|
4982
|
+
body
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4985
|
+
fn write_temp_mp4(bytes: &[u8]) -> std::path::PathBuf {
|
|
4986
|
+
let mut path = std::env::temp_dir();
|
|
4987
|
+
path.push(format!(
|
|
4988
|
+
"clips-audio-probe-test-{}-{}.mp4",
|
|
4989
|
+
std::process::id(),
|
|
4990
|
+
std::time::SystemTime::now()
|
|
4991
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
4992
|
+
.unwrap()
|
|
4993
|
+
.as_nanos()
|
|
4994
|
+
));
|
|
4995
|
+
let mut f = std::fs::File::create(&path).unwrap();
|
|
4996
|
+
f.write_all(bytes).unwrap();
|
|
4997
|
+
path
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
#[test]
|
|
5001
|
+
fn builds_native_audio_filter_chain_for_mic_noise_reduction() {
|
|
5002
|
+
assert_eq!(audio_filter_chain(false, false), AUDIO_LOUDNESS_FILTER);
|
|
5003
|
+
assert_eq!(
|
|
5004
|
+
audio_filter_chain(false, true),
|
|
5005
|
+
format!("{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}")
|
|
5006
|
+
);
|
|
5007
|
+
assert_eq!(
|
|
5008
|
+
audio_filter_chain(true, true),
|
|
5009
|
+
format!("{AUDIO_DOWNMIX_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}")
|
|
5010
|
+
);
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
#[test]
|
|
5014
|
+
fn detects_audio_track_present() {
|
|
5015
|
+
let mut moov_body = Vec::new();
|
|
5016
|
+
// moov > trak > mdia > hdlr(soun)
|
|
5017
|
+
let mut mdia_body = Vec::new();
|
|
5018
|
+
push_box(&mut mdia_body, b"hdlr", &hdlr_body(b"soun"));
|
|
5019
|
+
let mut trak_body = Vec::new();
|
|
5020
|
+
push_box(&mut trak_body, b"mdia", &mdia_body);
|
|
5021
|
+
push_box(&mut moov_body, b"trak", &trak_body);
|
|
5022
|
+
|
|
5023
|
+
let mut file = Vec::new();
|
|
5024
|
+
push_box(&mut file, b"ftyp", b"isommp42");
|
|
5025
|
+
push_box(&mut file, b"moov", &moov_body);
|
|
5026
|
+
file.extend_from_slice(b"mdatSOMEFAKEVIDEODATA");
|
|
5027
|
+
|
|
5028
|
+
let path = write_temp_mp4(&file);
|
|
5029
|
+
assert_eq!(mp4_has_audio_track(&path), Some(true));
|
|
5030
|
+
let _ = std::fs::remove_file(&path);
|
|
5031
|
+
}
|
|
5032
|
+
|
|
5033
|
+
#[test]
|
|
5034
|
+
fn detects_video_only_output_as_missing_audio() {
|
|
5035
|
+
// Simulates exactly the bug: ffmpeg with `-map 0:a?` succeeds but
|
|
5036
|
+
// only writes a video (`vide`) handler track, no `soun` track.
|
|
5037
|
+
let mut moov_body = Vec::new();
|
|
5038
|
+
let mut mdia_body = Vec::new();
|
|
5039
|
+
push_box(&mut mdia_body, b"hdlr", &hdlr_body(b"vide"));
|
|
5040
|
+
let mut trak_body = Vec::new();
|
|
5041
|
+
push_box(&mut trak_body, b"mdia", &mdia_body);
|
|
5042
|
+
push_box(&mut moov_body, b"trak", &trak_body);
|
|
5043
|
+
|
|
5044
|
+
let mut file = Vec::new();
|
|
5045
|
+
push_box(&mut file, b"ftyp", b"isommp42");
|
|
5046
|
+
push_box(&mut file, b"moov", &moov_body);
|
|
5047
|
+
file.extend_from_slice(b"mdatSOMEFAKEVIDEODATA");
|
|
5048
|
+
|
|
5049
|
+
let path = write_temp_mp4(&file);
|
|
5050
|
+
assert_eq!(mp4_has_audio_track(&path), Some(false));
|
|
5051
|
+
let _ = std::fs::remove_file(&path);
|
|
5052
|
+
}
|
|
5053
|
+
|
|
5054
|
+
#[test]
|
|
5055
|
+
fn detects_audio_track_among_multiple_tracks() {
|
|
5056
|
+
// video trak first, then audio trak — order shouldn't matter.
|
|
5057
|
+
let mut video_mdia = Vec::new();
|
|
5058
|
+
push_box(&mut video_mdia, b"hdlr", &hdlr_body(b"vide"));
|
|
5059
|
+
let mut video_trak = Vec::new();
|
|
5060
|
+
push_box(&mut video_trak, b"mdia", &video_mdia);
|
|
5061
|
+
|
|
5062
|
+
let mut audio_mdia = Vec::new();
|
|
5063
|
+
push_box(&mut audio_mdia, b"hdlr", &hdlr_body(b"soun"));
|
|
5064
|
+
let mut audio_trak = Vec::new();
|
|
5065
|
+
push_box(&mut audio_trak, b"mdia", &audio_mdia);
|
|
5066
|
+
|
|
5067
|
+
let mut moov_body = Vec::new();
|
|
5068
|
+
push_box(&mut moov_body, b"trak", &video_trak);
|
|
5069
|
+
push_box(&mut moov_body, b"trak", &audio_trak);
|
|
5070
|
+
|
|
5071
|
+
let mut file = Vec::new();
|
|
5072
|
+
push_box(&mut file, b"ftyp", b"isommp42");
|
|
5073
|
+
push_box(&mut file, b"moov", &moov_body);
|
|
5074
|
+
file.extend_from_slice(b"mdatSOMEFAKEVIDEODATA");
|
|
5075
|
+
|
|
5076
|
+
let path = write_temp_mp4(&file);
|
|
5077
|
+
assert_eq!(mp4_has_audio_track(&path), Some(true));
|
|
5078
|
+
let _ = std::fs::remove_file(&path);
|
|
5079
|
+
}
|
|
5080
|
+
|
|
5081
|
+
#[test]
|
|
5082
|
+
fn missing_file_returns_none() {
|
|
5083
|
+
let path = std::env::temp_dir().join("clips-audio-probe-test-does-not-exist.mp4");
|
|
5084
|
+
assert_eq!(mp4_has_audio_track(&path), None);
|
|
5085
|
+
}
|
|
5086
|
+
|
|
5087
|
+
#[test]
|
|
5088
|
+
fn no_moov_box_returns_false() {
|
|
5089
|
+
let mut file = Vec::new();
|
|
5090
|
+
push_box(&mut file, b"ftyp", b"isommp42");
|
|
5091
|
+
file.extend_from_slice(b"mdatSOMEFAKEVIDEODATA");
|
|
5092
|
+
|
|
5093
|
+
let path = write_temp_mp4(&file);
|
|
5094
|
+
assert_eq!(mp4_has_audio_track(&path), Some(false));
|
|
5095
|
+
let _ = std::fs::remove_file(&path);
|
|
5096
|
+
}
|
|
5097
|
+
|
|
5098
|
+
#[test]
|
|
5099
|
+
fn parses_ffmpeg_volumedetect_output() {
|
|
5100
|
+
let stderr = "\
|
|
5101
|
+
[Parsed_volumedetect_0 @ 0x123] mean_volume: -74.0 dB
|
|
5102
|
+
[Parsed_volumedetect_0 @ 0x123] max_volume: -57.6 dB
|
|
5103
|
+
";
|
|
5104
|
+
assert_eq!(parse_ffmpeg_volume_db(stderr, "mean_volume:"), Some(-74.0));
|
|
5105
|
+
assert_eq!(parse_ffmpeg_volume_db(stderr, "max_volume:"), Some(-57.6));
|
|
5106
|
+
}
|
|
5107
|
+
|
|
5108
|
+
#[test]
|
|
5109
|
+
fn rejects_effectively_silent_prepared_audio() {
|
|
5110
|
+
let silent = AudioSignalProbe {
|
|
5111
|
+
mean_volume_db: Some(-74.0),
|
|
5112
|
+
max_volume_db: Some(-57.6),
|
|
5113
|
+
};
|
|
5114
|
+
let quiet_noise_peak = AudioSignalProbe {
|
|
5115
|
+
mean_volume_db: Some(-71.1),
|
|
5116
|
+
max_volume_db: Some(-47.4),
|
|
5117
|
+
};
|
|
5118
|
+
let audible = AudioSignalProbe {
|
|
5119
|
+
mean_volume_db: Some(-21.5),
|
|
5120
|
+
max_volume_db: Some(-1.4),
|
|
5121
|
+
};
|
|
5122
|
+
assert!(!silent.has_audible_signal());
|
|
5123
|
+
assert!(!quiet_noise_peak.has_audible_signal());
|
|
5124
|
+
assert!(audible.has_audible_signal());
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
|
|
5128
|
+
#[cfg(test)]
|
|
5129
|
+
mod segment_recovery_tests {
|
|
5130
|
+
use super::{
|
|
5131
|
+
recover_from_unusable_current_segment, validate_recording_segment_file,
|
|
5132
|
+
NativeFullscreenSession, RestartInfo, MP4_RECORDING_MIME_TYPE,
|
|
5133
|
+
};
|
|
5134
|
+
use std::io::Write;
|
|
5135
|
+
use std::path::PathBuf;
|
|
5136
|
+
use std::time::{Duration, Instant};
|
|
5137
|
+
|
|
5138
|
+
fn push_box(buf: &mut Vec<u8>, box_type: &[u8; 4], body: &[u8]) {
|
|
5139
|
+
let size = (8 + body.len()) as u32;
|
|
5140
|
+
buf.extend_from_slice(&size.to_be_bytes());
|
|
5141
|
+
buf.extend_from_slice(box_type);
|
|
5142
|
+
buf.extend_from_slice(body);
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
fn temp_path(name: &str) -> PathBuf {
|
|
5146
|
+
let mut path = std::env::temp_dir();
|
|
5147
|
+
path.push(format!(
|
|
5148
|
+
"clips-segment-recovery-{name}-{}-{}.mp4",
|
|
5149
|
+
std::process::id(),
|
|
5150
|
+
std::time::SystemTime::now()
|
|
5151
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
5152
|
+
.unwrap()
|
|
5153
|
+
.as_nanos()
|
|
5154
|
+
));
|
|
5155
|
+
path
|
|
5156
|
+
}
|
|
5157
|
+
|
|
5158
|
+
fn write_mp4(path: &PathBuf, has_moov: bool) {
|
|
5159
|
+
let mut file = Vec::new();
|
|
5160
|
+
push_box(&mut file, b"ftyp", b"isommp42");
|
|
5161
|
+
if has_moov {
|
|
5162
|
+
push_box(&mut file, b"moov", &[]);
|
|
5163
|
+
}
|
|
5164
|
+
file.extend_from_slice(b"mdatSOMEFAKEVIDEODATA");
|
|
5165
|
+
let mut f = std::fs::File::create(path).unwrap();
|
|
5166
|
+
f.write_all(&file).unwrap();
|
|
5167
|
+
}
|
|
5168
|
+
|
|
5169
|
+
fn test_session(segments: Vec<PathBuf>) -> NativeFullscreenSession {
|
|
5170
|
+
let now = Instant::now();
|
|
5171
|
+
NativeFullscreenSession {
|
|
5172
|
+
backend: None,
|
|
5173
|
+
path: segments
|
|
5174
|
+
.first()
|
|
5175
|
+
.cloned()
|
|
5176
|
+
.unwrap_or_else(|| temp_path("final")),
|
|
5177
|
+
mime_type: MP4_RECORDING_MIME_TYPE,
|
|
5178
|
+
started_at: now,
|
|
5179
|
+
width: Some(1280),
|
|
5180
|
+
height: Some(720),
|
|
5181
|
+
segments,
|
|
5182
|
+
paused_total: Duration::ZERO,
|
|
5183
|
+
current_segment_started_at: now.checked_sub(Duration::from_millis(250)).unwrap_or(now),
|
|
5184
|
+
lost_segment_duration: Duration::ZERO,
|
|
5185
|
+
lost_segment_count: 0,
|
|
5186
|
+
paused_at: None,
|
|
5187
|
+
restart: RestartInfo {
|
|
5188
|
+
safe_id: "test".to_string(),
|
|
5189
|
+
include_audio: true,
|
|
5190
|
+
capture_system_audio: false,
|
|
5191
|
+
mic_captured_in_file: false,
|
|
5192
|
+
mic_device_id: None,
|
|
5193
|
+
mic_device_label: None,
|
|
5194
|
+
segment_counter: 0,
|
|
5195
|
+
target_display_id: None,
|
|
5196
|
+
capture_region: None,
|
|
5197
|
+
},
|
|
5198
|
+
pending_recording_output: false,
|
|
5199
|
+
disk_monitor_stop: None,
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
|
|
5203
|
+
#[test]
|
|
5204
|
+
fn drops_unusable_last_segment_when_empty_recovery_allowed() {
|
|
5205
|
+
let good = temp_path("good");
|
|
5206
|
+
let bad = temp_path("bad");
|
|
5207
|
+
write_mp4(&good, true);
|
|
5208
|
+
write_mp4(&bad, false);
|
|
5209
|
+
|
|
5210
|
+
let mut session = test_session(vec![good.clone(), bad.clone()]);
|
|
5211
|
+
assert!(recover_from_unusable_current_segment(
|
|
5212
|
+
&mut session,
|
|
5213
|
+
"test pause",
|
|
5214
|
+
true,
|
|
5215
|
+
));
|
|
5216
|
+
assert_eq!(session.segments, vec![good.clone()]);
|
|
5217
|
+
assert_eq!(session.lost_segment_count, 1);
|
|
5218
|
+
assert!(session.lost_segment_duration > Duration::ZERO);
|
|
5219
|
+
assert!(!bad.exists());
|
|
5220
|
+
|
|
5221
|
+
let _ = std::fs::remove_file(good);
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5224
|
+
#[test]
|
|
5225
|
+
fn keeps_only_bad_segment_when_empty_recovery_disallowed() {
|
|
5226
|
+
let bad = temp_path("only-bad");
|
|
5227
|
+
write_mp4(&bad, false);
|
|
5228
|
+
|
|
5229
|
+
let mut session = test_session(vec![bad.clone()]);
|
|
5230
|
+
assert!(!recover_from_unusable_current_segment(
|
|
5231
|
+
&mut session,
|
|
5232
|
+
"final stop",
|
|
5233
|
+
false,
|
|
5234
|
+
));
|
|
5235
|
+
assert_eq!(session.segments, vec![bad.clone()]);
|
|
5236
|
+
assert_eq!(session.lost_segment_count, 0);
|
|
5237
|
+
assert!(bad.exists());
|
|
5238
|
+
|
|
5239
|
+
let _ = std::fs::remove_file(bad);
|
|
5240
|
+
}
|
|
5241
|
+
|
|
5242
|
+
#[test]
|
|
5243
|
+
fn keeps_playable_last_segment() {
|
|
5244
|
+
let first = temp_path("first");
|
|
5245
|
+
let second = temp_path("second");
|
|
5246
|
+
write_mp4(&first, true);
|
|
5247
|
+
write_mp4(&second, true);
|
|
5248
|
+
|
|
5249
|
+
let mut session = test_session(vec![first.clone(), second.clone()]);
|
|
5250
|
+
assert!(!recover_from_unusable_current_segment(
|
|
5251
|
+
&mut session,
|
|
5252
|
+
"test pause",
|
|
5253
|
+
true,
|
|
5254
|
+
));
|
|
5255
|
+
assert_eq!(session.segments, vec![first.clone(), second.clone()]);
|
|
5256
|
+
assert_eq!(session.lost_segment_count, 0);
|
|
5257
|
+
|
|
5258
|
+
let _ = std::fs::remove_file(first);
|
|
5259
|
+
let _ = std::fs::remove_file(second);
|
|
5260
|
+
}
|
|
5261
|
+
|
|
5262
|
+
#[test]
|
|
5263
|
+
fn concat_validation_rejects_bad_middle_segment() {
|
|
5264
|
+
let first = temp_path("middle-first");
|
|
5265
|
+
let bad = temp_path("middle-bad");
|
|
5266
|
+
let last = temp_path("middle-last");
|
|
5267
|
+
write_mp4(&first, true);
|
|
5268
|
+
write_mp4(&bad, false);
|
|
5269
|
+
write_mp4(&last, true);
|
|
5270
|
+
|
|
5271
|
+
let mut error = None;
|
|
5272
|
+
for path in [&first, &bad, &last] {
|
|
5273
|
+
if let Err(err) = validate_recording_segment_file(path) {
|
|
5274
|
+
error = Some(err);
|
|
5275
|
+
break;
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
|
|
5279
|
+
let err = error.expect("bad middle segment should fail concat validation");
|
|
5280
|
+
assert!(err.contains("missing playback metadata"));
|
|
5281
|
+
assert!(err.contains(bad.to_string_lossy().as_ref()));
|
|
5282
|
+
|
|
5283
|
+
let _ = std::fs::remove_file(first);
|
|
5284
|
+
let _ = std::fs::remove_file(bad);
|
|
5285
|
+
let _ = std::fs::remove_file(last);
|
|
5286
|
+
}
|
|
5287
|
+
}
|