@agent-native/core 0.79.17 → 0.79.19
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 +14 -0
- package/corpus/core/docs/content/file-uploads.md +11 -4
- package/corpus/core/docs/content/locales/ar-SA/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/de-DE/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/es-ES/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/fr-FR/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/hi-IN/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/ja-JP/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/ko-KR/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/pt-BR/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/zh-CN/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/locales/zh-TW/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/multi-app-workspace.md +3 -3
- package/corpus/core/docs/content/template-clips.md +14 -5
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/engine/builder-engine.ts +8 -3
- package/corpus/core/src/cli/mcp.ts +11 -6
- package/corpus/core/src/client/AssistantChat.tsx +6 -2
- package/corpus/core/src/client/chat/markdown-renderer.tsx +1 -0
- package/corpus/core/src/client/i18n.tsx +77 -65
- package/corpus/core/src/client/index.ts +7 -1
- package/corpus/core/src/client/org/OrgSwitcher.tsx +5 -5
- package/corpus/core/src/client/org/TeamPage.tsx +11 -3
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +216 -0
- package/corpus/core/src/client/settings/index.ts +5 -0
- package/corpus/core/src/file-upload/index.ts +1 -0
- package/corpus/core/src/file-upload/registry.ts +22 -6
- package/corpus/core/src/file-upload/types.ts +6 -1
- package/corpus/core/src/integrations/adapters/email.ts +26 -18
- package/corpus/core/src/integrations/adapters/slack.ts +8 -7
- package/corpus/core/src/integrations/adapters/telegram.ts +9 -11
- package/corpus/core/src/integrations/adapters/whatsapp.ts +12 -9
- package/corpus/core/src/integrations/plugin.ts +79 -13
- package/corpus/core/src/onboarding/default-steps.ts +6 -3
- package/corpus/core/src/onboarding/types.ts +1 -1
- package/corpus/core/src/org/handlers.ts +1 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +4 -7
- package/corpus/core/src/server/better-auth-instance.ts +19 -62
- package/corpus/core/src/server/core-routes-plugin.ts +93 -180
- package/corpus/core/src/server/create-server.ts +55 -116
- package/corpus/core/src/server/email-actions.ts +3 -6
- package/corpus/core/src/server/email.ts +52 -17
- package/corpus/core/src/server/index.ts +2 -1
- package/corpus/core/src/server/scoped-key-storage.ts +223 -0
- package/corpus/core/src/shared/agent-env.ts +3 -3
- package/corpus/core/src/sharing/actions/share-resource.ts +1 -1
- package/corpus/core/src/styles/agent-native.css +47 -3
- package/corpus/core/src/templates/workspace-root/AGENTS.md +3 -2
- package/corpus/core/src/templates/workspace-root/README.md +1 -1
- package/corpus/core/src/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
- package/corpus/core/src/terminal/pty-server.ts +3 -38
- package/corpus/core/src/triggers/dispatcher.ts +1 -2
- package/corpus/templates/.retired/issues/package.json +7 -0
- package/corpus/templates/.retired/meeting-notes/package.json +7 -0
- package/corpus/templates/.retired/recruiting/package.json +7 -0
- package/corpus/templates/.retired/scheduling/package.json +7 -0
- package/corpus/templates/.retired/voice/package.json +7 -0
- package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -18
- package/corpus/templates/analytics/app/global.css +15 -0
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +7 -1
- package/corpus/templates/analytics/app/i18n-data.ts +71 -2
- package/corpus/templates/analytics/app/pages/Ask.tsx +1 -1
- package/corpus/templates/analytics/app/pages/Settings.tsx +120 -73
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +0 -11
- package/corpus/templates/analytics/app/routes/catalog.tsx +33 -176
- package/corpus/templates/analytics/app/routes/team.tsx +3 -2
- package/corpus/templates/analytics/changelog/2026-06-26-dashboard-templates-are-tucked-into-settings-and-show-simple.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-the-ask-tab-uses-a-softer-dark-gray-canvas-while-sidebar-cha.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-26-the-sessions-page-shows-a-single-page-title-instead-of-repea.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +37 -28
- package/corpus/templates/assets/app/routes/settings.tsx +65 -43
- package/corpus/templates/assets/app/routes/team.tsx +2 -10
- package/corpus/templates/brain/app/routes/settings.tsx +390 -358
- package/corpus/templates/brain/app/routes/team.tsx +2 -8
- package/corpus/templates/calendar/actions/db-connect.ts +5 -29
- package/corpus/templates/calendar/app/components/CloudUpgrade.tsx +8 -51
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +2 -1
- package/corpus/templates/calendar/app/components/calendar/GoogleSetupWizard.tsx +4 -2
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/calendar/app/pages/Settings.tsx +304 -271
- package/corpus/templates/calendar/app/routes/_app.team.tsx +2 -17
- package/corpus/templates/calendar/server/handlers/google-auth.ts +22 -9
- package/corpus/templates/calendar/server/lib/booking-emails.ts +1 -1
- package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +1 -1
- package/corpus/templates/calendar/server/lib/google-calendar.ts +48 -28
- package/corpus/templates/chat/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/chat/app/hooks/use-navigation-state.ts +4 -2
- package/corpus/templates/chat/app/routes/settings.tsx +54 -54
- package/corpus/templates/chat/app/routes/team.tsx +2 -10
- package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +7 -2
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +1 -1
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +7 -0
- package/corpus/templates/clips/AGENTS.md +8 -0
- package/corpus/templates/clips/actions/invite-member.ts +1 -1
- package/corpus/templates/clips/actions/request-transcript.ts +1 -1
- package/corpus/templates/clips/app/components/library/library-layout.tsx +325 -333
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +80 -40
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +2 -0
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +629 -643
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +263 -158
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +10 -2
- package/corpus/templates/clips/changelog/2026-06-26-long-desktop-recordings-are-checkpointed-so-macos-finalizati.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-non-mac-desktop-recordings-wait-for-upload-transcription.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-recording-details-button-stays-at-the-far-right-on-small-screen.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-s3-compatible-storage-keys-now-save-securely-from-clips-sett.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-shared-clip-links-guide-agents-through-processing-transcripts.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-the-agent-sidebar-now-slides-in-smoothly-without-duplicate-e.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-the-clip-details-sidebar-now-collapses-into-a-slide-out-pane.md +6 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +55 -3
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +217 -5
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +176 -5
- package/corpus/templates/clips/server/lib/public-agent-context.ts +31 -0
- package/corpus/templates/clips/server/lib/s3-upload-provider.ts +60 -14
- package/corpus/templates/clips/server/lib/video-storage.ts +37 -9
- package/corpus/templates/clips/server/routes/api/agent-transcript.json.get.ts +5 -0
- package/corpus/templates/content/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/content/app/components/sidebar/NotionButton.tsx +1 -0
- package/corpus/templates/content/app/routes/_app.settings.tsx +54 -55
- package/corpus/templates/content/app/routes/_app.team.tsx +2 -28
- package/corpus/templates/content/server/lib/notion.ts +43 -11
- package/corpus/templates/content/server/routes/api/notion/auth-url.get.ts +2 -2
- package/corpus/templates/content/server/routes/api/notion/status.get.ts +3 -3
- package/corpus/templates/design/app/routes/settings.tsx +60 -38
- package/corpus/templates/design/app/routes/team.tsx +2 -10
- package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +2 -2
- package/corpus/templates/dispatch/app/routes/settings.tsx +70 -55
- package/corpus/templates/dispatch/app/routes/team.tsx +5 -1
- package/corpus/templates/forms/actions/db-connect.ts +7 -36
- package/corpus/templates/forms/actions/navigate.ts +2 -2
- package/corpus/templates/forms/app/components/CloudUpgrade.tsx +8 -47
- package/corpus/templates/forms/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/forms/app/components/layout/Layout.tsx +42 -25
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +35 -76
- package/corpus/templates/forms/app/global.css +9 -25
- package/corpus/templates/forms/app/hooks/use-navigation-state.ts +4 -4
- package/corpus/templates/forms/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/forms/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/forms/app/i18n/en-US.ts +1 -0
- package/corpus/templates/forms/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/forms/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/forms/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/forms/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/forms/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/forms/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/forms/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/forms/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/forms/app/lib/route-prewarm.ts +9 -8
- package/corpus/templates/forms/app/pages/AskPage.tsx +70 -0
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +74 -81
- package/corpus/templates/forms/app/root.tsx +1 -1
- package/corpus/templates/forms/app/routes/_app.ask.tsx +14 -0
- package/corpus/templates/forms/app/routes/_app.settings.tsx +54 -54
- package/corpus/templates/forms/app/routes/_app.team.tsx +2 -9
- package/corpus/templates/forms/app/routes/_index.tsx +21 -130
- package/corpus/templates/forms/changelog/2026-06-26-ask-forms-opens-as-a-standard-app-tab-with-a-simpler-sidebar.md +6 -0
- package/corpus/templates/forms/changelog/2026-06-26-forms-are-shown-in-a-tighter-row-list-with-a-denser-sidebar.md +6 -0
- package/corpus/templates/forms/shared/navigation.ts +4 -2
- package/corpus/templates/macros/app/routes/settings.tsx +54 -54
- package/corpus/templates/macros/app/routes/team.tsx +2 -10
- package/corpus/templates/mail/actions/helpers.ts +2 -2
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -1
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +5 -5
- package/corpus/templates/mail/app/routes/team.tsx +3 -2
- package/corpus/templates/mail/server/handlers/emails.ts +3 -2
- package/corpus/templates/mail/server/handlers/google-auth.ts +19 -12
- package/corpus/templates/mail/server/lib/automation-engine.ts +3 -2
- package/corpus/templates/mail/server/lib/email-state.ts +2 -3
- package/corpus/templates/mail/server/lib/google-auth.ts +28 -11
- package/corpus/templates/mail/server/lib/jobs.ts +3 -2
- package/corpus/templates/mail/server/onboarding.ts +9 -6
- package/corpus/templates/mail/server/routes/api/attachments.get.ts +3 -3
- package/corpus/templates/plan/actions/request-plan-access.ts +1 -1
- package/corpus/templates/plan/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +56 -4
- package/corpus/templates/plan/app/global.css +8 -3
- package/corpus/templates/plan/app/hooks/use-navigation-state.ts +4 -2
- package/corpus/templates/plan/app/lib/route-prewarm.ts +0 -4
- package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -1
- package/corpus/templates/plan/app/routes/settings.tsx +75 -73
- package/corpus/templates/plan/app/routes/team.tsx +2 -8
- package/corpus/templates/plan/changelog/2026-06-26-canvas-grid-panning-now-stays-smooth-and-fills-the-workspace.md +6 -0
- package/corpus/templates/plan/server/lib/comment-notifications.ts +1 -1
- package/corpus/templates/slides/actions/check-image-gen.ts +2 -1
- package/corpus/templates/slides/actions/db-connect.ts +7 -45
- package/corpus/templates/slides/actions/edit-image.ts +6 -3
- package/corpus/templates/slides/actions/generate-image-api.ts +1 -1
- package/corpus/templates/slides/actions/generate-image.ts +2 -2
- package/corpus/templates/slides/actions/generate-slides-ai.ts +5 -3
- package/corpus/templates/slides/actions/image-gen-status.ts +4 -3
- package/corpus/templates/slides/actions/image-search.ts +6 -3
- package/corpus/templates/slides/actions/search-images.ts +6 -3
- package/corpus/templates/slides/app/components/CloudUpgrade.tsx +8 -51
- package/corpus/templates/slides/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/slides/app/components/layout/Sidebar.tsx +0 -2
- package/corpus/templates/slides/app/routes/settings.tsx +54 -54
- package/corpus/templates/slides/app/routes/team.tsx +2 -12
- package/corpus/templates/slides/server/handlers/google-docs-auth.ts +9 -9
- package/corpus/templates/slides/server/handlers/image-gen.ts +5 -1
- package/corpus/templates/slides/server/handlers/image-providers/gemini.ts +9 -1
- package/corpus/templates/slides/server/handlers/image-providers/index.ts +18 -8
- package/corpus/templates/slides/server/handlers/image-providers/openai.ts +10 -4
- package/corpus/templates/slides/server/handlers/image-providers/types.ts +1 -0
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +59 -34
- package/corpus/templates/slides/server/register-secrets.ts +60 -0
- package/corpus/templates/videos/actions/db-connect.ts +7 -45
- package/corpus/templates/videos/app/components/CloudUpgrade.tsx +8 -51
- package/corpus/templates/videos/app/components/Sidebar.tsx +1 -9
- package/corpus/templates/videos/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +0 -3
- package/corpus/templates/videos/app/routes/settings.tsx +54 -54
- package/corpus/templates/videos/app/routes/team.tsx +2 -8
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +8 -3
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +3 -4
- package/dist/cli/mcp.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +6 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
- package/dist/client/chat/markdown-renderer.js +1 -0
- package/dist/client/chat/markdown-renderer.js.map +1 -1
- package/dist/client/i18n.d.ts.map +1 -1
- package/dist/client/i18n.js +20 -9
- package/dist/client/i18n.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts +3 -3
- package/dist/client/org/OrgSwitcher.js +2 -2
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/org/TeamPage.d.ts +6 -1
- package/dist/client/org/TeamPage.d.ts.map +1 -1
- package/dist/client/org/TeamPage.js +2 -2
- package/dist/client/org/TeamPage.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts +27 -0
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -0
- package/dist/client/settings/SettingsTabsPage.js +108 -0
- package/dist/client/settings/SettingsTabsPage.js.map +1 -0
- package/dist/client/settings/index.d.ts +1 -0
- package/dist/client/settings/index.d.ts.map +1 -1
- package/dist/client/settings/index.js +1 -0
- package/dist/client/settings/index.js.map +1 -1
- package/dist/file-upload/index.d.ts +1 -1
- package/dist/file-upload/index.d.ts.map +1 -1
- package/dist/file-upload/index.js +1 -1
- package/dist/file-upload/index.js.map +1 -1
- package/dist/file-upload/registry.d.ts +1 -0
- package/dist/file-upload/registry.d.ts.map +1 -1
- package/dist/file-upload/registry.js +24 -6
- package/dist/file-upload/registry.js.map +1 -1
- package/dist/file-upload/types.d.ts +6 -1
- package/dist/file-upload/types.d.ts.map +1 -1
- package/dist/file-upload/types.js.map +1 -1
- package/dist/integrations/adapters/email.d.ts.map +1 -1
- package/dist/integrations/adapters/email.js +18 -18
- package/dist/integrations/adapters/email.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +8 -7
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/adapters/telegram.d.ts.map +1 -1
- package/dist/integrations/adapters/telegram.js +9 -9
- package/dist/integrations/adapters/telegram.js.map +1 -1
- package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
- package/dist/integrations/adapters/whatsapp.js +10 -9
- package/dist/integrations/adapters/whatsapp.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +47 -13
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +6 -6
- package/dist/onboarding/default-steps.d.ts.map +1 -1
- package/dist/onboarding/default-steps.js +6 -5
- package/dist/onboarding/default-steps.js.map +1 -1
- package/dist/onboarding/types.d.ts +1 -1
- package/dist/onboarding/types.d.ts.map +1 -1
- package/dist/onboarding/types.js.map +1 -1
- package/dist/org/handlers.js +1 -1
- package/dist/org/handlers.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +4 -6
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +2 -2
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +17 -56
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +67 -153
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/create-server.d.ts +3 -9
- package/dist/server/create-server.d.ts.map +1 -1
- package/dist/server/create-server.js +37 -91
- package/dist/server/create-server.js.map +1 -1
- package/dist/server/email-actions.d.ts +2 -3
- package/dist/server/email-actions.d.ts.map +1 -1
- package/dist/server/email-actions.js +3 -6
- package/dist/server/email-actions.js.map +1 -1
- package/dist/server/email.d.ts +3 -3
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +40 -19
- package/dist/server/email.js.map +1 -1
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/scoped-key-storage.d.ts +22 -0
- package/dist/server/scoped-key-storage.d.ts.map +1 -0
- package/dist/server/scoped-key-storage.js +152 -0
- package/dist/server/scoped-key-storage.js.map +1 -0
- package/dist/shared/agent-env.d.ts +3 -3
- package/dist/shared/agent-env.d.ts.map +1 -1
- package/dist/shared/agent-env.js +3 -3
- package/dist/shared/agent-env.js.map +1 -1
- package/dist/sharing/actions/share-resource.js +1 -1
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/styles/agent-native.css +47 -3
- package/dist/templates/workspace-root/AGENTS.md +3 -2
- package/dist/templates/workspace-root/README.md +1 -1
- package/dist/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
- package/dist/terminal/pty-server.d.ts.map +1 -1
- package/dist/terminal/pty-server.js +3 -36
- package/dist/terminal/pty-server.js.map +1 -1
- package/dist/triggers/dispatcher.js +1 -1
- package/dist/triggers/dispatcher.js.map +1 -1
- package/docs/content/file-uploads.md +11 -4
- package/docs/content/locales/ar-SA/multi-app-workspace.md +3 -3
- package/docs/content/locales/de-DE/multi-app-workspace.md +3 -3
- package/docs/content/locales/es-ES/multi-app-workspace.md +3 -3
- package/docs/content/locales/fr-FR/multi-app-workspace.md +3 -3
- package/docs/content/locales/hi-IN/multi-app-workspace.md +3 -3
- package/docs/content/locales/ja-JP/multi-app-workspace.md +3 -3
- package/docs/content/locales/ko-KR/multi-app-workspace.md +3 -3
- package/docs/content/locales/pt-BR/multi-app-workspace.md +3 -3
- package/docs/content/locales/zh-CN/multi-app-workspace.md +3 -3
- package/docs/content/locales/zh-TW/multi-app-workspace.md +3 -3
- package/docs/content/multi-app-workspace.md +3 -3
- package/docs/content/template-clips.md +14 -5
- package/package.json +1 -1
- package/src/templates/workspace-root/AGENTS.md +3 -2
- package/src/templates/workspace-root/README.md +1 -1
- package/src/templates/workspace-root/scripts/repair-workspace-org.ts +29 -90
- package/corpus/core/src/server/env-var-writes.ts +0 -16
- package/corpus/templates/analytics/app/pages/Team.tsx +0 -9
- package/corpus/templates/calendar/app/pages/Team.tsx +0 -12
- package/corpus/templates/mail/app/pages/Team.tsx +0 -17
- package/dist/server/env-var-writes.d.ts +0 -10
- package/dist/server/env-var-writes.d.ts.map +0 -1
- package/dist/server/env-var-writes.js +0 -18
- package/dist/server/env-var-writes.js.map +0 -1
|
@@ -86,6 +86,7 @@ const NATIVE_FULLSCREEN_RECORDING_FLAG = "clips:native-fullscreen-recording";
|
|
|
86
86
|
const DEV_SYNTHETIC_CAPTURE_FLAG = "clips:dev-synthetic-capture";
|
|
87
87
|
const LEGACY_DEV_REAL_CAPTURE_FLAG = "clips:dev-real-capture";
|
|
88
88
|
const LIVE_UPLOAD_CHUNK_MS = 1_000;
|
|
89
|
+
const NATIVE_FULLSCREEN_SEGMENT_MS = 5 * 60_000;
|
|
89
90
|
const CLOUD_CAPTURE_FRAME_RATE = 24;
|
|
90
91
|
const CLOUD_CAPTURE_MAX_WIDTH = 1920;
|
|
91
92
|
const CLOUD_CAPTURE_MAX_HEIGHT = 1080;
|
|
@@ -122,6 +123,13 @@ export function shouldUseNativeFullscreenRecording(
|
|
|
122
123
|
return isMacPlatform();
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
function shouldSaveLocalTranscriptionStartupFailure(): boolean {
|
|
127
|
+
// Local Whisper/SFSpeech capture is macOS-only today. Non-mac desktop builds
|
|
128
|
+
// should wait for upload transcription instead of publishing a misleading
|
|
129
|
+
// native-transcription failure before `request-transcript` runs.
|
|
130
|
+
return isMacPlatform();
|
|
131
|
+
}
|
|
132
|
+
|
|
125
133
|
function shouldUseDevSyntheticCapture(): boolean {
|
|
126
134
|
if (!import.meta.env.DEV) return false;
|
|
127
135
|
if (typeof localStorage === "undefined") return false;
|
|
@@ -962,7 +970,7 @@ async function saveRecordingTranscript(
|
|
|
962
970
|
recordingId,
|
|
963
971
|
fullText: text,
|
|
964
972
|
segments: transcript.segments,
|
|
965
|
-
source: "whisper",
|
|
973
|
+
source: transcript.source ?? "whisper",
|
|
966
974
|
}),
|
|
967
975
|
});
|
|
968
976
|
if (!res.ok) {
|
|
@@ -1753,6 +1761,8 @@ async function startNativeFullscreenRecording(
|
|
|
1753
1761
|
let cancelPromise: Promise<void> | null = null;
|
|
1754
1762
|
let stateUnlistens: UnlistenFn[] = [];
|
|
1755
1763
|
let tickHandle: ReturnType<typeof setInterval> | null = null;
|
|
1764
|
+
let segmentRotateHandle: ReturnType<typeof setInterval> | null = null;
|
|
1765
|
+
let segmentRotateInFlight = false;
|
|
1756
1766
|
// Pause/resume tracking. The Rust side actually stops the SCStream on
|
|
1757
1767
|
// pause and starts a new segment on resume; on stop it concatenates
|
|
1758
1768
|
// segments via AVFoundation. We keep the JS-side timer in sync so the
|
|
@@ -1760,6 +1770,37 @@ async function startNativeFullscreenRecording(
|
|
|
1760
1770
|
let pausedAt: number | null = null;
|
|
1761
1771
|
let accumulatedPauseMs = 0;
|
|
1762
1772
|
|
|
1773
|
+
function clearSegmentRotator() {
|
|
1774
|
+
if (segmentRotateHandle) {
|
|
1775
|
+
clearInterval(segmentRotateHandle);
|
|
1776
|
+
segmentRotateHandle = null;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function startSegmentRotator() {
|
|
1781
|
+
clearSegmentRotator();
|
|
1782
|
+
segmentRotateHandle = setInterval(() => {
|
|
1783
|
+
if (stopped || pausedAt != null || segmentRotateInFlight) return;
|
|
1784
|
+
segmentRotateInFlight = true;
|
|
1785
|
+
invoke("native_fullscreen_recording_rotate_segment")
|
|
1786
|
+
.catch((err) => {
|
|
1787
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1788
|
+
console.warn("[clips-recorder] native segment rotation failed:", err);
|
|
1789
|
+
if (
|
|
1790
|
+
!stopped &&
|
|
1791
|
+
pausedAt == null &&
|
|
1792
|
+
message.includes("paused recording")
|
|
1793
|
+
) {
|
|
1794
|
+
pausedAt = Date.now();
|
|
1795
|
+
emitState();
|
|
1796
|
+
}
|
|
1797
|
+
})
|
|
1798
|
+
.finally(() => {
|
|
1799
|
+
segmentRotateInFlight = false;
|
|
1800
|
+
});
|
|
1801
|
+
}, NATIVE_FULLSCREEN_SEGMENT_MS);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1763
1804
|
function emitState() {
|
|
1764
1805
|
const now = Date.now();
|
|
1765
1806
|
const pausedNowMs = pausedAt != null ? now - pausedAt : 0;
|
|
@@ -1781,6 +1822,7 @@ async function startNativeFullscreenRecording(
|
|
|
1781
1822
|
stopped = true;
|
|
1782
1823
|
console.log("[clips-recorder] native full-screen stop requested");
|
|
1783
1824
|
if (!localOnly) showFinalizingFeedback();
|
|
1825
|
+
clearSegmentRotator();
|
|
1784
1826
|
if (tickHandle) {
|
|
1785
1827
|
clearInterval(tickHandle);
|
|
1786
1828
|
tickHandle = null;
|
|
@@ -1940,6 +1982,7 @@ async function startNativeFullscreenRecording(
|
|
|
1940
1982
|
if (stopped) return;
|
|
1941
1983
|
cancelPromise = (async () => {
|
|
1942
1984
|
stopped = true;
|
|
1985
|
+
clearSegmentRotator();
|
|
1943
1986
|
if (tickHandle) {
|
|
1944
1987
|
clearInterval(tickHandle);
|
|
1945
1988
|
tickHandle = null;
|
|
@@ -2020,6 +2063,7 @@ async function startNativeFullscreenRecording(
|
|
|
2020
2063
|
]);
|
|
2021
2064
|
stateUnlistens = toolbarUnlistens;
|
|
2022
2065
|
tickHandle = setInterval(emitState, 500);
|
|
2066
|
+
startSegmentRotator();
|
|
2023
2067
|
emit("clips:toolbar-enabled", true).catch(() => {});
|
|
2024
2068
|
emitState();
|
|
2025
2069
|
|
|
@@ -2041,7 +2085,11 @@ async function startNativeFullscreenRecording(
|
|
|
2041
2085
|
if (stopped && transcriptionCapture) {
|
|
2042
2086
|
void transcriptionCapture.cancel().catch(() => {});
|
|
2043
2087
|
transcriptionCapture = null;
|
|
2044
|
-
} else if (
|
|
2088
|
+
} else if (
|
|
2089
|
+
wantsAudio &&
|
|
2090
|
+
!transcriptionCapture &&
|
|
2091
|
+
shouldSaveLocalTranscriptionStartupFailure()
|
|
2092
|
+
) {
|
|
2045
2093
|
void saveTranscriptFailure(
|
|
2046
2094
|
"macOS Speech recognition could not start for this recording. Check Speech Recognition and Microphone permissions, then retry transcription.",
|
|
2047
2095
|
);
|
|
@@ -2863,7 +2911,11 @@ async function startRecordingInner(
|
|
|
2863
2911
|
if (stopped && transcriptionCapture) {
|
|
2864
2912
|
void transcriptionCapture.cancel().catch(() => {});
|
|
2865
2913
|
transcriptionCapture = null;
|
|
2866
|
-
} else if (
|
|
2914
|
+
} else if (
|
|
2915
|
+
wantsAudio &&
|
|
2916
|
+
!transcriptionCapture &&
|
|
2917
|
+
shouldSaveLocalTranscriptionStartupFailure()
|
|
2918
|
+
) {
|
|
2867
2919
|
void saveTranscriptFailure(
|
|
2868
2920
|
"macOS Speech recognition could not start for this recording. Check Speech Recognition and Microphone permissions, then retry transcription.",
|
|
2869
2921
|
);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Live transcription for recordings.
|
|
3
3
|
*
|
|
4
|
-
* Thin wrapper over the shared transcription
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Thin wrapper over the shared transcription engines. It tries local
|
|
5
|
+
* whisper.cpp/macOS speech first (mic + optional system audio), then falls
|
|
6
|
+
* back to Web Speech in the desktop webview when the local Rust engine is not
|
|
7
|
+
* available (notably non-mac builds).
|
|
8
8
|
*
|
|
9
9
|
* The handle exposes `stop()` (returns the full speaker-labelled transcript,
|
|
10
10
|
* after a short grace for trailing finals) and `cancel()` (stops + discards).
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
|
|
24
24
|
/** Grace period after stop for whisper to emit any flushed trailing finals. */
|
|
25
25
|
const WHISPER_STOP_SETTLE_MS = 1500;
|
|
26
|
+
const WEB_SPEECH_STOP_SETTLE_MS = 1200;
|
|
26
27
|
|
|
27
28
|
function wait(ms: number): Promise<void> {
|
|
28
29
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
@@ -33,6 +34,8 @@ export interface CapturedTranscript {
|
|
|
33
34
|
text: string;
|
|
34
35
|
/** Real whisper segments with verbatim timestamps. */
|
|
35
36
|
segments: SourcedTranscriptSegment[];
|
|
37
|
+
/** Source stored with `save-browser-transcript`. */
|
|
38
|
+
source?: "web-speech" | "macos-native" | "whisper";
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export interface TranscriptionCapture {
|
|
@@ -40,6 +43,213 @@ export interface TranscriptionCapture {
|
|
|
40
43
|
cancel(): Promise<void>;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
interface SpeechRecognitionResultLike {
|
|
47
|
+
isFinal: boolean;
|
|
48
|
+
0?: { transcript?: string };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface SpeechRecognitionEventLike {
|
|
52
|
+
resultIndex: number;
|
|
53
|
+
results: ArrayLike<SpeechRecognitionResultLike>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface SpeechRecognitionErrorEventLike {
|
|
57
|
+
error: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface SpeechRecognitionLike {
|
|
61
|
+
continuous: boolean;
|
|
62
|
+
interimResults: boolean;
|
|
63
|
+
lang: string;
|
|
64
|
+
maxAlternatives: number;
|
|
65
|
+
onresult: ((event: SpeechRecognitionEventLike) => void) | null;
|
|
66
|
+
onerror: ((event: SpeechRecognitionErrorEventLike) => void) | null;
|
|
67
|
+
onend: (() => void) | null;
|
|
68
|
+
start: () => void;
|
|
69
|
+
stop: () => void;
|
|
70
|
+
abort: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type SpeechRecognitionCtor = new () => SpeechRecognitionLike;
|
|
74
|
+
|
|
75
|
+
function getSpeechRecognitionCtor(): SpeechRecognitionCtor | null {
|
|
76
|
+
if (typeof window === "undefined") return null;
|
|
77
|
+
const w = window as unknown as {
|
|
78
|
+
SpeechRecognition?: SpeechRecognitionCtor;
|
|
79
|
+
webkitSpeechRecognition?: SpeechRecognitionCtor;
|
|
80
|
+
};
|
|
81
|
+
return w.SpeechRecognition || w.webkitSpeechRecognition || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function browserLanguage(): string {
|
|
85
|
+
return navigator.language || "en-US";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function shouldUseBrowserTranscriptionFallback(): boolean {
|
|
89
|
+
if (typeof navigator === "undefined") return false;
|
|
90
|
+
const platform =
|
|
91
|
+
(navigator as { userAgentData?: { platform?: string } }).userAgentData
|
|
92
|
+
?.platform ||
|
|
93
|
+
navigator.platform ||
|
|
94
|
+
navigator.userAgent;
|
|
95
|
+
|
|
96
|
+
// Keep macOS on the existing local Whisper -> SFSpeech path. Web Speech is
|
|
97
|
+
// only a bridge for non-mac desktop builds where the Rust engines are absent.
|
|
98
|
+
return !/mac/i.test(platform);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function appendTranscriptText(current: string, next: string): string {
|
|
102
|
+
const cleanCurrent = current.trim();
|
|
103
|
+
const cleanNext = next.trim();
|
|
104
|
+
if (!cleanNext) return cleanCurrent;
|
|
105
|
+
if (!cleanCurrent) return cleanNext;
|
|
106
|
+
return `${cleanCurrent} ${cleanNext}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createWebSpeechTranscriptBuffer() {
|
|
110
|
+
let committedFinalText = "";
|
|
111
|
+
let sessionFinalText = "";
|
|
112
|
+
let interimText = "";
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
update(event: SpeechRecognitionEventLike) {
|
|
116
|
+
let nextFinal = "";
|
|
117
|
+
let nextInterim = "";
|
|
118
|
+
for (let i = 0; i < event.results.length; i++) {
|
|
119
|
+
const result = event.results[i];
|
|
120
|
+
const text = result[0]?.transcript ?? "";
|
|
121
|
+
if (result.isFinal) {
|
|
122
|
+
nextFinal += text;
|
|
123
|
+
} else {
|
|
124
|
+
nextInterim += text;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
sessionFinalText = nextFinal;
|
|
128
|
+
interimText = nextInterim;
|
|
129
|
+
},
|
|
130
|
+
commitSession(opts?: { preserveInterim?: boolean }) {
|
|
131
|
+
committedFinalText = appendTranscriptText(
|
|
132
|
+
committedFinalText,
|
|
133
|
+
sessionFinalText,
|
|
134
|
+
);
|
|
135
|
+
sessionFinalText = "";
|
|
136
|
+
if (!opts?.preserveInterim) {
|
|
137
|
+
interimText = "";
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
text() {
|
|
141
|
+
return [committedFinalText, sessionFinalText, interimText]
|
|
142
|
+
.map((part) => part.trim())
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.join(" ")
|
|
145
|
+
.trim();
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function startBrowserTranscriptionCapture(): Promise<TranscriptionCapture | null> {
|
|
151
|
+
const Ctor = getSpeechRecognitionCtor();
|
|
152
|
+
if (!Ctor) return null;
|
|
153
|
+
|
|
154
|
+
const recognition = new Ctor();
|
|
155
|
+
let disposed = false;
|
|
156
|
+
let stopped = false;
|
|
157
|
+
const transcriptBuffer = createWebSpeechTranscriptBuffer();
|
|
158
|
+
let stopResolver: ((value: CapturedTranscript) => void) | null = null;
|
|
159
|
+
let settleTimer: ReturnType<typeof window.setTimeout> | null = null;
|
|
160
|
+
|
|
161
|
+
const captured = (): CapturedTranscript => ({
|
|
162
|
+
text: transcriptBuffer.text(),
|
|
163
|
+
segments: [],
|
|
164
|
+
source: "web-speech",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const settleStop = () => {
|
|
168
|
+
if (!stopResolver) return;
|
|
169
|
+
if (settleTimer) {
|
|
170
|
+
window.clearTimeout(settleTimer);
|
|
171
|
+
settleTimer = null;
|
|
172
|
+
}
|
|
173
|
+
const resolve = stopResolver;
|
|
174
|
+
stopResolver = null;
|
|
175
|
+
disposed = true;
|
|
176
|
+
resolve(captured());
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
recognition.continuous = true;
|
|
180
|
+
recognition.interimResults = true;
|
|
181
|
+
recognition.lang = browserLanguage();
|
|
182
|
+
recognition.maxAlternatives = 1;
|
|
183
|
+
|
|
184
|
+
recognition.onresult = (event) => {
|
|
185
|
+
transcriptBuffer.update(event);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
recognition.onerror = (event) => {
|
|
189
|
+
if (event.error === "no-speech" || event.error === "aborted") return;
|
|
190
|
+
console.warn(
|
|
191
|
+
"[clips-recorder] Web Speech transcription error:",
|
|
192
|
+
event.error,
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
recognition.onend = () => {
|
|
197
|
+
if (disposed) return;
|
|
198
|
+
transcriptBuffer.commitSession({ preserveInterim: stopped });
|
|
199
|
+
if (stopped) {
|
|
200
|
+
settleStop();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
recognition.start();
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.warn(
|
|
207
|
+
"[clips-recorder] Web Speech transcription restart failed:",
|
|
208
|
+
err,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
recognition.start();
|
|
215
|
+
console.log("[clips-recorder] transcription started (web-speech mic)");
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.warn("[clips-recorder] Web Speech transcription unavailable:", err);
|
|
218
|
+
disposed = true;
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
stop() {
|
|
224
|
+
stopped = true;
|
|
225
|
+
return new Promise<CapturedTranscript>((resolve) => {
|
|
226
|
+
stopResolver = resolve;
|
|
227
|
+
settleTimer = window.setTimeout(settleStop, WEB_SPEECH_STOP_SETTLE_MS);
|
|
228
|
+
try {
|
|
229
|
+
recognition.stop();
|
|
230
|
+
} catch {
|
|
231
|
+
settleStop();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
async cancel() {
|
|
236
|
+
disposed = true;
|
|
237
|
+
stopped = true;
|
|
238
|
+
if (settleTimer) {
|
|
239
|
+
window.clearTimeout(settleTimer);
|
|
240
|
+
settleTimer = null;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
recognition.abort();
|
|
244
|
+
} catch {
|
|
245
|
+
// ignore
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const __test = { createWebSpeechTranscriptBuffer };
|
|
252
|
+
|
|
43
253
|
export async function startTranscriptionCapture(
|
|
44
254
|
mic?: {
|
|
45
255
|
deviceId?: string | null;
|
|
@@ -84,7 +294,9 @@ export async function startTranscriptionCapture(
|
|
|
84
294
|
} catch (err) {
|
|
85
295
|
cleanup();
|
|
86
296
|
console.warn("[clips-recorder] whisper transcript unavailable:", err);
|
|
87
|
-
return
|
|
297
|
+
return shouldUseBrowserTranscriptionFallback()
|
|
298
|
+
? startBrowserTranscriptionCapture()
|
|
299
|
+
: null;
|
|
88
300
|
}
|
|
89
301
|
|
|
90
302
|
return {
|
|
@@ -113,6 +113,7 @@ pub fn run() {
|
|
|
113
113
|
native_screen::native_fullscreen_recording_cancel,
|
|
114
114
|
native_screen::native_fullscreen_recording_pause,
|
|
115
115
|
native_screen::native_fullscreen_recording_resume,
|
|
116
|
+
native_screen::native_fullscreen_recording_rotate_segment,
|
|
116
117
|
native_screen::native_fullscreen_pending_uploads,
|
|
117
118
|
native_screen::native_fullscreen_recording_retry_upload,
|
|
118
119
|
native_screen::native_fullscreen_recording_discard_upload,
|
|
@@ -129,6 +129,14 @@ struct NativeFullscreenSession {
|
|
|
129
129
|
/// time when reporting `duration_ms`, so the upload metadata matches
|
|
130
130
|
/// the actual recorded content rather than wall-clock time.
|
|
131
131
|
paused_total: Duration,
|
|
132
|
+
/// Start time for the segment currently being captured. Used to subtract
|
|
133
|
+
/// unrecoverable segment time when ScreenCaptureKit fails finalization
|
|
134
|
+
/// but earlier segments are still playable.
|
|
135
|
+
current_segment_started_at: Instant,
|
|
136
|
+
/// Recorded time lost because a finalized segment was unusable and had to
|
|
137
|
+
/// be skipped during recovery.
|
|
138
|
+
lost_segment_duration: Duration,
|
|
139
|
+
lost_segment_count: u32,
|
|
132
140
|
/// When the current pause began, if paused. Folded into `paused_total`
|
|
133
141
|
/// on resume.
|
|
134
142
|
paused_at: Option<Instant>,
|
|
@@ -723,7 +731,9 @@ pub async fn native_fullscreen_recording_begin(
|
|
|
723
731
|
// Rebaseline the duration clock: the warm phase ran during the
|
|
724
732
|
// countdown, so `started_at` (set at warm time) is several seconds
|
|
725
733
|
// early. The clip's first written frame is now, so measure from now.
|
|
726
|
-
|
|
734
|
+
let now = Instant::now();
|
|
735
|
+
session.started_at = now;
|
|
736
|
+
session.current_segment_started_at = now;
|
|
727
737
|
session.pending_recording_output = false;
|
|
728
738
|
|
|
729
739
|
Ok(NativeFullscreenStartInfo {
|
|
@@ -1057,6 +1067,7 @@ pub async fn native_fullscreen_recording_resume(
|
|
|
1057
1067
|
session.backend = Some(backend);
|
|
1058
1068
|
session.segments.push(segment_path);
|
|
1059
1069
|
session.restart.segment_counter = next_counter;
|
|
1070
|
+
session.current_segment_started_at = Instant::now();
|
|
1060
1071
|
session.paused_total = session
|
|
1061
1072
|
.paused_total
|
|
1062
1073
|
.checked_add(paused_at.elapsed())
|
|
@@ -1065,6 +1076,106 @@ pub async fn native_fullscreen_recording_resume(
|
|
|
1065
1076
|
Ok(())
|
|
1066
1077
|
}
|
|
1067
1078
|
|
|
1079
|
+
/// Best-effort checkpoint for long ScreenCaptureKit recordings. It finalizes
|
|
1080
|
+
/// the current segment and immediately starts a sibling segment, so a later
|
|
1081
|
+
/// ReplayKit finalization failure can only lose the active segment instead of
|
|
1082
|
+
/// the entire recording.
|
|
1083
|
+
#[tauri::command]
|
|
1084
|
+
pub async fn native_fullscreen_recording_rotate_segment(
|
|
1085
|
+
app: AppHandle,
|
|
1086
|
+
state: State<'_, NativeFullscreenRecordingState>,
|
|
1087
|
+
) -> Result<(), String> {
|
|
1088
|
+
#[cfg(not(target_os = "macos"))]
|
|
1089
|
+
{
|
|
1090
|
+
let _ = (app, state);
|
|
1091
|
+
Ok(())
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
#[cfg(target_os = "macos")]
|
|
1095
|
+
{
|
|
1096
|
+
let mut guard = state.inner.lock().map_err(|e| e.to_string())?;
|
|
1097
|
+
let Some(session) = guard.as_mut() else {
|
|
1098
|
+
return Ok(());
|
|
1099
|
+
};
|
|
1100
|
+
if session.pending_recording_output || session.paused_at.is_some() {
|
|
1101
|
+
return Ok(());
|
|
1102
|
+
}
|
|
1103
|
+
if !matches!(
|
|
1104
|
+
session.backend.as_ref(),
|
|
1105
|
+
Some(NativeFullscreenBackend::ScreenCaptureKit { .. })
|
|
1106
|
+
) {
|
|
1107
|
+
return Ok(());
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
rotate_screencapturekit_segment(&app, session)
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
#[cfg(target_os = "macos")]
|
|
1115
|
+
fn rotate_screencapturekit_segment(
|
|
1116
|
+
app: &AppHandle,
|
|
1117
|
+
session: &mut NativeFullscreenSession,
|
|
1118
|
+
) -> Result<(), String> {
|
|
1119
|
+
let restart = session.restart.clone();
|
|
1120
|
+
let next_counter = restart.segment_counter.saturating_add(1);
|
|
1121
|
+
let extension = native_extension_for_mime_type(session.mime_type);
|
|
1122
|
+
let segment_path = segment_path_for(app, &restart.safe_id, extension, next_counter)?;
|
|
1123
|
+
let _ = std::fs::remove_file(&segment_path);
|
|
1124
|
+
|
|
1125
|
+
if let Some(free) = free_disk_bytes(segment_path.parent().unwrap_or(&segment_path)) {
|
|
1126
|
+
if free < DISK_SPACE_BLOCK_BYTES {
|
|
1127
|
+
eprintln!(
|
|
1128
|
+
"[clips-tray] skipping segment rotation because disk space is low: {} free",
|
|
1129
|
+
format_mb(free)
|
|
1130
|
+
);
|
|
1131
|
+
return Ok(());
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
eprintln!(
|
|
1136
|
+
"[clips-tray] rotating ScreenCaptureKit recording segment -> {}",
|
|
1137
|
+
segment_path.display()
|
|
1138
|
+
);
|
|
1139
|
+
let rotation_started = Instant::now();
|
|
1140
|
+
let stop_outcome = finalize_active_backend(session, true);
|
|
1141
|
+
if let Err(err) = &stop_outcome {
|
|
1142
|
+
eprintln!("[clips-tray] segment rotation finalize reported an error: {err}");
|
|
1143
|
+
}
|
|
1144
|
+
recover_from_unusable_current_segment(session, "segment rotation", true);
|
|
1145
|
+
|
|
1146
|
+
let start_result = start_screencapturekit_backend_at(
|
|
1147
|
+
&segment_path,
|
|
1148
|
+
restart.include_audio,
|
|
1149
|
+
restart.capture_system_audio,
|
|
1150
|
+
restart.mic_device_id.as_deref(),
|
|
1151
|
+
restart.mic_device_label.as_deref(),
|
|
1152
|
+
restart.target_display_id,
|
|
1153
|
+
restart.capture_region,
|
|
1154
|
+
false,
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
let (backend, _, _) = match start_result {
|
|
1158
|
+
Ok(result) => result,
|
|
1159
|
+
Err(err) => {
|
|
1160
|
+
session.paused_at = Some(rotation_started);
|
|
1161
|
+
return Err(format!(
|
|
1162
|
+
"ScreenCaptureKit segment rotation paused recording after a finalized checkpoint, but the next segment could not start: {err}"
|
|
1163
|
+
));
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
session.paused_total = session
|
|
1168
|
+
.paused_total
|
|
1169
|
+
.checked_add(rotation_started.elapsed())
|
|
1170
|
+
.unwrap_or(session.paused_total);
|
|
1171
|
+
session.backend = Some(backend);
|
|
1172
|
+
session.segments.push(segment_path);
|
|
1173
|
+
session.restart.segment_counter = next_counter;
|
|
1174
|
+
session.current_segment_started_at = Instant::now();
|
|
1175
|
+
session.paused_at = None;
|
|
1176
|
+
Ok(())
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1068
1179
|
/// Outcome of taking the active session out of state and finalizing
|
|
1069
1180
|
/// every backend / segment it owns. Both the upload and the save-locally
|
|
1070
1181
|
/// stop commands need exactly this prelude, so it lives in one place.
|
|
@@ -1076,9 +1187,10 @@ struct StoppedSession {
|
|
|
1076
1187
|
stop_outcome: Result<(), String>,
|
|
1077
1188
|
/// Result of merging segment files into `session.path`.
|
|
1078
1189
|
consolidate_outcome: Result<(), String>,
|
|
1079
|
-
/// True when more than one segment was captured (
|
|
1080
|
-
///
|
|
1081
|
-
/// failure is fatal — single-segment consolidation is just a
|
|
1190
|
+
/// True when more than one segment was captured (manual pause/resume or
|
|
1191
|
+
/// automatic long-recording checkpoints). Used to decide whether a
|
|
1192
|
+
/// consolidation failure is fatal — single-segment consolidation is just a
|
|
1193
|
+
/// rename.
|
|
1082
1194
|
multi_segment: bool,
|
|
1083
1195
|
}
|
|
1084
1196
|
|
|
@@ -1105,6 +1217,7 @@ fn take_and_finalize_active_session(
|
|
|
1105
1217
|
// playable. The caller persists recovery metadata so a finalize
|
|
1106
1218
|
// failure doesn't orphan the file.
|
|
1107
1219
|
let stop_outcome = finalize_active_backend(&mut session, true);
|
|
1220
|
+
recover_from_unusable_current_segment(&mut session, "final stop", false);
|
|
1108
1221
|
println!(
|
|
1109
1222
|
"[clips-tray] finalize backend done (ok={}); {}",
|
|
1110
1223
|
stop_outcome.is_ok(),
|
|
@@ -1124,10 +1237,18 @@ fn take_and_finalize_active_session(
|
|
|
1124
1237
|
session.segments.len(),
|
|
1125
1238
|
describe_recording_path(&session.path)
|
|
1126
1239
|
);
|
|
1240
|
+
if session.lost_segment_count > 0 {
|
|
1241
|
+
eprintln!(
|
|
1242
|
+
"[clips-tray] recovered recording by dropping {} unusable segment(s), approx {}s lost",
|
|
1243
|
+
session.lost_segment_count,
|
|
1244
|
+
session.lost_segment_duration.as_secs()
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1127
1247
|
let duration_ms = session
|
|
1128
1248
|
.started_at
|
|
1129
1249
|
.elapsed()
|
|
1130
1250
|
.saturating_sub(session.paused_total)
|
|
1251
|
+
.saturating_sub(session.lost_segment_duration)
|
|
1131
1252
|
.as_millis();
|
|
1132
1253
|
Ok(StoppedSession {
|
|
1133
1254
|
session,
|
|
@@ -1150,6 +1271,52 @@ fn finalize_active_backend(
|
|
|
1150
1271
|
stop_native_recording(&mut backend, wait_for_finalize)
|
|
1151
1272
|
}
|
|
1152
1273
|
|
|
1274
|
+
fn playable_recording_file(path: &Path, mime_type: &str) -> bool {
|
|
1275
|
+
match std::fs::metadata(path) {
|
|
1276
|
+
Ok(metadata) if metadata.len() > 0 => {}
|
|
1277
|
+
_ => return false,
|
|
1278
|
+
}
|
|
1279
|
+
if mime_type == MP4_RECORDING_MIME_TYPE || mime_type == QUICKTIME_RECORDING_MIME_TYPE {
|
|
1280
|
+
return mp4_has_moov(path) != Some(false);
|
|
1281
|
+
}
|
|
1282
|
+
true
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
fn recover_from_unusable_current_segment(
|
|
1286
|
+
session: &mut NativeFullscreenSession,
|
|
1287
|
+
reason: &str,
|
|
1288
|
+
allow_empty: bool,
|
|
1289
|
+
) -> bool {
|
|
1290
|
+
let Some(current) = session.segments.last().cloned() else {
|
|
1291
|
+
return false;
|
|
1292
|
+
};
|
|
1293
|
+
if playable_recording_file(¤t, session.mime_type) {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
if !allow_empty && session.segments.len() <= 1 {
|
|
1297
|
+
eprintln!(
|
|
1298
|
+
"[clips-tray] current recording segment is unusable after {reason}, but no earlier segment exists to recover"
|
|
1299
|
+
);
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
let dropped = session.segments.pop();
|
|
1304
|
+
if dropped.as_ref() == Some(¤t) {
|
|
1305
|
+
let _ = std::fs::remove_file(¤t);
|
|
1306
|
+
session.lost_segment_count = session.lost_segment_count.saturating_add(1);
|
|
1307
|
+
session.lost_segment_duration = session
|
|
1308
|
+
.lost_segment_duration
|
|
1309
|
+
.checked_add(session.current_segment_started_at.elapsed())
|
|
1310
|
+
.unwrap_or(session.lost_segment_duration);
|
|
1311
|
+
eprintln!(
|
|
1312
|
+
"[clips-tray] dropped unusable recording segment after {reason}; recovered {} earlier segment(s)",
|
|
1313
|
+
session.segments.len()
|
|
1314
|
+
);
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
false
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1153
1320
|
/// Best-effort cleanup of a session being discarded (cancel, or a stale
|
|
1154
1321
|
/// session displaced by a new start). Finalizes any active backend and
|
|
1155
1322
|
/// deletes every on-disk artifact — segment files and the final path.
|
|
@@ -2243,15 +2410,19 @@ fn new_fullscreen_session(
|
|
|
2243
2410
|
height: Option<u32>,
|
|
2244
2411
|
restart: RestartInfo,
|
|
2245
2412
|
) -> NativeFullscreenSession {
|
|
2413
|
+
let now = Instant::now();
|
|
2246
2414
|
NativeFullscreenSession {
|
|
2247
2415
|
backend: Some(backend),
|
|
2248
2416
|
path: path.clone(),
|
|
2249
2417
|
mime_type,
|
|
2250
|
-
started_at:
|
|
2418
|
+
started_at: now,
|
|
2251
2419
|
width,
|
|
2252
2420
|
height,
|
|
2253
2421
|
segments: vec![path],
|
|
2254
2422
|
paused_total: Duration::ZERO,
|
|
2423
|
+
current_segment_started_at: now,
|
|
2424
|
+
lost_segment_duration: Duration::ZERO,
|
|
2425
|
+
lost_segment_count: 0,
|
|
2255
2426
|
paused_at: None,
|
|
2256
2427
|
restart,
|
|
2257
2428
|
pending_recording_output: false,
|
|
@@ -455,6 +455,34 @@ function compactBugReport(report: PublicAgentBugReport) {
|
|
|
455
455
|
};
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
export function transcriptStatusInstructions(
|
|
459
|
+
transcript: PublicAgentTranscript,
|
|
460
|
+
) {
|
|
461
|
+
const status = transcript?.status ?? "missing";
|
|
462
|
+
const failureReason = transcript?.failureReason ?? "";
|
|
463
|
+
const lowerReason = failureReason.toLowerCase();
|
|
464
|
+
|
|
465
|
+
if (status === "pending") {
|
|
466
|
+
return [
|
|
467
|
+
"The transcript is still pending. For long recordings this can take several minutes; wait 15-30 seconds and fetch apis.context.url or apis.transcript.url again a few times before falling back to frames or telling the user no transcript exists.",
|
|
468
|
+
];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (status === "failed" && lowerReason.includes("credits exhausted")) {
|
|
472
|
+
return [
|
|
473
|
+
"Transcription failed because Builder transcription credits are exhausted. Tell the user to upgrade or connect Builder.io credits, or configure a Groq key for backup speech-to-text. Generic OpenAI or Anthropic chat keys do not transcribe Clips recordings.",
|
|
474
|
+
];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (status === "failed" && failureReason) {
|
|
478
|
+
return [
|
|
479
|
+
`Transcription failed with reason: ${failureReason}. Explain this to the user and suggest retrying transcription or configuring the supported Builder.io/Groq transcription fallback.`,
|
|
480
|
+
];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
|
|
458
486
|
export function buildPublicAgentContext({
|
|
459
487
|
event,
|
|
460
488
|
access,
|
|
@@ -496,6 +524,7 @@ export function buildPublicAgentContext({
|
|
|
496
524
|
}));
|
|
497
525
|
const instructions = [
|
|
498
526
|
"Use transcript.segments for timestamped spoken context.",
|
|
527
|
+
...transcriptStatusInstructions(transcript),
|
|
499
528
|
...(bugReport
|
|
500
529
|
? [
|
|
501
530
|
"Use bugReport for the submitted product context: source URL, page title, app version, environment, severity, and redacted host metadata.",
|
|
@@ -559,6 +588,8 @@ export function buildPublicAgentContext({
|
|
|
559
588
|
transcript: {
|
|
560
589
|
status: transcript?.status ?? "missing",
|
|
561
590
|
language: transcript?.language ?? null,
|
|
591
|
+
failureReason: transcript?.failureReason ?? null,
|
|
592
|
+
retryAfterSeconds: transcript?.status === "pending" ? 15 : null,
|
|
562
593
|
fullText: transcript?.fullText ?? "",
|
|
563
594
|
segments: agentSegments,
|
|
564
595
|
segmentCount: agentSegments.length,
|