@agent-native/core 0.69.0 → 0.70.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -15
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +109 -0
- package/corpus/core/docs/design/durable-agent-runs.md +217 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +89 -0
- package/corpus/core/src/agent/model-config.ts +0 -2
- package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
- package/corpus/core/src/changelog/parse.ts +245 -0
- package/corpus/core/src/cli/changelog.ts +199 -0
- package/corpus/core/src/cli/index.ts +19 -0
- package/corpus/core/src/cli/recap.ts +24 -1
- package/corpus/core/src/cli/skills.ts +74 -47
- package/corpus/core/src/client/AgentPanel.tsx +5 -3
- package/corpus/core/src/client/AssistantChat.tsx +8 -0
- package/corpus/core/src/client/CommandMenu.tsx +183 -75
- package/corpus/core/src/client/agent-chat-adapter.ts +44 -7
- package/corpus/core/src/client/analytics.ts +181 -0
- package/corpus/core/src/client/changelog/Changelog.tsx +321 -0
- package/corpus/core/src/client/client-surface.ts +41 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
- package/corpus/core/src/client/feedback-context.ts +4 -0
- package/corpus/core/src/client/index.ts +12 -0
- package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +54 -0
- package/corpus/core/src/client/settings/useBuilderStatus.ts +82 -20
- package/corpus/core/src/client/sse-event-processor.ts +7 -1
- package/corpus/core/src/mcp/build-server.ts +57 -1
- package/corpus/core/src/mcp/embed-app.ts +197 -7
- package/corpus/core/src/mcp/oauth-route.ts +109 -13
- package/corpus/core/src/server/attribution.ts +211 -0
- package/corpus/core/src/server/auth.ts +6 -0
- package/corpus/core/src/server/better-auth-instance.ts +49 -5
- package/corpus/core/src/server/builder-browser.ts +27 -1
- package/corpus/core/src/server/core-routes-plugin.ts +136 -0
- package/corpus/core/src/server/google-oauth.ts +19 -6
- package/corpus/core/src/server/onboarding-html.ts +35 -0
- package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
- package/corpus/core/src/styles/agent-conversation.css +21 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
- package/corpus/templates/analytics/AGENTS.md +8 -1
- package/corpus/templates/analytics/CHANGELOG.md +10 -0
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +99 -4
- package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +39 -8
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
- package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +180 -144
- package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -3
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -5
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +221 -215
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +6 -6
- package/corpus/templates/analytics/app/lib/demo-dashboard-path.ts +1 -1
- package/corpus/templates/analytics/app/lib/last-opened.ts +2 -1
- package/corpus/templates/analytics/app/pages/Settings.tsx +4 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +2 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +101 -99
- package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +1 -1
- package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +18 -5
- package/corpus/templates/analytics/app/routes/catalog.tsx +1 -1
- package/corpus/templates/analytics/app/routes/dashboards.$id.tsx +9 -0
- package/corpus/templates/analytics/app/routes/traffic.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-23-the-dashboards-and-analyses-sidebar-settings-buttons-now-sta.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +145 -0
- package/corpus/templates/analytics/server/db/index.ts +1 -1
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -2
- package/corpus/templates/analytics/server/lib/demo-dashboards.ts +1 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -2
- package/corpus/templates/analytics/server/plugins/core-routes.ts +6 -6
- package/corpus/templates/assets/CHANGELOG.md +10 -0
- package/corpus/templates/assets/actions/generate-image.ts +24 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
- package/corpus/templates/assets/app/global.css +17 -1
- package/corpus/templates/assets/app/root.tsx +7 -1
- package/corpus/templates/assets/app/routes/_index.tsx +94 -14
- package/corpus/templates/assets/app/routes/settings.tsx +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
- package/corpus/templates/brain/CHANGELOG.md +10 -0
- package/corpus/templates/brain/app/root.tsx +7 -1
- package/corpus/templates/brain/app/routes/settings.tsx +8 -1
- package/corpus/templates/calendar/CHANGELOG.md +10 -0
- package/corpus/templates/calendar/app/pages/Settings.tsx +4 -0
- package/corpus/templates/calendar/app/root.tsx +7 -1
- package/corpus/templates/chat/CHANGELOG.md +10 -0
- package/corpus/templates/chat/app/root.tsx +7 -1
- package/corpus/templates/clips/CHANGELOG.md +10 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +33 -7
- package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +8 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +150 -20
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +7 -11
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +31 -0
- package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +71 -0
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
- package/corpus/templates/clips/app/root.tsx +7 -1
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +15 -1
- package/corpus/templates/clips/app/routes/record.tsx +78 -5
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -5
- package/corpus/templates/clips/changelog/2026-06-23-clips-shared-in-slack-no-longer-show-a-could-not-start-playb.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-made-the-play-button-on-shared-and-embedded-clips-scale-with.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-public-clip-share-links-now-show-whether-they-ll-play-inline.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-screen-camera-recordings-now-keep-the-presenter-s-face-in-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
- package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
- package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
- package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
- package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
- package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
- package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
- package/corpus/templates/clips/desktop/src/overlays/region-record-border.tsx +76 -0
- package/corpus/templates/clips/desktop/src/styles.css +119 -2
- package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +140 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +4 -1
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +85 -6
- package/corpus/templates/clips/shared/share-attribution.ts +79 -0
- package/corpus/templates/content/AGENTS.md +20 -3
- package/corpus/templates/content/CHANGELOG.md +10 -0
- package/corpus/templates/content/actions/_database-utils.ts +15 -0
- package/corpus/templates/content/actions/_property-utils.ts +371 -4
- package/corpus/templates/content/actions/configure-document-property.ts +35 -1
- package/corpus/templates/content/actions/create-content-database.ts +5 -1
- package/corpus/templates/content/actions/delete-document-property.ts +52 -2
- package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
- package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
- package/corpus/templates/content/actions/set-document-property.ts +39 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
- package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
- package/corpus/templates/content/app/root.tsx +7 -1
- package/corpus/templates/content/server/db/schema.ts +30 -0
- package/corpus/templates/content/server/plugins/db.ts +87 -0
- package/corpus/templates/content/shared/api.ts +7 -0
- package/corpus/templates/content/shared/properties.ts +109 -0
- package/corpus/templates/design/CHANGELOG.md +10 -0
- package/corpus/templates/design/actions/generate-design.ts +4 -0
- package/corpus/templates/design/app/root.tsx +7 -1
- package/corpus/templates/design/app/routes/settings.tsx +10 -2
- package/corpus/templates/dispatch/CHANGELOG.md +10 -0
- package/corpus/templates/dispatch/app/root.tsx +7 -1
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
- package/corpus/templates/forms/CHANGELOG.md +10 -0
- package/corpus/templates/forms/actions/export-responses.ts +6 -0
- package/corpus/templates/forms/actions/list-responses.ts +2 -0
- package/corpus/templates/forms/actions/response-insights.ts +2 -0
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
- package/corpus/templates/forms/app/root.tsx +7 -1
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
- package/corpus/templates/forms/server/db/schema.ts +7 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
- package/corpus/templates/forms/server/lib/integrations.ts +76 -2
- package/corpus/templates/forms/server/plugins/db.ts +20 -0
- package/corpus/templates/forms/shared/types.ts +12 -0
- package/corpus/templates/macros/CHANGELOG.md +10 -0
- package/corpus/templates/macros/app/root.tsx +7 -1
- package/corpus/templates/mail/CHANGELOG.md +10 -0
- package/corpus/templates/mail/app/components/layout/CommandPalette.tsx +3 -0
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +27 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +31 -17
- package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -4
- package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +34 -24
- package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -4
- package/corpus/templates/plan/AGENTS.md +4 -0
- package/corpus/templates/plan/CHANGELOG.md +10 -0
- package/corpus/templates/plan/actions/view-screen.ts +5 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +98 -1
- package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
- package/corpus/templates/plan/app/pages/PlansPage.tsx +107 -7
- package/corpus/templates/plan/app/root.tsx +7 -1
- package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
- package/corpus/templates/plan/shared/share-attribution.ts +72 -0
- package/corpus/templates/slides/CHANGELOG.md +10 -0
- package/corpus/templates/slides/app/root.tsx +7 -1
- package/corpus/templates/videos/CHANGELOG.md +4 -151
- package/corpus/templates/videos/app/root.tsx +7 -1
- package/dist/action.js +87 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/engine/builder-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/model-config.d.ts +2 -2
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +0 -2
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +13 -0
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +44 -0
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/changelog/parse.d.ts +70 -0
- package/dist/changelog/parse.d.ts.map +1 -0
- package/dist/changelog/parse.js +200 -0
- package/dist/changelog/parse.js.map +1 -0
- package/dist/cli/changelog.d.ts +4 -0
- package/dist/cli/changelog.d.ts.map +1 -0
- package/dist/cli/changelog.js +166 -0
- package/dist/cli/changelog.js.map +1 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +22 -1
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -3
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +74 -47
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/CommandMenu.d.ts +17 -1
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +49 -13
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +43 -6
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/analytics.d.ts +18 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +177 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/changelog/Changelog.d.ts +47 -0
- package/dist/client/changelog/Changelog.d.ts.map +1 -0
- package/dist/client/changelog/Changelog.js +138 -0
- package/dist/client/changelog/Changelog.js.map +1 -0
- package/dist/client/client-surface.d.ts +17 -0
- package/dist/client/client-surface.d.ts.map +1 -0
- package/dist/client/client-surface.js +24 -0
- package/dist/client/client-surface.js.map +1 -0
- package/dist/client/composer/TiptapComposer.d.ts +26 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +27 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/feedback-context.d.ts +3 -0
- package/dist/client/feedback-context.d.ts.map +1 -1
- package/dist/client/feedback-context.js +2 -0
- package/dist/client/feedback-context.js.map +1 -1
- package/dist/client/index.d.ts +3 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
- package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.js +42 -1
- package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +2 -0
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +58 -10
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +21 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +43 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +197 -7
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/oauth-route.d.ts.map +1 -1
- package/dist/mcp/oauth-route.js +95 -12
- package/dist/mcp/oauth-route.js.map +1 -1
- package/dist/server/attribution.d.ts +83 -0
- package/dist/server/attribution.d.ts.map +1 -0
- package/dist/server/attribution.js +178 -0
- package/dist/server/attribution.js.map +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +7 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts +9 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +32 -2
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/builder-browser.d.ts +4 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +20 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +25 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +101 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +16 -2
- package/dist/server/google-oauth.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +35 -0
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/styles/agent-conversation.css +21 -0
- package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/docs/design/durable-agent-runs.md +217 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/corpus/templates/analytics/app/pages/About.tsx +0 -108
- package/corpus/templates/analytics/app/routes/about.tsx +0 -9
- /package/corpus/templates/analytics/app/routes/{dashboards.tsx → dashboards._index.tsx} +0 -0
|
@@ -73,6 +73,101 @@ import { emit, listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|
|
73
73
|
|
|
74
74
|
const CONNECT_TIMEOUT_MS = 3000;
|
|
75
75
|
|
|
76
|
+
// ---- bitrate tuning for the loopback preview ----------------------------
|
|
77
|
+
//
|
|
78
|
+
// This is a SAME-MACHINE loopback (popover → bubble, both on 127.0.0.1),
|
|
79
|
+
// so there is effectively no bandwidth limit. Left untuned, WebKit's
|
|
80
|
+
// congestion controller still starts the encoder at libwebrtc's cautious
|
|
81
|
+
// ~300 kbps default and ramps up over several seconds, and with the
|
|
82
|
+
// default `balanced` degradation preference it downscales the picture
|
|
83
|
+
// while the bitrate is low. That produced the "bubble is blurry for the
|
|
84
|
+
// first ~10s, then sharpens" regression after the bubble stopped owning a
|
|
85
|
+
// direct local camera stream. We pin a high start/max bitrate and forbid
|
|
86
|
+
// resolution downscaling so the bubble is crisp from the first frame.
|
|
87
|
+
//
|
|
88
|
+
// 720p webcam is comfortable at a few Mbps; these are generous, not silly.
|
|
89
|
+
const BUBBLE_START_BITRATE_KBPS = 2500;
|
|
90
|
+
const BUBBLE_MIN_BITRATE_KBPS = 1200;
|
|
91
|
+
const BUBBLE_MAX_BITRATE_KBPS = 5000;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Pin the sender to a high, fixed bitrate and forbid resolution
|
|
95
|
+
* downscaling. Called after `setLocalDescription` so `encodings` is
|
|
96
|
+
* populated on every WebKit build. Best-effort — older WebViews may not
|
|
97
|
+
* support every field, so we swallow failures.
|
|
98
|
+
*/
|
|
99
|
+
async function configureBubbleSender(
|
|
100
|
+
sender: RTCRtpSender | null,
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
if (!sender) return;
|
|
103
|
+
try {
|
|
104
|
+
const params = sender.getParameters();
|
|
105
|
+
if (!params.encodings || params.encodings.length === 0) {
|
|
106
|
+
// Some WebKit builds report empty encodings until the first
|
|
107
|
+
// negotiation — seed one so our settings have somewhere to land.
|
|
108
|
+
params.encodings = [{}];
|
|
109
|
+
}
|
|
110
|
+
params.encodings[0].maxBitrate = BUBBLE_MAX_BITRATE_KBPS * 1000;
|
|
111
|
+
params.encodings[0].maxFramerate = 30;
|
|
112
|
+
// `minBitrate` is non-standard but honored by libwebrtc-based stacks
|
|
113
|
+
// (WebKit, Chromium); harmless where unsupported.
|
|
114
|
+
(params.encodings[0] as { minBitrate?: number }).minBitrate =
|
|
115
|
+
BUBBLE_MIN_BITRATE_KBPS * 1000;
|
|
116
|
+
// Never trade resolution for framerate — a soft bubble looks broken;
|
|
117
|
+
// a marginally lower fps does not.
|
|
118
|
+
params.degradationPreference = "maintain-resolution";
|
|
119
|
+
await sender.setParameters(params);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.warn("[clips-bubble-webrtc] setParameters failed", err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Raise the encoder's START bitrate via SDP so it doesn't begin at
|
|
127
|
+
* libwebrtc's cautious default and slowly climb (the visible quality
|
|
128
|
+
* ramp). WebKit reads these codec params from our own local description
|
|
129
|
+
* and applies them to our encoder. Also stamps a high `b=AS`/`b=TIAS`
|
|
130
|
+
* ceiling on the video m-section.
|
|
131
|
+
*/
|
|
132
|
+
function boostBubbleVideoBitrate(sdp: string | undefined): string {
|
|
133
|
+
if (!sdp) return sdp ?? "";
|
|
134
|
+
const eol = sdp.includes("\r\n") ? "\r\n" : "\n";
|
|
135
|
+
const lines = sdp.split(/\r\n|\n/);
|
|
136
|
+
const out: string[] = [];
|
|
137
|
+
let inVideo = false;
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
if (line.startsWith("m=")) {
|
|
140
|
+
inVideo = line.startsWith("m=video");
|
|
141
|
+
out.push(line);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (inVideo && line.startsWith("c=")) {
|
|
145
|
+
out.push(line);
|
|
146
|
+
// b=AS is kbps; b=TIAS is bps. Both must follow the c= line.
|
|
147
|
+
out.push(`b=AS:${BUBBLE_MAX_BITRATE_KBPS}`);
|
|
148
|
+
out.push(`b=TIAS:${BUBBLE_MAX_BITRATE_KBPS * 1000}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
inVideo &&
|
|
153
|
+
line.startsWith("a=fmtp:") &&
|
|
154
|
+
// Skip RTX/FEC payloads (their fmtp carries `apt=`); only primary
|
|
155
|
+
// codecs honor the x-google-* hints.
|
|
156
|
+
!line.includes("apt=") &&
|
|
157
|
+
!line.includes("x-google-start-bitrate")
|
|
158
|
+
) {
|
|
159
|
+
out.push(
|
|
160
|
+
`${line};x-google-start-bitrate=${BUBBLE_START_BITRATE_KBPS}` +
|
|
161
|
+
`;x-google-min-bitrate=${BUBBLE_MIN_BITRATE_KBPS}` +
|
|
162
|
+
`;x-google-max-bitrate=${BUBBLE_MAX_BITRATE_KBPS}`,
|
|
163
|
+
);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
out.push(line);
|
|
167
|
+
}
|
|
168
|
+
return out.join(eol);
|
|
169
|
+
}
|
|
170
|
+
|
|
76
171
|
export interface BubbleWebrtcHandle {
|
|
77
172
|
/** Tear down the peer connection and unsubscribe listeners. */
|
|
78
173
|
stop(): void;
|
|
@@ -243,11 +338,23 @@ export function startBubbleWebrtc(
|
|
|
243
338
|
}
|
|
244
339
|
};
|
|
245
340
|
|
|
341
|
+
// Hint the encoder to PRESERVE RESOLUTION. A raw camera track defaults
|
|
342
|
+
// to "motion" semantics — under any bitrate pressure WebKit downscales
|
|
343
|
+
// the picture to protect the frame rate, which on this loopback shows
|
|
344
|
+
// up as a soft/blurry bubble. "detail" flips the default toward keeping
|
|
345
|
+
// pixels, complementing the explicit `maintain-resolution` set below.
|
|
346
|
+
try {
|
|
347
|
+
videoTrack.contentHint = "detail";
|
|
348
|
+
} catch {
|
|
349
|
+
// contentHint is a harmless no-op on older WebViews.
|
|
350
|
+
}
|
|
351
|
+
|
|
246
352
|
// Add the camera video track. WebRTC will renegotiate if the track
|
|
247
353
|
// id changes, but we only add once per handshake so this is a
|
|
248
|
-
// one-shot.
|
|
354
|
+
// one-shot. Capture the sender so we can pin its bitrate below.
|
|
355
|
+
let sender: RTCRtpSender | null = null;
|
|
249
356
|
try {
|
|
250
|
-
localPc.addTrack(videoTrack, stream);
|
|
357
|
+
sender = localPc.addTrack(videoTrack, stream);
|
|
251
358
|
} catch (err) {
|
|
252
359
|
console.warn("[clips-bubble-webrtc] addTrack failed:", err);
|
|
253
360
|
onFailure("add-track-failed");
|
|
@@ -258,6 +365,10 @@ export function startBubbleWebrtc(
|
|
|
258
365
|
let offer: RTCSessionDescriptionInit;
|
|
259
366
|
try {
|
|
260
367
|
offer = await localPc.createOffer();
|
|
368
|
+
// Raise the encoder's start/max bitrate before we commit the local
|
|
369
|
+
// description so the bubble is sharp immediately instead of ramping
|
|
370
|
+
// up over ~10s. See `boostBubbleVideoBitrate`.
|
|
371
|
+
offer = { ...offer, sdp: boostBubbleVideoBitrate(offer.sdp) };
|
|
261
372
|
await localPc.setLocalDescription(offer);
|
|
262
373
|
} catch (err) {
|
|
263
374
|
console.warn(
|
|
@@ -270,6 +381,12 @@ export function startBubbleWebrtc(
|
|
|
270
381
|
}
|
|
271
382
|
if (stopped || myId !== handshakeId) return;
|
|
272
383
|
|
|
384
|
+
// Now that the local description is set, `getParameters().encodings`
|
|
385
|
+
// is populated on every WebKit build — pin the bitrate floor/ceiling
|
|
386
|
+
// and forbid resolution downscaling.
|
|
387
|
+
await configureBubbleSender(sender);
|
|
388
|
+
if (stopped || myId !== handshakeId) return;
|
|
389
|
+
|
|
273
390
|
try {
|
|
274
391
|
await emit("clips:webrtc-offer", {
|
|
275
392
|
handshakeId: myId,
|
|
@@ -1473,6 +1473,22 @@ async function showRegionGuidesForRecording(wantsScreen: boolean) {
|
|
|
1473
1473
|
});
|
|
1474
1474
|
}
|
|
1475
1475
|
|
|
1476
|
+
// Frame the chosen screen region with a live border so the user can see exactly
|
|
1477
|
+
// what's being captured throughout the countdown and recording. The overlay is
|
|
1478
|
+
// capture-excluded and draws its stroke outside the captured pixels, so it never
|
|
1479
|
+
// lands in the video. Torn down with the rest of the recording chrome on stop.
|
|
1480
|
+
async function showRegionRecordBorder(region: RegionCaptureRect | null) {
|
|
1481
|
+
if (!region) return;
|
|
1482
|
+
await invoke("show_region_record_border", {
|
|
1483
|
+
x: region.x,
|
|
1484
|
+
y: region.y,
|
|
1485
|
+
width: region.width,
|
|
1486
|
+
height: region.height,
|
|
1487
|
+
}).catch((err) => {
|
|
1488
|
+
console.warn("[clips-recorder] show_region_record_border failed:", err);
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1476
1492
|
async function runRecordingCountdown(wantsScreen: boolean, audioCue: AudioCue) {
|
|
1477
1493
|
const countdownEvent = waitForCountdownEvent(
|
|
1478
1494
|
COUNTDOWN_EVENT_TIMEOUT_MS,
|
|
@@ -1566,6 +1582,10 @@ async function startNativeFullscreenRecording(
|
|
|
1566
1582
|
|
|
1567
1583
|
if (params.source === "region") {
|
|
1568
1584
|
captureRegion = await selectRegionForRecording();
|
|
1585
|
+
// Frame the selected area now so it stays visible through the countdown
|
|
1586
|
+
// and the whole recording. hide_recording_chrome / hide_overlays tear it
|
|
1587
|
+
// down on stop, cancel, and the error paths below.
|
|
1588
|
+
await showRegionRecordBorder(captureRegion);
|
|
1569
1589
|
}
|
|
1570
1590
|
|
|
1571
1591
|
if (localOnly && localRecordingMode === "separate" && wantsCamera) {
|
|
@@ -12,6 +12,7 @@ import { MeetingNub } from "./overlays/meeting-nub";
|
|
|
12
12
|
import { FlowBar } from "./overlays/flow-bar";
|
|
13
13
|
import { RecordingPill } from "./overlays/recording-pill";
|
|
14
14
|
import { RegionGuideEditor, RegionGuides } from "./overlays/region-guides";
|
|
15
|
+
import { RegionRecordBorder } from "./overlays/region-record-border";
|
|
15
16
|
import "./styles.css";
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -55,6 +56,8 @@ function pickRoute(route: string): React.ReactElement {
|
|
|
55
56
|
return <RegionGuideEditor />;
|
|
56
57
|
case "region-capture-selector":
|
|
57
58
|
return <RegionGuideEditor mode="capture" />;
|
|
59
|
+
case "region-record-border":
|
|
60
|
+
return <RegionRecordBorder />;
|
|
58
61
|
default:
|
|
59
62
|
return <App />;
|
|
60
63
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { emit } from "@tauri-apps/api/event";
|
|
3
3
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
4
|
+
import { IconPlayerSkipForwardFilled, IconX } from "@tabler/icons-react";
|
|
4
5
|
|
|
5
6
|
const COUNTDOWN_STEP_MS = 1000;
|
|
6
7
|
|
|
@@ -62,8 +63,26 @@ export function Countdown() {
|
|
|
62
63
|
|
|
63
64
|
return (
|
|
64
65
|
<div className="countdown-root">
|
|
65
|
-
<div className="countdown-
|
|
66
|
-
|
|
66
|
+
<div className="countdown-controls">
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
className="countdown-control countdown-control-cancel"
|
|
70
|
+
aria-label="Cancel recording"
|
|
71
|
+
onClick={() => closeWithEvent("clips:countdown-cancel")}
|
|
72
|
+
>
|
|
73
|
+
<IconX size={30} stroke={2.4} />
|
|
74
|
+
</button>
|
|
75
|
+
<div className="countdown-number" key={n} aria-live="polite">
|
|
76
|
+
{n > 0 ? n : ""}
|
|
77
|
+
</div>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
className="countdown-control countdown-control-skip"
|
|
81
|
+
aria-label="Skip countdown and start recording now"
|
|
82
|
+
onClick={() => closeWithEvent("clips:countdown-done")}
|
|
83
|
+
>
|
|
84
|
+
<IconPlayerSkipForwardFilled size={28} />
|
|
85
|
+
</button>
|
|
67
86
|
</div>
|
|
68
87
|
<div className="countdown-hint" aria-label="Countdown shortcuts">
|
|
69
88
|
<span>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
3
|
+
|
|
4
|
+
interface BorderRect {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function clamp01(value: number): number {
|
|
12
|
+
if (!Number.isFinite(value)) return 0;
|
|
13
|
+
return Math.min(1, Math.max(0, value));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The live recording region is handed to this overlay window as a normalized
|
|
18
|
+
* rect in the URL query (`?region=x,y,width,height`) so it never has to wait
|
|
19
|
+
* on a Tauri event (which could race the webview load). All four values are
|
|
20
|
+
* fractions [0,1] of the recording monitor — the same space the region-capture
|
|
21
|
+
* selector emitted them in.
|
|
22
|
+
*/
|
|
23
|
+
function parseRect(search: string): BorderRect | null {
|
|
24
|
+
const raw = new URLSearchParams(search).get("region");
|
|
25
|
+
if (!raw) return null;
|
|
26
|
+
const parts = raw.split(",").map((value) => Number.parseFloat(value));
|
|
27
|
+
if (parts.length !== 4 || parts.some((value) => !Number.isFinite(value))) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const [x, y, width, height] = parts;
|
|
31
|
+
if (width <= 0 || height <= 0) return null;
|
|
32
|
+
return {
|
|
33
|
+
x: clamp01(x),
|
|
34
|
+
y: clamp01(y),
|
|
35
|
+
width: clamp01(width),
|
|
36
|
+
height: clamp01(height),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A thin frame painted around the screen region currently being recorded. The
|
|
42
|
+
* window itself is full-screen, click-through, and capture-excluded (see
|
|
43
|
+
* `show_region_record_border` on the Rust side). The frame is drawn entirely
|
|
44
|
+
* OUTWARD via an outset box-shadow with no fill, so its pixels sit just outside
|
|
45
|
+
* the captured rect and never leak into the recording even on the macOS 15.4+
|
|
46
|
+
* builds where `NSWindowSharingNone` is occasionally bypassed.
|
|
47
|
+
*/
|
|
48
|
+
export function RegionRecordBorder() {
|
|
49
|
+
const [rect] = useState<BorderRect | null>(() =>
|
|
50
|
+
typeof window === "undefined" ? null : parseRect(window.location.search),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (rect) return;
|
|
55
|
+
// No usable region — close the overlay so an empty window never lingers.
|
|
56
|
+
getCurrentWindow()
|
|
57
|
+
.close()
|
|
58
|
+
.catch(() => {});
|
|
59
|
+
}, [rect]);
|
|
60
|
+
|
|
61
|
+
if (!rect) return null;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="region-record-border-layer" aria-hidden>
|
|
65
|
+
<div
|
|
66
|
+
className="region-record-border-frame"
|
|
67
|
+
style={{
|
|
68
|
+
left: `${rect.x * 100}%`,
|
|
69
|
+
top: `${rect.y * 100}%`,
|
|
70
|
+
width: `${rect.width * 100}%`,
|
|
71
|
+
height: `${rect.height * 100}%`,
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -2037,6 +2037,70 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
2037
2037
|
pointer-events: none;
|
|
2038
2038
|
}
|
|
2039
2039
|
|
|
2040
|
+
/*
|
|
2041
|
+
* Horizontal row of [cancel] [number] [skip]. The two circular buttons are
|
|
2042
|
+
* absolutely centered at exactly ±200px (logical) from the row center so the
|
|
2043
|
+
* native cursor-poll hit-rects in clips/mod.rs (CONTROL_OFFSET_X = 200.0,
|
|
2044
|
+
* CONTROL_DIAMETER = 64.0) line up with the rendered controls. Keep these
|
|
2045
|
+
* numbers in sync with that Rust code.
|
|
2046
|
+
*/
|
|
2047
|
+
.countdown-controls {
|
|
2048
|
+
position: relative;
|
|
2049
|
+
display: flex;
|
|
2050
|
+
align-items: center;
|
|
2051
|
+
justify-content: center;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
.countdown-control {
|
|
2055
|
+
position: absolute;
|
|
2056
|
+
top: 50%;
|
|
2057
|
+
display: inline-flex;
|
|
2058
|
+
align-items: center;
|
|
2059
|
+
justify-content: center;
|
|
2060
|
+
width: 64px;
|
|
2061
|
+
height: 64px;
|
|
2062
|
+
margin: 0;
|
|
2063
|
+
padding: 0;
|
|
2064
|
+
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
2065
|
+
border-radius: 50%;
|
|
2066
|
+
background: rgba(10, 12, 16, 0.42);
|
|
2067
|
+
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.28);
|
|
2068
|
+
color: #ffffff;
|
|
2069
|
+
cursor: pointer;
|
|
2070
|
+
pointer-events: auto;
|
|
2071
|
+
backdrop-filter: blur(8px);
|
|
2072
|
+
transform: translateY(-50%);
|
|
2073
|
+
transition:
|
|
2074
|
+
background-color 0.15s ease,
|
|
2075
|
+
border-color 0.15s ease,
|
|
2076
|
+
transform 0.1s ease;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
.countdown-control-cancel {
|
|
2080
|
+
/* center at -200px from row center: -200 - (64 / 2) = -232px */
|
|
2081
|
+
left: -232px;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
.countdown-control-skip {
|
|
2085
|
+
/* center at +200px from row center: 200 - (64 / 2) = 168px */
|
|
2086
|
+
right: -232px;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
.countdown-control:hover {
|
|
2090
|
+
background: rgba(255, 255, 255, 0.92);
|
|
2091
|
+
border-color: rgba(255, 255, 255, 0.95);
|
|
2092
|
+
color: #0b0d12;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
.countdown-control:active {
|
|
2096
|
+
transform: translateY(-50%) scale(0.94);
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
.countdown-control:focus-visible {
|
|
2100
|
+
outline: 2px solid rgba(255, 255, 255, 0.9);
|
|
2101
|
+
outline-offset: 2px;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2040
2104
|
.countdown-number {
|
|
2041
2105
|
font-size: 240px;
|
|
2042
2106
|
font-weight: 800;
|
|
@@ -3749,6 +3813,55 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
3749
3813
|
backdrop-filter: blur(14px);
|
|
3750
3814
|
}
|
|
3751
3815
|
|
|
3816
|
+
/* ------------------------------------------------------------------------- */
|
|
3817
|
+
/* Overlay: live recording-region border */
|
|
3818
|
+
/* ------------------------------------------------------------------------- */
|
|
3819
|
+
|
|
3820
|
+
.region-record-border-layer {
|
|
3821
|
+
position: fixed;
|
|
3822
|
+
inset: 0;
|
|
3823
|
+
width: 100vw;
|
|
3824
|
+
height: 100vh;
|
|
3825
|
+
overflow: hidden;
|
|
3826
|
+
pointer-events: none;
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
.region-record-border-frame {
|
|
3830
|
+
position: absolute;
|
|
3831
|
+
border-radius: 5px;
|
|
3832
|
+
/* No fill, and the stroke is painted OUTWARD with an outset box-shadow so it
|
|
3833
|
+
lands just outside the captured rect — never inside the recorded video. */
|
|
3834
|
+
box-shadow:
|
|
3835
|
+
0 0 0 2px rgba(239, 68, 68, 0.95),
|
|
3836
|
+
0 0 0 4px rgba(239, 68, 68, 0.32),
|
|
3837
|
+
0 0 26px 6px rgba(239, 68, 68, 0.26);
|
|
3838
|
+
animation: region-record-border-pulse 1.8s ease-in-out infinite;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
/* Gentle pulse that mirrors the live recording dot, signalling "recording
|
|
3842
|
+
now". Honour reduced-motion by holding the frame steady. */
|
|
3843
|
+
@keyframes region-record-border-pulse {
|
|
3844
|
+
0%,
|
|
3845
|
+
100% {
|
|
3846
|
+
box-shadow:
|
|
3847
|
+
0 0 0 2px rgba(239, 68, 68, 0.9),
|
|
3848
|
+
0 0 0 4px rgba(239, 68, 68, 0.28),
|
|
3849
|
+
0 0 22px 5px rgba(239, 68, 68, 0.2);
|
|
3850
|
+
}
|
|
3851
|
+
50% {
|
|
3852
|
+
box-shadow:
|
|
3853
|
+
0 0 0 2px rgba(239, 68, 68, 1),
|
|
3854
|
+
0 0 0 4px rgba(239, 68, 68, 0.44),
|
|
3855
|
+
0 0 30px 8px rgba(239, 68, 68, 0.34);
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3860
|
+
.region-record-border-frame {
|
|
3861
|
+
animation: none;
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3752
3865
|
@keyframes flow-bar-in {
|
|
3753
3866
|
from {
|
|
3754
3867
|
opacity: 0;
|
|
@@ -3799,11 +3912,15 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
3799
3912
|
}
|
|
3800
3913
|
|
|
3801
3914
|
/* Collapsed (not expanded). Shrinks to dot + waveform; controls revealed via
|
|
3802
|
-
CSS :hover which works at the WebKit level even when the app is unfocused.
|
|
3915
|
+
CSS :hover which works at the WebKit level even when the app is unfocused.
|
|
3916
|
+
Top-anchored (not centered) inside the headroom-sized window so the hover
|
|
3917
|
+
reveal of the drag grip grows the capsule strictly DOWNWARD — centering it
|
|
3918
|
+
would split the growth and raise the top edge, sliding buttons up under the
|
|
3919
|
+
cursor and causing misclicks. */
|
|
3803
3920
|
.pill-inner-compact {
|
|
3804
3921
|
width: fit-content;
|
|
3805
3922
|
height: fit-content;
|
|
3806
|
-
align-self:
|
|
3923
|
+
align-self: flex-start;
|
|
3807
3924
|
border-radius: 18px;
|
|
3808
3925
|
}
|
|
3809
3926
|
|
|
@@ -4,6 +4,7 @@ use std::path::PathBuf;
|
|
|
4
4
|
use std::process::Command;
|
|
5
5
|
#[cfg(target_os = "macos")]
|
|
6
6
|
use std::process::Stdio;
|
|
7
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
7
8
|
use std::thread;
|
|
8
9
|
use std::time::Duration;
|
|
9
10
|
use tauri::{
|
|
@@ -26,11 +27,23 @@ use crate::util::{
|
|
|
26
27
|
/// React bundle with a hash route that `main.tsx` uses to pick the component.
|
|
27
28
|
const COUNTDOWN_LABEL: &str = "countdown";
|
|
28
29
|
const TOOLBAR_LABEL: &str = "toolbar";
|
|
30
|
+
// Geometry of the two circular cancel/skip buttons that flank the countdown
|
|
31
|
+
// number. These MUST stay in sync with the CSS in
|
|
32
|
+
// `templates/clips/desktop/src/styles.css` (`.countdown-control` is 64px and
|
|
33
|
+
// its center sits ±200px logical from the window center). A few px of slop is
|
|
34
|
+
// added to each hit-rect so edge clicks register.
|
|
35
|
+
const COUNTDOWN_CONTROL_OFFSET_X: f64 = 200.0;
|
|
36
|
+
const COUNTDOWN_CONTROL_DIAMETER: f64 = 64.0;
|
|
37
|
+
const COUNTDOWN_CONTROL_HIT_PAD: f64 = 8.0;
|
|
38
|
+
// Guards the single cursor-poll loop that toggles click-through on the
|
|
39
|
+
// countdown overlay so only the button zones are interactive.
|
|
40
|
+
static COUNTDOWN_CONTROL_TRACKING: AtomicBool = AtomicBool::new(false);
|
|
29
41
|
const BUBBLE_LABEL: &str = "bubble";
|
|
30
42
|
const FINALIZING_LABEL: &str = "finalizing";
|
|
31
43
|
const FLOW_BAR_LABEL: &str = "flow-bar";
|
|
32
44
|
const REGION_GUIDES_LABEL: &str = "region-guides";
|
|
33
45
|
const REGION_GUIDE_EDITOR_LABEL: &str = "region-guide-editor";
|
|
46
|
+
const REGION_RECORD_BORDER_LABEL: &str = "region-record-border";
|
|
34
47
|
|
|
35
48
|
/// Physical-pixel bubble sizes. Logical px on retina = physical / 2, so these
|
|
36
49
|
/// map to ~96 (small) and ~180 (medium) logical px — matching Loom's camera
|
|
@@ -303,6 +316,7 @@ pub async fn show_countdown(app: AppHandle) -> Result<(), String> {
|
|
|
303
316
|
dlog!("[clips-tray] show_countdown invoked");
|
|
304
317
|
mark_popover_shown(&app);
|
|
305
318
|
if let Some(existing) = app.get_webview_window(COUNTDOWN_LABEL) {
|
|
319
|
+
stop_countdown_control_tracking();
|
|
306
320
|
let _ = app.emit("clips:countdown-shortcuts-active", false);
|
|
307
321
|
let _ = existing.close();
|
|
308
322
|
}
|
|
@@ -339,10 +353,68 @@ pub async fn show_countdown(app: AppHandle) -> Result<(), String> {
|
|
|
339
353
|
configure_overlay_behavior(&win);
|
|
340
354
|
let _ = win.show();
|
|
341
355
|
let _ = app.emit("clips:countdown-shortcuts-active", true);
|
|
356
|
+
start_countdown_control_tracking(&app);
|
|
342
357
|
dlog!("[clips-tray] countdown shown");
|
|
343
358
|
Ok(())
|
|
344
359
|
}
|
|
345
360
|
|
|
361
|
+
/// True when the global cursor sits inside either circular cancel/skip button
|
|
362
|
+
/// zone of the countdown overlay. The controls row is centered in the window;
|
|
363
|
+
/// each button's center is `COUNTDOWN_CONTROL_OFFSET_X` logical px to the
|
|
364
|
+
/// left/right of the window center, vertically at the window center. Cursor,
|
|
365
|
+
/// window position, and window size all come from Tauri in physical px with a
|
|
366
|
+
/// desktop top-left origin, so we convert the logical button geometry using the
|
|
367
|
+
/// window's scale factor and do a plain point-in-rect test.
|
|
368
|
+
fn cursor_over_countdown_control(window: &WebviewWindow) -> bool {
|
|
369
|
+
let (Ok(c), Ok(p), Ok(s), Ok(scale)) = (
|
|
370
|
+
window.cursor_position(),
|
|
371
|
+
window.outer_position(),
|
|
372
|
+
window.outer_size(),
|
|
373
|
+
window.scale_factor(),
|
|
374
|
+
) else {
|
|
375
|
+
return false;
|
|
376
|
+
};
|
|
377
|
+
let center_x = p.x as f64 + s.width as f64 / 2.0;
|
|
378
|
+
let center_y = p.y as f64 + s.height as f64 / 2.0;
|
|
379
|
+
let half = (COUNTDOWN_CONTROL_DIAMETER / 2.0 + COUNTDOWN_CONTROL_HIT_PAD) * scale;
|
|
380
|
+
let offset = COUNTDOWN_CONTROL_OFFSET_X * scale;
|
|
381
|
+
let in_button = |bx: f64| -> bool {
|
|
382
|
+
c.x >= bx - half && c.x <= bx + half && c.y >= center_y - half && c.y <= center_y + half
|
|
383
|
+
};
|
|
384
|
+
in_button(center_x - offset) || in_button(center_x + offset)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// Poll the cursor against the two button zones while the countdown overlay is
|
|
388
|
+
/// alive, toggling `set_ignore_cursor_events` so the buttons are clickable only
|
|
389
|
+
/// when the cursor is over them and the rest of the screen stays click-through.
|
|
390
|
+
/// Idempotent; mirrors `start_pill_hover_tracking` in `recording_indicator.rs`.
|
|
391
|
+
fn start_countdown_control_tracking(app: &AppHandle) {
|
|
392
|
+
if COUNTDOWN_CONTROL_TRACKING.swap(true, Ordering::SeqCst) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
let app = app.clone();
|
|
396
|
+
tauri::async_runtime::spawn(async move {
|
|
397
|
+
let mut prev_interactive = false;
|
|
398
|
+
while COUNTDOWN_CONTROL_TRACKING.load(Ordering::Relaxed) {
|
|
399
|
+
let Some(win) = app.get_webview_window(COUNTDOWN_LABEL) else {
|
|
400
|
+
break;
|
|
401
|
+
};
|
|
402
|
+
let over = cursor_over_countdown_control(&win);
|
|
403
|
+
if over != prev_interactive {
|
|
404
|
+
prev_interactive = over;
|
|
405
|
+
// ignore_cursor_events(false) => clicks land on the buttons.
|
|
406
|
+
let _ = win.set_ignore_cursor_events(!over);
|
|
407
|
+
}
|
|
408
|
+
tokio::time::sleep(Duration::from_millis(70)).await;
|
|
409
|
+
}
|
|
410
|
+
COUNTDOWN_CONTROL_TRACKING.store(false, Ordering::SeqCst);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
fn stop_countdown_control_tracking() {
|
|
415
|
+
COUNTDOWN_CONTROL_TRACKING.store(false, Ordering::SeqCst);
|
|
416
|
+
}
|
|
417
|
+
|
|
346
418
|
/// Full-screen transparent overlay that shows compact bottom-left progress
|
|
347
419
|
/// while the recorder flushes its final chunks and awaits the server finalize.
|
|
348
420
|
/// Rendered immediately after the user clicks Stop so they don't stare at a
|
|
@@ -448,6 +520,65 @@ pub async fn hide_region_guides(app: AppHandle) -> Result<(), String> {
|
|
|
448
520
|
Ok(())
|
|
449
521
|
}
|
|
450
522
|
|
|
523
|
+
/// Live border framing the region currently being recorded. Like the region
|
|
524
|
+
/// guides this is a full-screen, click-through, capture-excluded overlay — but
|
|
525
|
+
/// the rect is ephemeral (it belongs to one recording, not the saved preset),
|
|
526
|
+
/// so it's handed in via the URL query rather than read from config. The React
|
|
527
|
+
/// view paints only an OUTWARD frame so the stroke stays out of the captured
|
|
528
|
+
/// pixels. The recording flow owns this window; it's torn down by
|
|
529
|
+
/// `hide_recording_chrome` / `hide_overlays` when capture stops.
|
|
530
|
+
#[tauri::command]
|
|
531
|
+
pub async fn show_region_record_border(
|
|
532
|
+
app: AppHandle,
|
|
533
|
+
x: f64,
|
|
534
|
+
y: f64,
|
|
535
|
+
width: f64,
|
|
536
|
+
height: f64,
|
|
537
|
+
) -> Result<(), String> {
|
|
538
|
+
if !(width > 0.0 && height > 0.0) {
|
|
539
|
+
return Ok(());
|
|
540
|
+
}
|
|
541
|
+
if let Some(existing) = app.get_webview_window(REGION_RECORD_BORDER_LABEL) {
|
|
542
|
+
let _ = existing.close();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
let (mx, my, mw, mh) = tray_monitor_physical_rect(&app);
|
|
546
|
+
let url = WebviewUrl::App(
|
|
547
|
+
format!("index.html?region={x:.6},{y:.6},{width:.6},{height:.6}#region-record-border")
|
|
548
|
+
.into(),
|
|
549
|
+
);
|
|
550
|
+
let win = WebviewWindowBuilder::new(&app, REGION_RECORD_BORDER_LABEL, url)
|
|
551
|
+
.title("Clips Recording Region")
|
|
552
|
+
.decorations(false)
|
|
553
|
+
.transparent(true)
|
|
554
|
+
.always_on_top(true)
|
|
555
|
+
.skip_taskbar(true)
|
|
556
|
+
.resizable(false)
|
|
557
|
+
.shadow(false)
|
|
558
|
+
.visible(false)
|
|
559
|
+
.focused(false)
|
|
560
|
+
.build()
|
|
561
|
+
.map_err(|e| {
|
|
562
|
+
eprintln!("[clips-tray] region record border build failed: {}", e);
|
|
563
|
+
e.to_string()
|
|
564
|
+
})?;
|
|
565
|
+
let _ = win.set_size(tauri::Size::Physical(PhysicalSize::new(mw, mh)));
|
|
566
|
+
let _ = win.set_position(PhysicalPosition::new(mx, my));
|
|
567
|
+
let _ = win.set_ignore_cursor_events(true);
|
|
568
|
+
set_capture_excluded_always(&win);
|
|
569
|
+
configure_overlay_behavior(&win);
|
|
570
|
+
crate::util::show_without_activation(&win);
|
|
571
|
+
Ok(())
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#[tauri::command]
|
|
575
|
+
pub async fn hide_region_record_border(app: AppHandle) -> Result<(), String> {
|
|
576
|
+
if let Some(w) = app.get_webview_window(REGION_RECORD_BORDER_LABEL) {
|
|
577
|
+
let _ = w.close();
|
|
578
|
+
}
|
|
579
|
+
Ok(())
|
|
580
|
+
}
|
|
581
|
+
|
|
451
582
|
/// Reconcile the always-on region-guides overlay with the current config.
|
|
452
583
|
/// Called from config saves and on startup so the guides window matches the
|
|
453
584
|
/// `always_visible` toggle without needing a recording-flow round trip. When a
|
|
@@ -740,6 +871,7 @@ pub async fn set_bubble_capture_excluded(app: AppHandle, excluded: bool) -> Resu
|
|
|
740
871
|
|
|
741
872
|
#[tauri::command]
|
|
742
873
|
pub async fn hide_overlays(app: AppHandle) -> Result<(), String> {
|
|
874
|
+
stop_countdown_control_tracking();
|
|
743
875
|
let _ = app.emit("clips:countdown-shortcuts-active", false);
|
|
744
876
|
for label in [
|
|
745
877
|
COUNTDOWN_LABEL,
|
|
@@ -748,6 +880,7 @@ pub async fn hide_overlays(app: AppHandle) -> Result<(), String> {
|
|
|
748
880
|
FINALIZING_LABEL,
|
|
749
881
|
FLOW_BAR_LABEL,
|
|
750
882
|
REGION_GUIDES_LABEL,
|
|
883
|
+
REGION_RECORD_BORDER_LABEL,
|
|
751
884
|
] {
|
|
752
885
|
if let Some(w) = app.get_webview_window(label) {
|
|
753
886
|
let _ = w.close();
|
|
@@ -766,13 +899,16 @@ pub async fn hide_overlays(app: AppHandle) -> Result<(), String> {
|
|
|
766
899
|
/// popover-close).
|
|
767
900
|
#[tauri::command]
|
|
768
901
|
pub async fn hide_recording_chrome(app: AppHandle) -> Result<(), String> {
|
|
902
|
+
stop_countdown_control_tracking();
|
|
769
903
|
let _ = app.emit("clips:countdown-shortcuts-active", false);
|
|
770
904
|
// The countdown + toolbar always tear down on recording stop. The region
|
|
771
905
|
// guides only tear down when they aren't pinned on-screen via the always-on
|
|
772
906
|
// toggle — otherwise we'd flicker close→reopen right after stop.
|
|
773
907
|
let g = crate::config::feature_config(&app).region_guides;
|
|
774
908
|
let keep_region_guides = g.always_visible && g.enabled && !g.rects.is_empty();
|
|
775
|
-
|
|
909
|
+
// The recording-region border belongs to a single recording (never pinned),
|
|
910
|
+
// so it always tears down here alongside the countdown + toolbar.
|
|
911
|
+
let mut labels: Vec<&str> = vec![COUNTDOWN_LABEL, TOOLBAR_LABEL, REGION_RECORD_BORDER_LABEL];
|
|
776
912
|
if !keep_region_guides {
|
|
777
913
|
labels.push(REGION_GUIDES_LABEL);
|
|
778
914
|
}
|
|
@@ -1438,6 +1574,9 @@ pub async fn reset_state(app: AppHandle) -> Result<(), String> {
|
|
|
1438
1574
|
if let Some(w) = app.get_webview_window(REGION_GUIDES_LABEL) {
|
|
1439
1575
|
let _ = w.close();
|
|
1440
1576
|
}
|
|
1577
|
+
if let Some(w) = app.get_webview_window(REGION_RECORD_BORDER_LABEL) {
|
|
1578
|
+
let _ = w.close();
|
|
1579
|
+
}
|
|
1441
1580
|
if let Some(window) = app.get_webview_window("popover") {
|
|
1442
1581
|
// Restore normal size in case the window was shrunk to a pinhole
|
|
1443
1582
|
// during recording — otherwise it would reappear as a 2×2 dot.
|