@agent-native/core 0.79.16 → 0.79.18
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/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 +55 -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 +0 -17
- 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/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-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 +3 -3
- 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 +219 -155
- 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-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-clip-details-sidebar-now-collapses-into-a-slide-out-pane.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-the-clips-library-now-matches-the-shared-recessed-shell-styl.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/hooks/use-navigation-state.ts +4 -2
- package/corpus/templates/plan/app/lib/route-prewarm.ts +0 -4
- package/corpus/templates/plan/app/routes/settings.tsx +75 -73
- package/corpus/templates/plan/app/routes/team.tsx +2 -8
- 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/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/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +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 +1 -1
- 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/progress/routes.d.ts +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 +1 -1
- 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 +55 -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
|
@@ -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,
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { FileUploadProvider } from "@agent-native/core/file-upload";
|
|
19
|
+
import { resolveSecret } from "@agent-native/core/server";
|
|
19
20
|
|
|
20
21
|
interface S3Config {
|
|
21
22
|
region: string;
|
|
@@ -26,27 +27,71 @@ interface S3Config {
|
|
|
26
27
|
publicBaseUrl: string | null;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
function cleanValue(value: string | null | undefined): string | undefined {
|
|
31
|
+
const cleaned = value?.trim();
|
|
32
|
+
return cleaned ? cleaned : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildS3Config(values: {
|
|
36
|
+
bucket?: string;
|
|
37
|
+
accessKeyId?: string;
|
|
38
|
+
secretAccessKey?: string;
|
|
39
|
+
endpoint?: string;
|
|
40
|
+
region?: string;
|
|
41
|
+
publicBaseUrl?: string;
|
|
42
|
+
}): S3Config | null {
|
|
43
|
+
const bucket = cleanValue(values.bucket);
|
|
44
|
+
const accessKeyId = cleanValue(values.accessKeyId);
|
|
45
|
+
const secretAccessKey = cleanValue(values.secretAccessKey);
|
|
46
|
+
const endpoint = cleanValue(values.endpoint);
|
|
35
47
|
if (!bucket || !accessKeyId || !secretAccessKey || !endpoint) return null;
|
|
36
48
|
return {
|
|
37
|
-
region:
|
|
49
|
+
region: cleanValue(values.region) ?? "auto",
|
|
38
50
|
bucket,
|
|
39
51
|
accessKeyId,
|
|
40
52
|
secretAccessKey,
|
|
41
53
|
endpoint: endpoint.replace(/\/+$/, ""),
|
|
42
54
|
publicBaseUrl:
|
|
43
|
-
(
|
|
44
|
-
/\/+$/,
|
|
45
|
-
"",
|
|
46
|
-
) || null,
|
|
55
|
+
cleanValue(values.publicBaseUrl)?.replace(/\/+$/, "") ?? null,
|
|
47
56
|
};
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
function readS3EnvConfig(): S3Config | null {
|
|
60
|
+
const env = process.env;
|
|
61
|
+
return buildS3Config({
|
|
62
|
+
bucket: env.S3_BUCKET || env.R2_BUCKET,
|
|
63
|
+
accessKeyId: env.S3_ACCESS_KEY_ID || env.R2_ACCESS_KEY_ID,
|
|
64
|
+
secretAccessKey: env.S3_SECRET_ACCESS_KEY || env.R2_SECRET_ACCESS_KEY,
|
|
65
|
+
endpoint: env.S3_ENDPOINT || env.R2_ENDPOINT,
|
|
66
|
+
region: env.S3_REGION || env.R2_REGION,
|
|
67
|
+
publicBaseUrl: env.S3_PUBLIC_BASE_URL || env.R2_PUBLIC_BASE_URL,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function resolveS3Secret(primary: string, fallback: string) {
|
|
72
|
+
return (
|
|
73
|
+
cleanValue(await resolveSecret(primary).catch(() => null)) ??
|
|
74
|
+
cleanValue(await resolveSecret(fallback).catch(() => null))
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function readS3Config(): Promise<S3Config | null> {
|
|
79
|
+
return buildS3Config({
|
|
80
|
+
bucket: await resolveS3Secret("S3_BUCKET", "R2_BUCKET"),
|
|
81
|
+
accessKeyId: await resolveS3Secret("S3_ACCESS_KEY_ID", "R2_ACCESS_KEY_ID"),
|
|
82
|
+
secretAccessKey: await resolveS3Secret(
|
|
83
|
+
"S3_SECRET_ACCESS_KEY",
|
|
84
|
+
"R2_SECRET_ACCESS_KEY",
|
|
85
|
+
),
|
|
86
|
+
endpoint: await resolveS3Secret("S3_ENDPOINT", "R2_ENDPOINT"),
|
|
87
|
+
region: await resolveS3Secret("S3_REGION", "R2_REGION"),
|
|
88
|
+
publicBaseUrl: await resolveS3Secret(
|
|
89
|
+
"S3_PUBLIC_BASE_URL",
|
|
90
|
+
"R2_PUBLIC_BASE_URL",
|
|
91
|
+
),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
50
95
|
// ── SigV4 helpers (Web Crypto, no SDK) ────────────────────────────────
|
|
51
96
|
|
|
52
97
|
async function hmac(key: ArrayBuffer, msg: string): Promise<ArrayBuffer> {
|
|
@@ -187,10 +232,11 @@ async function putObject(
|
|
|
187
232
|
export const s3FileUploadProvider: FileUploadProvider = {
|
|
188
233
|
id: "s3",
|
|
189
234
|
name: "S3-compatible storage",
|
|
190
|
-
isConfigured: () =>
|
|
235
|
+
isConfigured: () => readS3EnvConfig() !== null,
|
|
236
|
+
isConfiguredForRequest: async () => (await readS3Config()) !== null,
|
|
191
237
|
upload: async ({ data, filename, mimeType }) => {
|
|
192
|
-
const cfg = readS3Config();
|
|
193
|
-
if (!cfg) throw new Error("S3
|
|
238
|
+
const cfg = await readS3Config();
|
|
239
|
+
if (!cfg) throw new Error("S3 credentials are not configured");
|
|
194
240
|
|
|
195
241
|
const ext = filename?.split(".").pop() ?? "bin";
|
|
196
242
|
const stamp = Date.now();
|