@agent-native/core 0.79.11 → 0.79.15
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 +25 -0
- package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.md +68 -68
- package/corpus/core/docs/content/locales/zh-TW/actions.md +82 -82
- package/corpus/core/docs/content/locales/zh-TW/agent-mentions.md +22 -22
- package/corpus/core/docs/content/locales/zh-TW/agent-surfaces.md +82 -82
- package/corpus/core/docs/content/locales/zh-TW/agent-teams.md +29 -29
- package/corpus/core/docs/content/locales/zh-TW/agent-web-surfaces.md +11 -11
- package/corpus/core/docs/content/locales/zh-TW/audit-log.md +16 -16
- package/corpus/core/docs/content/locales/zh-TW/authentication.md +32 -32
- package/corpus/core/docs/content/locales/zh-TW/automations.md +32 -32
- package/corpus/core/docs/content/locales/zh-TW/blueprint-installer.md +19 -19
- package/corpus/core/docs/content/locales/zh-TW/cli-adapters.md +17 -17
- package/corpus/core/docs/content/locales/zh-TW/client.md +62 -62
- package/corpus/core/docs/content/locales/zh-TW/cloneable-saas.md +32 -32
- package/corpus/core/docs/content/locales/zh-TW/code-agents-ui.md +91 -91
- package/corpus/core/docs/content/locales/zh-TW/components.md +51 -51
- package/corpus/core/docs/content/locales/zh-TW/context-awareness.md +66 -66
- package/corpus/core/docs/content/locales/zh-TW/creating-templates.md +39 -39
- package/corpus/core/docs/content/locales/zh-TW/cross-app-sso.md +29 -29
- package/corpus/core/docs/content/locales/zh-TW/database.md +21 -21
- package/corpus/core/docs/content/locales/zh-TW/deployment.md +37 -37
- package/corpus/core/docs/content/locales/zh-TW/dispatch.md +43 -43
- package/corpus/core/docs/content/locales/zh-TW/drop-in-agent.md +34 -34
- package/corpus/core/docs/content/locales/zh-TW/durable-resume.md +18 -18
- package/corpus/core/docs/content/locales/zh-TW/embedding-sdk.md +73 -73
- package/corpus/core/docs/content/locales/zh-TW/evals.md +16 -16
- package/corpus/core/docs/content/locales/zh-TW/extensions.md +81 -81
- package/corpus/core/docs/content/locales/zh-TW/external-agents.md +146 -146
- package/corpus/core/docs/content/locales/zh-TW/faq.md +43 -43
- package/corpus/core/docs/content/locales/zh-TW/file-uploads.md +11 -11
- package/corpus/core/docs/content/locales/zh-TW/frames.md +33 -33
- package/corpus/core/docs/content/locales/zh-TW/getting-started.md +28 -28
- package/corpus/core/docs/content/locales/zh-TW/harness-agents.md +78 -78
- package/corpus/core/docs/content/locales/zh-TW/human-approval.md +27 -27
- package/corpus/core/docs/content/locales/zh-TW/internationalization.md +19 -19
- package/corpus/core/docs/content/locales/zh-TW/key-concepts.md +68 -68
- package/corpus/core/docs/content/locales/zh-TW/local-file-mode.md +50 -50
- package/corpus/core/docs/content/locales/zh-TW/mcp-apps.md +38 -38
- package/corpus/core/docs/content/locales/zh-TW/mcp-clients.md +67 -67
- package/corpus/core/docs/content/locales/zh-TW/mcp-protocol.md +50 -50
- package/corpus/core/docs/content/locales/zh-TW/messaging.md +99 -99
- package/corpus/core/docs/content/locales/zh-TW/migration-workbench.md +6 -6
- package/corpus/core/docs/content/locales/zh-TW/multi-app-workspace.md +39 -39
- package/corpus/core/docs/content/locales/zh-TW/multi-tenancy.md +7 -7
- package/corpus/core/docs/content/locales/zh-TW/native-chat-ui.md +38 -38
- package/corpus/core/docs/content/locales/zh-TW/notifications.md +23 -23
- package/corpus/core/docs/content/locales/zh-TW/observability.md +49 -49
- package/corpus/core/docs/content/locales/zh-TW/observational-memory.md +23 -23
- package/corpus/core/docs/content/locales/zh-TW/onboarding.md +40 -40
- package/corpus/core/docs/content/locales/zh-TW/plan-plugin.md +37 -37
- package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.md +72 -72
- package/corpus/core/docs/content/locales/zh-TW/processors.md +17 -17
- package/corpus/core/docs/content/locales/zh-TW/progress.md +15 -15
- package/corpus/core/docs/content/locales/zh-TW/pure-agent-apps.md +10 -10
- package/corpus/core/docs/content/locales/zh-TW/real-time-collaboration.md +103 -103
- package/corpus/core/docs/content/locales/zh-TW/recurring-jobs.md +19 -19
- package/corpus/core/docs/content/locales/zh-TW/routing.md +9 -9
- package/corpus/core/docs/content/locales/zh-TW/sandbox-adapters.md +48 -48
- package/corpus/core/docs/content/locales/zh-TW/security.md +49 -49
- package/corpus/core/docs/content/locales/zh-TW/server.md +47 -47
- package/corpus/core/docs/content/locales/zh-TW/sharing.md +22 -22
- package/corpus/core/docs/content/locales/zh-TW/skills-guide.md +46 -46
- package/corpus/core/docs/content/locales/zh-TW/template-analytics.md +40 -40
- package/corpus/core/docs/content/locales/zh-TW/template-assets.md +69 -69
- package/corpus/core/docs/content/locales/zh-TW/template-brain.md +81 -81
- package/corpus/core/docs/content/locales/zh-TW/template-calendar.md +40 -40
- package/corpus/core/docs/content/locales/zh-TW/template-chat.md +27 -27
- package/corpus/core/docs/content/locales/zh-TW/template-clips.md +85 -85
- package/corpus/core/docs/content/locales/zh-TW/template-content.md +65 -66
- package/corpus/core/docs/content/locales/zh-TW/template-design.md +29 -29
- package/corpus/core/docs/content/locales/zh-TW/template-dispatch.md +51 -51
- package/corpus/core/docs/content/locales/zh-TW/template-forms.md +25 -25
- package/corpus/core/docs/content/locales/zh-TW/template-mail.md +71 -71
- package/corpus/core/docs/content/locales/zh-TW/template-plan.md +92 -92
- package/corpus/core/docs/content/locales/zh-TW/template-slides.md +52 -52
- package/corpus/core/docs/content/locales/zh-TW/template-videos.md +30 -30
- package/corpus/core/docs/content/locales/zh-TW/tracking.md +29 -29
- package/corpus/core/docs/content/locales/zh-TW/using-your-agent.md +18 -18
- package/corpus/core/docs/content/locales/zh-TW/voice-input.md +19 -19
- package/corpus/core/docs/content/locales/zh-TW/what-is-agent-native.md +61 -61
- package/corpus/core/docs/content/locales/zh-TW/workspace-connections.md +41 -41
- package/corpus/core/docs/content/locales/zh-TW/workspace-management.md +27 -27
- package/corpus/core/docs/content/locales/zh-TW/workspace.md +58 -58
- package/corpus/core/docs/content/locales/zh-TW/writing-agent-instructions.md +41 -41
- package/corpus/core/docs/content/template-clips.md +48 -2
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/client/AgentPanel.tsx +27 -3
- package/corpus/core/src/deploy/build.ts +738 -57
- package/corpus/core/src/deploy/route-discovery.ts +1 -1
- package/corpus/core/src/server/edge.ts +50 -0
- package/corpus/core/src/styles/agent-native.css +93 -0
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +44 -44
- package/corpus/core/src/vite/client.ts +1 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +5 -3
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +69 -70
- package/corpus/templates/analytics/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -3
- package/corpus/templates/assets/app/i18n/zh-TW.ts +82 -82
- package/corpus/templates/assets/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/brain/app/components/layout/Layout.tsx +6 -4
- package/corpus/templates/brain/app/i18n/zh-TW.ts +90 -90
- package/corpus/templates/brain/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +1 -1
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +57 -57
- package/corpus/templates/calendar/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/calendar/server/lib/booking-og-fonts.ts +20 -6
- package/corpus/templates/calendar/server/lib/booking-og-image.ts +17 -9
- package/corpus/templates/chat/app/components/layout/Layout.tsx +5 -3
- package/corpus/templates/chat/app/i18n/zh-TW.ts +3 -3
- package/corpus/templates/chat/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/clips/AGENTS.md +6 -0
- package/corpus/templates/clips/actions/delete-recording-permanent.ts +7 -1
- package/corpus/templates/clips/actions/get-recording-player-data.ts +44 -2
- package/corpus/templates/clips/actions/navigate.ts +2 -0
- package/corpus/templates/clips/actions/save-bug-report-context.ts +173 -0
- package/corpus/templates/clips/actions/view-screen.ts +58 -7
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +9 -3
- package/corpus/templates/clips/app/hooks/use-navigation-state.ts +17 -0
- package/corpus/templates/clips/app/i18n/ar-SA.ts +31 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +32 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +32 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +32 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +32 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +32 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +31 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +32 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +32 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +30 -0
- package/corpus/templates/clips/app/i18n/zh-TW.ts +461 -451
- package/corpus/templates/clips/app/routes/bug-report.done.tsx +126 -0
- package/corpus/templates/clips/app/routes/bug-report.tsx +222 -0
- package/corpus/templates/clips/app/routes/record.tsx +104 -12
- package/corpus/templates/clips/changelog/2026-06-26-brief-network-blips-no-longer-block-recording-or-wrongly-pro.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-bug-report-recordings-can-keep-redacted-host-app-context-wit.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-bug-report-widgets-can-launch-clips-recordings-from-embedded.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-desktop-recording-keeps-connected-users-out-of-the-storage-s.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-26-if-a-recording-can-t-be-uploaded-the-chrome-extension-now-sa.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +9 -2
- package/corpus/templates/clips/chrome-extension/src/background.ts +43 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +111 -6
- package/corpus/templates/clips/chrome-extension/src/popup.ts +18 -7
- package/corpus/templates/clips/desktop/src/app.tsx +53 -19
- package/corpus/templates/clips/server/db/schema.ts +26 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +51 -0
- package/corpus/templates/clips/server/plugins/auth.ts +1 -0
- package/corpus/templates/clips/server/plugins/db.ts +28 -0
- package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +4 -1
- package/corpus/templates/clips/shared/bug-report.ts +161 -0
- package/corpus/templates/content/app/components/layout/Layout.tsx +10 -8
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -1
- package/corpus/templates/content/app/i18n/zh-TW.ts +67 -66
- package/corpus/templates/content/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/design/app/components/layout/Layout.tsx +3 -3
- package/corpus/templates/design/app/i18n/zh-TW.ts +44 -44
- package/corpus/templates/design/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/dispatch/app/i18n/zh-TW.ts +5 -4
- package/corpus/templates/dispatch/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/forms/app/components/layout/Layout.tsx +4 -2
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/forms/app/i18n/zh-TW.ts +48 -48
- package/corpus/templates/forms/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -3
- package/corpus/templates/macros/app/i18n/zh-TW.ts +5 -5
- package/corpus/templates/macros/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/mail/app/i18n/zh-TW.ts +83 -83
- package/corpus/templates/mail/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/plan/app/components/layout/Layout.tsx +5 -3
- package/corpus/templates/plan/app/i18n/zh-TW.ts +69 -69
- package/corpus/templates/plan/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -3
- package/corpus/templates/slides/app/i18n/zh-TW.ts +64 -64
- package/corpus/templates/slides/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/corpus/templates/videos/app/components/layout/Layout.tsx +2 -2
- package/corpus/templates/videos/app/i18n/zh-TW.ts +54 -54
- package/corpus/templates/videos/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +6 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +14 -4
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +29 -2
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +587 -51
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/route-discovery.d.ts +1 -1
- package/dist/deploy/route-discovery.js +1 -1
- package/dist/deploy/route-discovery.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +7 -7
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/edge.d.ts +11 -0
- package/dist/server/edge.d.ts.map +1 -0
- package/dist/server/edge.js +11 -0
- package/dist/server/edge.js.map +1 -0
- package/dist/styles/agent-native.css +93 -0
- package/dist/templates/default/app/i18n/zh-TW.ts +44 -44
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +1 -0
- package/dist/vite/client.js.map +1 -1
- package/docs/content/locales/zh-TW/a2a-protocol.md +68 -68
- package/docs/content/locales/zh-TW/actions.md +82 -82
- package/docs/content/locales/zh-TW/agent-mentions.md +22 -22
- package/docs/content/locales/zh-TW/agent-surfaces.md +82 -82
- package/docs/content/locales/zh-TW/agent-teams.md +29 -29
- package/docs/content/locales/zh-TW/agent-web-surfaces.md +11 -11
- package/docs/content/locales/zh-TW/audit-log.md +16 -16
- package/docs/content/locales/zh-TW/authentication.md +32 -32
- package/docs/content/locales/zh-TW/automations.md +32 -32
- package/docs/content/locales/zh-TW/blueprint-installer.md +19 -19
- package/docs/content/locales/zh-TW/cli-adapters.md +17 -17
- package/docs/content/locales/zh-TW/client.md +62 -62
- package/docs/content/locales/zh-TW/cloneable-saas.md +32 -32
- package/docs/content/locales/zh-TW/code-agents-ui.md +91 -91
- package/docs/content/locales/zh-TW/components.md +51 -51
- package/docs/content/locales/zh-TW/context-awareness.md +66 -66
- package/docs/content/locales/zh-TW/creating-templates.md +39 -39
- package/docs/content/locales/zh-TW/cross-app-sso.md +29 -29
- package/docs/content/locales/zh-TW/database.md +21 -21
- package/docs/content/locales/zh-TW/deployment.md +37 -37
- package/docs/content/locales/zh-TW/dispatch.md +43 -43
- package/docs/content/locales/zh-TW/drop-in-agent.md +34 -34
- package/docs/content/locales/zh-TW/durable-resume.md +18 -18
- package/docs/content/locales/zh-TW/embedding-sdk.md +73 -73
- package/docs/content/locales/zh-TW/evals.md +16 -16
- package/docs/content/locales/zh-TW/extensions.md +81 -81
- package/docs/content/locales/zh-TW/external-agents.md +146 -146
- package/docs/content/locales/zh-TW/faq.md +43 -43
- package/docs/content/locales/zh-TW/file-uploads.md +11 -11
- package/docs/content/locales/zh-TW/frames.md +33 -33
- package/docs/content/locales/zh-TW/getting-started.md +28 -28
- package/docs/content/locales/zh-TW/harness-agents.md +78 -78
- package/docs/content/locales/zh-TW/human-approval.md +27 -27
- package/docs/content/locales/zh-TW/internationalization.md +19 -19
- package/docs/content/locales/zh-TW/key-concepts.md +68 -68
- package/docs/content/locales/zh-TW/local-file-mode.md +50 -50
- package/docs/content/locales/zh-TW/mcp-apps.md +38 -38
- package/docs/content/locales/zh-TW/mcp-clients.md +67 -67
- package/docs/content/locales/zh-TW/mcp-protocol.md +50 -50
- package/docs/content/locales/zh-TW/messaging.md +99 -99
- package/docs/content/locales/zh-TW/migration-workbench.md +6 -6
- package/docs/content/locales/zh-TW/multi-app-workspace.md +39 -39
- package/docs/content/locales/zh-TW/multi-tenancy.md +7 -7
- package/docs/content/locales/zh-TW/native-chat-ui.md +38 -38
- package/docs/content/locales/zh-TW/notifications.md +23 -23
- package/docs/content/locales/zh-TW/observability.md +49 -49
- package/docs/content/locales/zh-TW/observational-memory.md +23 -23
- package/docs/content/locales/zh-TW/onboarding.md +40 -40
- package/docs/content/locales/zh-TW/plan-plugin.md +37 -37
- package/docs/content/locales/zh-TW/pr-visual-recap.md +72 -72
- package/docs/content/locales/zh-TW/processors.md +17 -17
- package/docs/content/locales/zh-TW/progress.md +15 -15
- package/docs/content/locales/zh-TW/pure-agent-apps.md +10 -10
- package/docs/content/locales/zh-TW/real-time-collaboration.md +103 -103
- package/docs/content/locales/zh-TW/recurring-jobs.md +19 -19
- package/docs/content/locales/zh-TW/routing.md +9 -9
- package/docs/content/locales/zh-TW/sandbox-adapters.md +48 -48
- package/docs/content/locales/zh-TW/security.md +49 -49
- package/docs/content/locales/zh-TW/server.md +47 -47
- package/docs/content/locales/zh-TW/sharing.md +22 -22
- package/docs/content/locales/zh-TW/skills-guide.md +46 -46
- package/docs/content/locales/zh-TW/template-analytics.md +40 -40
- package/docs/content/locales/zh-TW/template-assets.md +69 -69
- package/docs/content/locales/zh-TW/template-brain.md +81 -81
- package/docs/content/locales/zh-TW/template-calendar.md +40 -40
- package/docs/content/locales/zh-TW/template-chat.md +27 -27
- package/docs/content/locales/zh-TW/template-clips.md +85 -85
- package/docs/content/locales/zh-TW/template-content.md +65 -66
- package/docs/content/locales/zh-TW/template-design.md +29 -29
- package/docs/content/locales/zh-TW/template-dispatch.md +51 -51
- package/docs/content/locales/zh-TW/template-forms.md +25 -25
- package/docs/content/locales/zh-TW/template-mail.md +71 -71
- package/docs/content/locales/zh-TW/template-plan.md +92 -92
- package/docs/content/locales/zh-TW/template-slides.md +52 -52
- package/docs/content/locales/zh-TW/template-videos.md +30 -30
- package/docs/content/locales/zh-TW/tracking.md +29 -29
- package/docs/content/locales/zh-TW/using-your-agent.md +18 -18
- package/docs/content/locales/zh-TW/voice-input.md +19 -19
- package/docs/content/locales/zh-TW/what-is-agent-native.md +61 -61
- package/docs/content/locales/zh-TW/workspace-connections.md +41 -41
- package/docs/content/locales/zh-TW/workspace-management.md +27 -27
- package/docs/content/locales/zh-TW/workspace.md +58 -58
- package/docs/content/locales/zh-TW/writing-agent-instructions.md +41 -41
- package/docs/content/template-clips.md +48 -2
- package/package.json +2 -1
- package/src/templates/default/app/i18n/zh-TW.ts +44 -44
|
@@ -146,6 +146,10 @@ type NativeRecording = {
|
|
|
146
146
|
status: NativeRecordingStatus;
|
|
147
147
|
recordingUrl: string;
|
|
148
148
|
error: string | null;
|
|
149
|
+
// When an upload fails, the recording is saved to the user's Downloads as a
|
|
150
|
+
// fallback so it is never lost; these describe that saved file.
|
|
151
|
+
savedToDisk?: boolean;
|
|
152
|
+
savedFilename?: string;
|
|
149
153
|
};
|
|
150
154
|
|
|
151
155
|
type OffscreenStatusMessage = {
|
|
@@ -156,6 +160,8 @@ type OffscreenStatusMessage = {
|
|
|
156
160
|
result?: Record<string, unknown>;
|
|
157
161
|
error?: string;
|
|
158
162
|
storageSetupRequired?: boolean;
|
|
163
|
+
savedToDisk?: boolean;
|
|
164
|
+
savedFilename?: string;
|
|
159
165
|
};
|
|
160
166
|
|
|
161
167
|
type ExtensionErrorMessage = {
|
|
@@ -2032,6 +2038,36 @@ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
|
|
|
2032
2038
|
return { ok: true };
|
|
2033
2039
|
}
|
|
2034
2040
|
|
|
2041
|
+
// The offscreen recorder asks us to save a recording to disk when its upload
|
|
2042
|
+
// fails (storage not connected, network drop, size cap), so the recording is
|
|
2043
|
+
// never lost. The offscreen document holds the blob URL alive until its next
|
|
2044
|
+
// acquire(); we only need the download to be accepted, not fully written.
|
|
2045
|
+
if (type === "CLIPS_SAVE_RECORDING_TO_DISK") {
|
|
2046
|
+
const { url, filename } = message as { url?: string; filename?: string };
|
|
2047
|
+
if (typeof url !== "string" || !url) {
|
|
2048
|
+
return { ok: false, error: "Missing recording URL." };
|
|
2049
|
+
}
|
|
2050
|
+
try {
|
|
2051
|
+
const options: chrome.downloads.DownloadOptions = {
|
|
2052
|
+
url,
|
|
2053
|
+
saveAs: false,
|
|
2054
|
+
conflictAction: "uniquify",
|
|
2055
|
+
};
|
|
2056
|
+
if (typeof filename === "string" && filename) options.filename = filename;
|
|
2057
|
+
const downloadId = await chrome.downloads.download(options);
|
|
2058
|
+
return { ok: typeof downloadId === "number" && downloadId >= 0 };
|
|
2059
|
+
} catch (err) {
|
|
2060
|
+
console.error("[clips-bg] save-to-disk download failed", err);
|
|
2061
|
+
captureExtensionError(err, {
|
|
2062
|
+
tags: { surface: "background", messageType: type },
|
|
2063
|
+
});
|
|
2064
|
+
return {
|
|
2065
|
+
ok: false,
|
|
2066
|
+
error: err instanceof Error ? err.message : "Download failed.",
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2035
2071
|
// Status updates streamed from the offscreen recorder.
|
|
2036
2072
|
if (type === "CLIPS_NATIVE_STATUS") {
|
|
2037
2073
|
const status = message as OffscreenStatusMessage;
|
|
@@ -2060,6 +2096,13 @@ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
|
|
|
2060
2096
|
activeNativeRecording,
|
|
2061
2097
|
);
|
|
2062
2098
|
}
|
|
2099
|
+
if (status.status === "error") {
|
|
2100
|
+
activeNativeRecording.savedToDisk = status.savedToDisk === true;
|
|
2101
|
+
activeNativeRecording.savedFilename =
|
|
2102
|
+
typeof status.savedFilename === "string"
|
|
2103
|
+
? status.savedFilename
|
|
2104
|
+
: undefined;
|
|
2105
|
+
}
|
|
2063
2106
|
await saveActiveNativeRecording();
|
|
2064
2107
|
// The offscreen recorder finished its pre-roll and actually started —
|
|
2065
2108
|
// advance from countdown to recording (the reliable phase flip).
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { scheduleReadyChime } from "@shared/recording-audio";
|
|
20
20
|
import { chunkUploadUrl, pickMimeType } from "@shared/recording-core";
|
|
21
|
+
import { MAX_UPLOAD_BYTES } from "@shared/upload-limits";
|
|
21
22
|
|
|
22
23
|
import { captureExtensionError, initExtensionSentry } from "./sentry";
|
|
23
24
|
|
|
@@ -110,6 +111,15 @@ type ActiveRecording = {
|
|
|
110
111
|
chunkIndex: number;
|
|
111
112
|
uploadPromises: Promise<unknown>[];
|
|
112
113
|
uploadFailure: Error | null;
|
|
114
|
+
// Local safety buffer: every recorded blob is kept here (browser-managed,
|
|
115
|
+
// disk-backed Blob refs — not raw heap) so that if the upload fails (storage
|
|
116
|
+
// not connected, network drop, size cap) we can still save the finished
|
|
117
|
+
// recording to disk instead of losing it. Mirrors the web/desktop recorders.
|
|
118
|
+
recordedBlobs: Blob[];
|
|
119
|
+
recordedBytes: number;
|
|
120
|
+
// Set if the recording grew past the buffer ceiling and we stopped retaining
|
|
121
|
+
// — at that point a local save can't be guaranteed, so we don't promise one.
|
|
122
|
+
localBufferOverflow: boolean;
|
|
113
123
|
cancelled: boolean;
|
|
114
124
|
// Set when the recorder is being torn down to start over on the same source
|
|
115
125
|
// streams, so the stop handler skips the usual track cleanup.
|
|
@@ -133,8 +143,24 @@ type ActiveRecording = {
|
|
|
133
143
|
|
|
134
144
|
const UPLOAD_SLICE_BYTES = 3 * 1024 * 1024;
|
|
135
145
|
|
|
146
|
+
// Don't retain more than the upload ceiling — past it the server rejects the
|
|
147
|
+
// recording anyway, so there is nothing a local save could recover.
|
|
148
|
+
const MAX_LOCAL_BUFFER_BYTES = MAX_UPLOAD_BYTES;
|
|
149
|
+
// Below this, a failed recording is too short to be worth dumping a file into
|
|
150
|
+
// the user's Downloads (e.g. the storage gate was bypassed and the very first
|
|
151
|
+
// chunk was rejected ~2s in). The connect-storage message is enough there.
|
|
152
|
+
const MIN_LOCAL_SAVE_BYTES = 2 * 1024 * 1024;
|
|
153
|
+
|
|
136
154
|
let prepared: PreparedStreams | null = null;
|
|
137
155
|
let activeRecording: ActiveRecording | null = null;
|
|
156
|
+
// Blob URLs handed to the background for save-to-disk recovery downloads. Kept
|
|
157
|
+
// alive until the next recording starts so the download can finish reading.
|
|
158
|
+
const pendingSaveUrls = new Set<string>();
|
|
159
|
+
|
|
160
|
+
function releasePendingSaveUrls(): void {
|
|
161
|
+
for (const url of pendingSaveUrls) URL.revokeObjectURL(url);
|
|
162
|
+
pendingSaveUrls.clear();
|
|
163
|
+
}
|
|
138
164
|
|
|
139
165
|
function reportStatus(
|
|
140
166
|
sessionId: string,
|
|
@@ -454,6 +480,20 @@ async function uploadChunk(
|
|
|
454
480
|
return data;
|
|
455
481
|
}
|
|
456
482
|
|
|
483
|
+
// Keep a local copy of every recorded blob so a failed upload can still be
|
|
484
|
+
// saved to disk. Blob references are browser-managed (often disk-backed), so
|
|
485
|
+
// this is far cheaper than holding ArrayBuffers. We stop retaining past the
|
|
486
|
+
// upload ceiling (a larger recording can't be uploaded anyway).
|
|
487
|
+
function retainRecordedBlob(recording: ActiveRecording, blob: Blob): void {
|
|
488
|
+
if (recording.localBufferOverflow) return;
|
|
489
|
+
if (recording.recordedBytes + blob.size > MAX_LOCAL_BUFFER_BYTES) {
|
|
490
|
+
recording.localBufferOverflow = true;
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
recording.recordedBlobs.push(blob);
|
|
494
|
+
recording.recordedBytes += blob.size;
|
|
495
|
+
}
|
|
496
|
+
|
|
457
497
|
async function uploadBlobInSlices(
|
|
458
498
|
recording: ActiveRecording,
|
|
459
499
|
blob: Blob,
|
|
@@ -501,6 +541,8 @@ async function acquire(message: AcquireMessage): Promise<{
|
|
|
501
541
|
// Discard any half-prepared capture from a cancelled attempt.
|
|
502
542
|
stopPreparedStreams();
|
|
503
543
|
disposePrepared();
|
|
544
|
+
// A prior recording's recovery download has finished by now; free its URL.
|
|
545
|
+
releasePendingSaveUrls();
|
|
504
546
|
|
|
505
547
|
let displayStream: MediaStream | null = null;
|
|
506
548
|
let micStream: MediaStream | null = null;
|
|
@@ -649,6 +691,9 @@ async function begin(message: BeginMessage): Promise<{
|
|
|
649
691
|
chunkIndex: 0,
|
|
650
692
|
uploadPromises: [],
|
|
651
693
|
uploadFailure: null,
|
|
694
|
+
recordedBlobs: [],
|
|
695
|
+
recordedBytes: 0,
|
|
696
|
+
localBufferOverflow: false,
|
|
652
697
|
cancelled: false,
|
|
653
698
|
restarting: false,
|
|
654
699
|
startTimer: null,
|
|
@@ -671,14 +716,15 @@ async function begin(message: BeginMessage): Promise<{
|
|
|
671
716
|
activeRecording = recording;
|
|
672
717
|
|
|
673
718
|
recorder.addEventListener("dataavailable", (event) => {
|
|
674
|
-
if (
|
|
675
|
-
recording.cancelled ||
|
|
676
|
-
!event.data ||
|
|
677
|
-
event.data.size === 0 ||
|
|
678
|
-
recording.uploadFailure
|
|
679
|
-
) {
|
|
719
|
+
if (recording.cancelled || !event.data || event.data.size === 0) {
|
|
680
720
|
return;
|
|
681
721
|
}
|
|
722
|
+
// Always keep a local copy first — even after an upload failure — so the
|
|
723
|
+
// saved-to-disk fallback can assemble the COMPLETE recording. Without this,
|
|
724
|
+
// a recording whose upload is rejected (storage disconnected, network drop,
|
|
725
|
+
// size cap) would be lost; the extension has no other on-disk copy.
|
|
726
|
+
retainRecordedBlob(recording, event.data);
|
|
727
|
+
if (recording.uploadFailure) return;
|
|
682
728
|
// Record the failure and stop, but do NOT re-throw: re-throwing leaves a
|
|
683
729
|
// rejected promise that surfaces as an "Uncaught (in promise)" error (bad
|
|
684
730
|
// look in a Chrome Web Store review). finalizeStop reads recording.upload-
|
|
@@ -782,6 +828,61 @@ function startRecorderNow(recording: ActiveRecording): void {
|
|
|
782
828
|
}
|
|
783
829
|
}
|
|
784
830
|
|
|
831
|
+
function recordingDownloadFilename(recording: ActiveRecording): string {
|
|
832
|
+
const ext = /mp4/i.test(recording.mimeType) ? "mp4" : "webm";
|
|
833
|
+
const stamp = new Date(recording.startedAtMs || Date.now())
|
|
834
|
+
.toISOString()
|
|
835
|
+
.replace(/[:.]/g, "-")
|
|
836
|
+
.replace("T", "_")
|
|
837
|
+
.slice(0, 19);
|
|
838
|
+
return `clip-${stamp}.${ext}`;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Last-resort recovery: if a finished recording can't be uploaded, save the
|
|
842
|
+
// locally-buffered bytes to the user's Downloads so the recording is never
|
|
843
|
+
// lost. The offscreen document can't call chrome.downloads, so it hands the
|
|
844
|
+
// background a blob URL to download. The URL is revoked on the next acquire().
|
|
845
|
+
async function saveRecordingToDisk(
|
|
846
|
+
recording: ActiveRecording,
|
|
847
|
+
): Promise<{ savedToDisk: boolean; savedFilename?: string }> {
|
|
848
|
+
if (
|
|
849
|
+
recording.cancelled ||
|
|
850
|
+
recording.localBufferOverflow ||
|
|
851
|
+
recording.recordedBlobs.length === 0 ||
|
|
852
|
+
recording.recordedBytes < MIN_LOCAL_SAVE_BYTES
|
|
853
|
+
) {
|
|
854
|
+
return { savedToDisk: false };
|
|
855
|
+
}
|
|
856
|
+
try {
|
|
857
|
+
const blob = new Blob(recording.recordedBlobs, {
|
|
858
|
+
type: recording.mimeType,
|
|
859
|
+
});
|
|
860
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
861
|
+
pendingSaveUrls.add(objectUrl);
|
|
862
|
+
const filename = recordingDownloadFilename(recording);
|
|
863
|
+
const response = (await chrome.runtime.sendMessage({
|
|
864
|
+
type: "CLIPS_SAVE_RECORDING_TO_DISK",
|
|
865
|
+
sessionId: recording.sessionId,
|
|
866
|
+
url: objectUrl,
|
|
867
|
+
filename,
|
|
868
|
+
})) as { ok?: boolean } | undefined;
|
|
869
|
+
if (response?.ok) return { savedToDisk: true, savedFilename: filename };
|
|
870
|
+
// The download was not accepted — drop the URL we just created.
|
|
871
|
+
pendingSaveUrls.delete(objectUrl);
|
|
872
|
+
URL.revokeObjectURL(objectUrl);
|
|
873
|
+
return { savedToDisk: false };
|
|
874
|
+
} catch (err) {
|
|
875
|
+
captureExtensionError(err, {
|
|
876
|
+
tags: { surface: "offscreen", recordingStep: "save-to-disk" },
|
|
877
|
+
extra: {
|
|
878
|
+
recordingId: recording.recordingId,
|
|
879
|
+
bytes: recording.recordedBytes,
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
return { savedToDisk: false };
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
785
886
|
async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
786
887
|
if (recording.restarting) {
|
|
787
888
|
// restart() re-homes the source streams; do not stop or upload anything.
|
|
@@ -866,11 +967,15 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
866
967
|
durationMs: Math.max(0, Date.now() - recording.startedAtMs),
|
|
867
968
|
},
|
|
868
969
|
});
|
|
970
|
+
// The upload failed — save the buffered recording to disk so it isn't lost.
|
|
971
|
+
const saved = await saveRecordingToDisk(recording);
|
|
869
972
|
reportStatus(recording.sessionId, "error", {
|
|
870
973
|
recordingId: recording.recordingId,
|
|
871
974
|
error: error.message,
|
|
872
975
|
storageSetupRequired: (error as { storageSetupRequired?: boolean })
|
|
873
976
|
.storageSetupRequired,
|
|
977
|
+
savedToDisk: saved.savedToDisk,
|
|
978
|
+
savedFilename: saved.savedFilename,
|
|
874
979
|
});
|
|
875
980
|
recording.rejectStopped(error);
|
|
876
981
|
}
|
|
@@ -45,6 +45,8 @@ type NativeRecording = {
|
|
|
45
45
|
status: NativeRecordingStatus;
|
|
46
46
|
recordingUrl: string;
|
|
47
47
|
error: string | null;
|
|
48
|
+
savedToDisk?: boolean;
|
|
49
|
+
savedFilename?: string;
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
type PopupStatusResponse = {
|
|
@@ -719,22 +721,31 @@ function renderActiveRecording(recording: NativeRecording | null): void {
|
|
|
719
721
|
titleKey && hostKey && (titleKey === hostKey || hostKey.includes(titleKey));
|
|
720
722
|
recordingUrl.textContent = duplicate ? "" : host;
|
|
721
723
|
recordingUrl.hidden = !host || Boolean(duplicate);
|
|
724
|
+
const storageFailure = isStorageSetupFailureMessage(recording.error);
|
|
725
|
+
let errorText = storageFailure
|
|
726
|
+
? STORAGE_SETUP_REQUIRED_MESSAGE
|
|
727
|
+
: recording.error || "Recording needs attention";
|
|
728
|
+
// If the upload failed but we saved the recording to disk, lead with the
|
|
729
|
+
// reassurance (it's not lost) and the re-upload action.
|
|
730
|
+
if (recording.status === "error" && recording.savedToDisk) {
|
|
731
|
+
const named = recording.savedFilename
|
|
732
|
+
? ` (${recording.savedFilename})`
|
|
733
|
+
: "";
|
|
734
|
+
errorText = storageFailure
|
|
735
|
+
? `Couldn't upload — storage isn't connected. Your clip is saved to your downloads${named}. Connect storage, then re-upload it with "Upload video".`
|
|
736
|
+
: `Couldn't upload your clip. It's saved to your downloads${named} — try re-uploading it with "Upload video".`;
|
|
737
|
+
}
|
|
722
738
|
recordingStatus.textContent =
|
|
723
739
|
recording.status === "uploading"
|
|
724
740
|
? "Saving..."
|
|
725
741
|
: recording.status === "stopping"
|
|
726
742
|
? "Stopping..."
|
|
727
743
|
: recording.status === "error"
|
|
728
|
-
?
|
|
729
|
-
? STORAGE_SETUP_REQUIRED_MESSAGE
|
|
730
|
-
: recording.error || "Recording needs attention"
|
|
744
|
+
? errorText
|
|
731
745
|
: `Recording ${formatDuration(recording.startedAtMs)}`;
|
|
732
746
|
recordingStatus.dataset.kind =
|
|
733
747
|
recording.status === "error" ? "error" : "info";
|
|
734
|
-
setStorageHelp(
|
|
735
|
-
recording.status === "error" &&
|
|
736
|
-
isStorageSetupFailureMessage(recording.error),
|
|
737
|
-
);
|
|
748
|
+
setStorageHelp(recording.status === "error" && storageFailure);
|
|
738
749
|
}
|
|
739
750
|
|
|
740
751
|
async function init(): Promise<void> {
|
|
@@ -204,9 +204,21 @@ function serverUrlForPendingUpload(
|
|
|
204
204
|
return normalizedCurrent || normalizeServerUrl(upload.serverUrl || "");
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
|
|
207
|
+
// "configured"/"missing" are definitive answers from the server; "unknown"
|
|
208
|
+
// means the check could not be completed (network error, unreachable server, or
|
|
209
|
+
// an unparseable/non-OK response). An "unknown" result must never downgrade an
|
|
210
|
+
// already-connected user to the setup flow.
|
|
211
|
+
type VideoStorageProbe = "configured" | "missing" | "unknown";
|
|
212
|
+
|
|
213
|
+
async function hasConfiguredVideoStorage(
|
|
214
|
+
serverUrl: string,
|
|
215
|
+
): Promise<VideoStorageProbe> {
|
|
208
216
|
const base = serverUrl.replace(/\/+$/, "");
|
|
209
217
|
|
|
218
|
+
// Track whether any endpoint gave a definitive answer. If both checks throw
|
|
219
|
+
// or return non-OK/unparseable responses, we can't tell and return "unknown".
|
|
220
|
+
let sawDefinitiveAnswer = false;
|
|
221
|
+
|
|
210
222
|
try {
|
|
211
223
|
const uploadStatus = await fetch(
|
|
212
224
|
`${base}/_agent-native/file-upload/status`,
|
|
@@ -215,12 +227,15 @@ async function hasConfiguredVideoStorage(serverUrl: string): Promise<boolean> {
|
|
|
215
227
|
cache: "no-store",
|
|
216
228
|
},
|
|
217
229
|
);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
230
|
+
if (uploadStatus.ok) {
|
|
231
|
+
const body = (await uploadStatus.json().catch(() => null)) as {
|
|
232
|
+
configured?: boolean;
|
|
233
|
+
} | null;
|
|
234
|
+
if (body) {
|
|
235
|
+
sawDefinitiveAnswer = true;
|
|
236
|
+
if (body.configured) return "configured";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
224
239
|
} catch {
|
|
225
240
|
// Fall through to the Builder status endpoint.
|
|
226
241
|
}
|
|
@@ -230,15 +245,20 @@ async function hasConfiguredVideoStorage(serverUrl: string): Promise<boolean> {
|
|
|
230
245
|
credentials: "include",
|
|
231
246
|
cache: "no-store",
|
|
232
247
|
});
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
248
|
+
if (builderStatus.ok) {
|
|
249
|
+
const body = (await builderStatus.json().catch(() => null)) as {
|
|
250
|
+
configured?: boolean;
|
|
251
|
+
} | null;
|
|
252
|
+
if (body) {
|
|
253
|
+
sawDefinitiveAnswer = true;
|
|
254
|
+
if (body.configured) return "configured";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
239
257
|
} catch {
|
|
240
|
-
|
|
258
|
+
// Network error or unreachable server — treat as indeterminate below.
|
|
241
259
|
}
|
|
260
|
+
|
|
261
|
+
return sawDefinitiveAnswer ? "missing" : "unknown";
|
|
242
262
|
}
|
|
243
263
|
|
|
244
264
|
function authTokenStorageKey(serverUrl: string): string {
|
|
@@ -658,10 +678,21 @@ export function App() {
|
|
|
658
678
|
return true;
|
|
659
679
|
}
|
|
660
680
|
|
|
661
|
-
setVideoStorageStatus("checking");
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
681
|
+
setVideoStorageStatus((prev) => (prev === "missing" ? prev : "checking"));
|
|
682
|
+
const probe = await hasConfiguredVideoStorage(serverUrl);
|
|
683
|
+
if (probe === "unknown") {
|
|
684
|
+
// The check couldn't be completed (offline/unreachable). Never downgrade
|
|
685
|
+
// an already-connected user to "missing" on an indeterminate result;
|
|
686
|
+
// preserve the last known status and let the poll retry. If we never
|
|
687
|
+
// determined a status, fall back to "checking" so the poll keeps trying
|
|
688
|
+
// rather than hard-blocking the record button.
|
|
689
|
+
setVideoStorageStatus((prev) =>
|
|
690
|
+
prev === "configured" || prev === "missing" ? prev : "checking",
|
|
691
|
+
);
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
setVideoStorageStatus(probe);
|
|
695
|
+
return probe === "configured";
|
|
665
696
|
}, [authStatus, localRecordingMode, serverUrl]);
|
|
666
697
|
|
|
667
698
|
useEffect(() => {
|
|
@@ -672,7 +703,10 @@ export function App() {
|
|
|
672
703
|
if (
|
|
673
704
|
authStatus !== "authed" ||
|
|
674
705
|
localRecordingMode !== "off" ||
|
|
675
|
-
|
|
706
|
+
// Re-poll while storage is "missing" (server may become configured) and
|
|
707
|
+
// while still "checking" (an indeterminate/unreachable first probe should
|
|
708
|
+
// keep retrying instead of hard-blocking the record button).
|
|
709
|
+
(videoStorageStatus !== "missing" && videoStorageStatus !== "checking")
|
|
676
710
|
) {
|
|
677
711
|
return;
|
|
678
712
|
}
|
|
@@ -253,6 +253,32 @@ export const recordingBrowserDiagnostics = table(
|
|
|
253
253
|
},
|
|
254
254
|
);
|
|
255
255
|
|
|
256
|
+
export const recordingBugReports = table("recording_bug_reports", {
|
|
257
|
+
recordingId: text("recording_id").primaryKey(),
|
|
258
|
+
ownerEmail: text("owner_email").notNull().default("local@localhost"),
|
|
259
|
+
organizationId: text("workspace_id").notNull(),
|
|
260
|
+
orgId: text("org_id"),
|
|
261
|
+
projectId: text("project_id"),
|
|
262
|
+
title: text("title"),
|
|
263
|
+
description: text("description").notNull().default(""),
|
|
264
|
+
severity: text("severity", {
|
|
265
|
+
enum: ["low", "normal", "high", "urgent"],
|
|
266
|
+
})
|
|
267
|
+
.notNull()
|
|
268
|
+
.default("normal"),
|
|
269
|
+
sourceUrl: text("source_url"),
|
|
270
|
+
pageTitle: text("page_title"),
|
|
271
|
+
appVersion: text("app_version"),
|
|
272
|
+
environment: text("environment"),
|
|
273
|
+
reporterEmail: text("reporter_email"),
|
|
274
|
+
reporterName: text("reporter_name"),
|
|
275
|
+
reporterId: text("reporter_id"),
|
|
276
|
+
metadataJson: text("metadata_json").notNull().default("{}"),
|
|
277
|
+
submittedAt: text("submitted_at").notNull().default(now()),
|
|
278
|
+
createdAt: text("created_at").notNull().default(now()),
|
|
279
|
+
updatedAt: text("updated_at").notNull().default(now()),
|
|
280
|
+
});
|
|
281
|
+
|
|
256
282
|
export const recordingCtas = table("recording_ctas", {
|
|
257
283
|
id: text("id").primaryKey(),
|
|
258
284
|
recordingId: text("recording_id").notNull(),
|
|
@@ -33,6 +33,9 @@ export type PublicAgentRecording = typeof schema.recordings._.inferSelect;
|
|
|
33
33
|
export type PublicAgentTranscript =
|
|
34
34
|
| typeof schema.recordingTranscripts._.inferSelect
|
|
35
35
|
| null;
|
|
36
|
+
export type PublicAgentBugReport =
|
|
37
|
+
| typeof schema.recordingBugReports._.inferSelect
|
|
38
|
+
| null;
|
|
36
39
|
|
|
37
40
|
export interface PublicAgentAccess {
|
|
38
41
|
recording: PublicAgentRecording;
|
|
@@ -337,6 +340,17 @@ export async function loadAgentBrowserDiagnostics(recordingId: string) {
|
|
|
337
340
|
return parseBrowserDiagnosticsRow(row);
|
|
338
341
|
}
|
|
339
342
|
|
|
343
|
+
export async function loadAgentBugReport(
|
|
344
|
+
recordingId: string,
|
|
345
|
+
): Promise<PublicAgentBugReport> {
|
|
346
|
+
const [row] = await getDb()
|
|
347
|
+
.select()
|
|
348
|
+
.from(schema.recordingBugReports)
|
|
349
|
+
.where(eq(schema.recordingBugReports.recordingId, recordingId))
|
|
350
|
+
.limit(1);
|
|
351
|
+
return row ?? null;
|
|
352
|
+
}
|
|
353
|
+
|
|
340
354
|
// Most-recent console logs / network requests surfaced in the public agent
|
|
341
355
|
// context. Bounded to keep the agent payload small; the summary still reports
|
|
342
356
|
// the true total counts.
|
|
@@ -412,6 +426,35 @@ function compactBrowserDiagnostics(diagnostics: BrowserDiagnosticsData | null) {
|
|
|
412
426
|
};
|
|
413
427
|
}
|
|
414
428
|
|
|
429
|
+
function safeMetadataObject(raw: string | null | undefined) {
|
|
430
|
+
if (!raw) return {};
|
|
431
|
+
try {
|
|
432
|
+
const parsed = JSON.parse(raw);
|
|
433
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
434
|
+
? parsed
|
|
435
|
+
: {};
|
|
436
|
+
} catch {
|
|
437
|
+
return {};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function compactBugReport(report: PublicAgentBugReport) {
|
|
442
|
+
if (!report) return null;
|
|
443
|
+
return {
|
|
444
|
+
projectId: report.projectId,
|
|
445
|
+
title: report.title,
|
|
446
|
+
description: report.description,
|
|
447
|
+
severity: report.severity,
|
|
448
|
+
sourceUrl: report.sourceUrl,
|
|
449
|
+
pageTitle: report.pageTitle,
|
|
450
|
+
appVersion: report.appVersion,
|
|
451
|
+
environment: report.environment,
|
|
452
|
+
metadata: safeMetadataObject(report.metadataJson),
|
|
453
|
+
submittedAt: report.submittedAt,
|
|
454
|
+
note: "Reporter identity fields are omitted from public agent context. Host metadata is redacted and bounded at capture time.",
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
415
458
|
export function buildPublicAgentContext({
|
|
416
459
|
event,
|
|
417
460
|
access,
|
|
@@ -420,6 +463,7 @@ export function buildPublicAgentContext({
|
|
|
420
463
|
chapters,
|
|
421
464
|
ctas,
|
|
422
465
|
browserDiagnostics,
|
|
466
|
+
bugReport,
|
|
423
467
|
}: {
|
|
424
468
|
event: H3Event;
|
|
425
469
|
access: PublicAgentAccess;
|
|
@@ -428,6 +472,7 @@ export function buildPublicAgentContext({
|
|
|
428
472
|
chapters: ReturnType<typeof parseAgentChapters>;
|
|
429
473
|
ctas: Awaited<ReturnType<typeof loadAgentCtas>>;
|
|
430
474
|
browserDiagnostics?: BrowserDiagnosticsData | null;
|
|
475
|
+
bugReport?: PublicAgentBugReport;
|
|
431
476
|
}) {
|
|
432
477
|
const recording = access.recording;
|
|
433
478
|
const requestUrl = getRequestURL(event);
|
|
@@ -451,6 +496,11 @@ export function buildPublicAgentContext({
|
|
|
451
496
|
}));
|
|
452
497
|
const instructions = [
|
|
453
498
|
"Use transcript.segments for timestamped spoken context.",
|
|
499
|
+
...(bugReport
|
|
500
|
+
? [
|
|
501
|
+
"Use bugReport for the submitted product context: source URL, page title, app version, environment, severity, and redacted host metadata.",
|
|
502
|
+
]
|
|
503
|
+
: []),
|
|
454
504
|
...(browserDiagnostics
|
|
455
505
|
? [
|
|
456
506
|
"Use browserDiagnostics.consoleLogs for the redacted console stream (all levels: debug/log/info/warn/error) and browserDiagnostics.networkRequests for the fetch/XHR requests (method, sanitized URL, status, duration) captured during the recording. browserDiagnostics.consoleIssues highlights just the warnings/errors, and browserDiagnostics.failedNetworkRequests highlights failed requests.",
|
|
@@ -515,6 +565,7 @@ export function buildPublicAgentContext({
|
|
|
515
565
|
},
|
|
516
566
|
chapters,
|
|
517
567
|
recommendedFrames: suggestedFrames,
|
|
568
|
+
bugReport: compactBugReport(bugReport ?? null),
|
|
518
569
|
browserDiagnostics: compactBrowserDiagnostics(browserDiagnostics ?? null),
|
|
519
570
|
ctas: ctas.map((cta) => ({
|
|
520
571
|
label: cta.label,
|
|
@@ -22,6 +22,7 @@ export default createAuthPlugin({
|
|
|
22
22
|
"/r",
|
|
23
23
|
"/embed",
|
|
24
24
|
"/download",
|
|
25
|
+
"/bug-report",
|
|
25
26
|
// React Router's lazy route-discovery endpoint. If this is gated by
|
|
26
27
|
// auth it returns an HTML login page; the client tries to parse it
|
|
27
28
|
// as JSON, fails, and can't resolve any public route the user lands
|
|
@@ -721,6 +721,34 @@ const migrations = runMigrations(
|
|
|
721
721
|
`CREATE INDEX IF NOT EXISTS slack_installations_org_idx ON slack_installations (org_id, created_at)`,
|
|
722
722
|
].join("; "),
|
|
723
723
|
},
|
|
724
|
+
{
|
|
725
|
+
version: 44,
|
|
726
|
+
sql: [
|
|
727
|
+
`CREATE TABLE IF NOT EXISTS recording_bug_reports (
|
|
728
|
+
recording_id TEXT PRIMARY KEY,
|
|
729
|
+
owner_email TEXT NOT NULL DEFAULT 'local@localhost',
|
|
730
|
+
workspace_id TEXT NOT NULL,
|
|
731
|
+
org_id TEXT,
|
|
732
|
+
project_id TEXT,
|
|
733
|
+
title TEXT,
|
|
734
|
+
description TEXT NOT NULL DEFAULT '',
|
|
735
|
+
severity TEXT NOT NULL DEFAULT 'normal',
|
|
736
|
+
source_url TEXT,
|
|
737
|
+
page_title TEXT,
|
|
738
|
+
app_version TEXT,
|
|
739
|
+
environment TEXT,
|
|
740
|
+
reporter_email TEXT,
|
|
741
|
+
reporter_name TEXT,
|
|
742
|
+
reporter_id TEXT,
|
|
743
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
744
|
+
submitted_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
745
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
746
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
747
|
+
)`,
|
|
748
|
+
`CREATE INDEX IF NOT EXISTS recording_bug_reports_owner_idx ON recording_bug_reports (owner_email, updated_at)`,
|
|
749
|
+
`CREATE INDEX IF NOT EXISTS recording_bug_reports_project_idx ON recording_bug_reports (project_id, updated_at)`,
|
|
750
|
+
].join("; "),
|
|
751
|
+
},
|
|
724
752
|
],
|
|
725
753
|
{ table: "clips_migrations" },
|
|
726
754
|
);
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
applyAgentJsonHeaders,
|
|
18
18
|
buildPublicAgentContext,
|
|
19
|
+
loadAgentBugReport,
|
|
19
20
|
loadAgentBrowserDiagnostics,
|
|
20
21
|
loadAgentCtas,
|
|
21
22
|
loadAgentTranscript,
|
|
@@ -40,11 +41,12 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
const recording = accessResult.access.recording;
|
|
43
|
-
const [{ transcript, agentSegments }, ctas, browserDiagnostics] =
|
|
44
|
+
const [{ transcript, agentSegments }, ctas, browserDiagnostics, bugReport] =
|
|
44
45
|
await Promise.all([
|
|
45
46
|
loadAgentTranscript(recording.id, recording.durationMs),
|
|
46
47
|
loadAgentCtas(recording.id),
|
|
47
48
|
loadAgentBrowserDiagnostics(recording.id),
|
|
49
|
+
loadAgentBugReport(recording.id),
|
|
48
50
|
]);
|
|
49
51
|
const chapters = parseAgentChapters(recording);
|
|
50
52
|
|
|
@@ -56,5 +58,6 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
56
58
|
chapters,
|
|
57
59
|
ctas,
|
|
58
60
|
browserDiagnostics,
|
|
61
|
+
bugReport,
|
|
59
62
|
});
|
|
60
63
|
});
|