@agent-native/core-corpus 0.210.2 → 0.211.0
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 +1 -1
- package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
- package/corpus/templates/analytics/CHANGELOG.md +18 -0
- package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
- package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
- package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
- package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
- package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
- package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
- package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
- package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
- package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
- package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
- package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
- package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
- package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
- package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
- package/corpus/templates/analytics/server/db/schema.ts +1 -0
- package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
- package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
- package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
- package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
- package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
- package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
- package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
- package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
- package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
- package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
- package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
- package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
- package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
- package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
- package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
- package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
- package/corpus/templates/analytics/server/plugins/db.ts +9 -1
- package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
- package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
- package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/calendar/actions/delete-event.ts +1 -3
- package/corpus/templates/calendar/actions/delete-events.ts +1 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
- package/corpus/templates/calendar/actions/update-event.ts +2 -6
- package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/clips/CHANGELOG.md +11 -24
- package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
- package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
- package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
- package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
- package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
- package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
- package/corpus/templates/clips/app/root.tsx +22 -10
- package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
- package/corpus/templates/clips/app/routes/import.tsx +115 -110
- package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +29 -186
- package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
- package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
- package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
- package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
- package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
- package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/content/CHANGELOG.md +12 -0
- package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
- package/corpus/templates/content/actions/_database-navigation.ts +3 -3
- package/corpus/templates/content/actions/_database-utils.ts +10 -22
- package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
- package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
- package/corpus/templates/content/actions/export-document.ts +2 -14
- package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
- package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
- package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
- package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
- package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
- package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
- package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
- package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
- package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
- package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
- package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
- package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
- package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
- package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
- package/corpus/templates/content/app/global.css +23 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
- package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-edits-made-from-a-block-s-menu-or-a-table-s-row-and-column-m.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
- package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
- package/corpus/templates/content/e2e/helpers.ts +10 -0
- package/corpus/templates/content/shared/api.ts +18 -12
- package/corpus/templates/content/shared/database-query.ts +107 -13
- package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
- package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/design/CHANGELOG.md +2 -0
- package/corpus/templates/design/actions/delete-file.ts +1 -4
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
- package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
- package/corpus/templates/design/changelog/2026-10-19-reply-and-edit-on-a-comment-in-the-sidebar-or-on-a-canvas-pi.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +185 -85
- package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
- package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
- package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
- package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
- package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
- package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
- package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
- package/corpus/templates/mail/AGENTS.md +1 -1
- package/corpus/templates/mail/CHANGELOG.md +14 -0
- package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
- package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
- package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
- package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
- package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
- package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
- package/corpus/templates/mail/app/root.tsx +10 -2
- package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
- package/corpus/templates/mail/server/handlers/emails.ts +9 -0
- package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/plan/CHANGELOG.md +4 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
- package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
- package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
- package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
- package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
- package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
- package/package.json +1 -1
|
@@ -95,9 +95,9 @@ MCP clients see no `outputSchema` by default. Set `mcpOutputSchema: true` (requi
|
|
|
95
95
|
|
|
96
96
|
`authorize` decides whether the caller may run the action at all. It wraps
|
|
97
97
|
`run`, so it holds at **all six dispatch sites** — agent tool, HTTP, frontend
|
|
98
|
-
hook, MCP, A2A, CLI
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
hook, MCP, A2A, CLI. Use `authorize` for "not everyone may do this"; use
|
|
99
|
+
`needsApproval` to ask a human to bless one call a permitted caller is already
|
|
100
|
+
allowed to make.
|
|
101
101
|
|
|
102
102
|
```ts
|
|
103
103
|
import { coachAccess } from "../lib/access.js"; // defineAppRoles(...)
|
|
@@ -140,4 +140,6 @@ export default defineAction({
|
|
|
140
140
|
|
|
141
141
|
When the gate is truthy and the call isn't yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. A predicate gates conditionally (e.g. only external recipients) and **fails closed**: a throw is treated as "approval required". It receives its own copy of the prepared snapshot of the validated arguments, defaults applied, and the approval binds to the snapshot's hash, so an approved call runs only with the arguments that were approved (a Date binds by its time). The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's `approvalKey`, and only then does the action run.
|
|
142
142
|
|
|
143
|
+
Approval is given in the app, so HTTP never grants it. The HTTP action route and the generated Workers entry run a gated call only for the proven app UI (a frontend call from the signed-in user, same origin, holding the short-lived UI capability; `isProvenUiRequest`, the proof `uiOnly` uses) or the app's MCP directory widget. Every other HTTP caller (bearer or API key, MCP OAuth token, org service token, A2A, extension iframe, mobile app) gets `409 approval_required`, and a service identity can never approve. The shared client presents the capability with every write from a signed-in session (minted once per user, reused for nine minutes, never for reads or public pages), so a gated button works on its first request. A call without proof gets `403 ui_capability_required`; the client then re-mints and retries once, which is only the fallback for an expired or rejected capability. Every surface evaluates the gate through `preparedCallNeedsApproval`, so a throwing predicate asks there too. A caller that must run unattended needs a different action, never a bypass of the gate.
|
|
144
|
+
|
|
143
145
|
**Keep approvals rare** — the default is off and almost every action should leave it off. The canonical example is Mail's `send-email` (`needsApproval: true`). See the `security` skill and the Human Approval doc.
|
|
@@ -95,9 +95,9 @@ MCP clients see no `outputSchema` by default. Set `mcpOutputSchema: true` (requi
|
|
|
95
95
|
|
|
96
96
|
`authorize` decides whether the caller may run the action at all. It wraps
|
|
97
97
|
`run`, so it holds at **all six dispatch sites** — agent tool, HTTP, frontend
|
|
98
|
-
hook, MCP, A2A, CLI
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
hook, MCP, A2A, CLI. Use `authorize` for "not everyone may do this"; use
|
|
99
|
+
`needsApproval` to ask a human to bless one call a permitted caller is already
|
|
100
|
+
allowed to make.
|
|
101
101
|
|
|
102
102
|
```ts
|
|
103
103
|
import { coachAccess } from "../lib/access.js"; // defineAppRoles(...)
|
|
@@ -140,4 +140,6 @@ export default defineAction({
|
|
|
140
140
|
|
|
141
141
|
When the gate is truthy and the call isn't yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. A predicate gates conditionally (e.g. only external recipients) and **fails closed**: a throw is treated as "approval required". It receives its own copy of the prepared snapshot of the validated arguments, defaults applied, and the approval binds to the snapshot's hash, so an approved call runs only with the arguments that were approved (a Date binds by its time). The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's `approvalKey`, and only then does the action run.
|
|
142
142
|
|
|
143
|
+
Approval is given in the app, so HTTP never grants it. The HTTP action route and the generated Workers entry run a gated call only for the proven app UI (a frontend call from the signed-in user, same origin, holding the short-lived UI capability; `isProvenUiRequest`, the proof `uiOnly` uses) or the app's MCP directory widget. Every other HTTP caller (bearer or API key, MCP OAuth token, org service token, A2A, extension iframe, mobile app) gets `409 approval_required`, and a service identity can never approve. The shared client presents the capability with every write from a signed-in session (minted once per user, reused for nine minutes, never for reads or public pages), so a gated button works on its first request. A call without proof gets `403 ui_capability_required`; the client then re-mints and retries once, which is only the fallback for an expired or rejected capability. Every surface evaluates the gate through `preparedCallNeedsApproval`, so a throwing predicate asks there too. A caller that must run unattended needs a different action, never a bypass of the gate.
|
|
144
|
+
|
|
143
145
|
**Keep approvals rare** — the default is off and almost every action should leave it off. The canonical example is Mail's `send-email` (`needsApproval: true`). See the `security` skill and the Human Approval doc.
|
|
@@ -3,10 +3,20 @@
|
|
|
3
3
|
All notable user-facing changes to Clips are documented here. Open it any time
|
|
4
4
|
from the command menu (Cmd+K → "What's new") or from Settings.
|
|
5
5
|
|
|
6
|
+
## 2026-10-20
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Locally saved desktop recordings now appear in the recovery list with upload and storage setup actions.
|
|
11
|
+
|
|
6
12
|
## 2026-10-19
|
|
7
13
|
|
|
8
14
|
### Fixed
|
|
9
15
|
|
|
16
|
+
- Agents see a clip's frames as soon as it finishes processing instead of a cached "still processing" answer.
|
|
17
|
+
- Chapter list times now match the player's clock, and each chapter shows a focus ring when you tab to it.
|
|
18
|
+
- Editing a comment puts the cursor after its text, and Reply on another notification moves the cursor to its reply box.
|
|
19
|
+
- Video imports use clearer labels, YouTube links work without connected storage, and embedded players stay clear of timeline hover previews.
|
|
10
20
|
- Screen Memory can transcribe desktop recordings without a separate FFmpeg install.
|
|
11
21
|
|
|
12
22
|
### Changed
|
|
@@ -57,6 +67,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
57
67
|
|
|
58
68
|
### Fixed
|
|
59
69
|
|
|
70
|
+
- Recordings uploaded by a connected agent can now use free Clips storage, like recordings made in the app.
|
|
60
71
|
- An agent upload too large for free hosted storage is now refused before it starts, with the size limit that applies.
|
|
61
72
|
- Agents connected to Clips over MCP can now import a transcript file, instead of getting an unknown-tool error.
|
|
62
73
|
- Clips keeps meeting transcripts recoverable while a save or notes finalization is still in progress.
|
|
@@ -1616,28 +1627,4 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
1616
1627
|
- Fixed the Mac menu bar popover so it opens reliably after launch at login.
|
|
1617
1628
|
- Localized Clips Settings changelog and extension sidebar labels in every supported interface language.
|
|
1618
1629
|
|
|
1619
|
-
## 2026-06-24
|
|
1620
|
-
|
|
1621
|
-
### Added
|
|
1622
|
-
|
|
1623
|
-
- Added a language picker and localized app chrome for supported languages.
|
|
1624
|
-
- Clips now includes a language picker with Simplified Chinese support for core navigation, empty states, and settings.
|
|
1625
|
-
|
|
1626
|
-
### Improved
|
|
1627
|
-
|
|
1628
|
-
- Chrome extension setup now shows a clear all-done confirmation when permissions are ready.
|
|
1629
|
-
- Settings now link directly to Agent settings for model, API key, automation, and voice preferences.
|
|
1630
|
-
- Settings now puts What's new in a compact sidebar, prioritizes Builder.io setup, and lets you manage S3 storage and AI provider keys in place.
|
|
1631
|
-
- Shared clips now expose their full console log stream (all levels, redacted) to connected agents, not just warnings and errors
|
|
1632
|
-
- Shared clips now expose their full fetch/XHR network request stream (method, sanitized URL, status, duration) to connected agents, not just failed requests
|
|
1633
|
-
|
|
1634
|
-
### Fixed
|
|
1635
|
-
|
|
1636
|
-
- Archive and spaces navigation is more resilient when sidebar count loading fails.
|
|
1637
|
-
- Inline Slack playback controls no longer shift when adjusting volume, and compact clip times stay on one line.
|
|
1638
|
-
|
|
1639
|
-
### Changed
|
|
1640
|
-
|
|
1641
|
-
- The Chrome extension option now only appears on supported Clips hosts unless a custom deployment explicitly enables it.
|
|
1642
|
-
|
|
1643
1630
|
For the full list of updates, see the [changelog folder](./changelog/).
|
|
@@ -82,7 +82,11 @@ export default defineAction({
|
|
|
82
82
|
// The ticket comes first: if it is refused or fails to save, no recording
|
|
83
83
|
// is left stranded in "uploading" with nothing able to finish it.
|
|
84
84
|
const recordingId = nanoid();
|
|
85
|
-
const ticket = await createRecordingUploadTicket(
|
|
85
|
+
const ticket = await createRecordingUploadTicket(
|
|
86
|
+
ownerEmail,
|
|
87
|
+
recordingId,
|
|
88
|
+
getRequestContext()?.authUserId,
|
|
89
|
+
);
|
|
86
90
|
let created: Awaited<ReturnType<typeof createRecording.run>>;
|
|
87
91
|
try {
|
|
88
92
|
created = await createRecording.run(
|
|
@@ -45,7 +45,6 @@ import {
|
|
|
45
45
|
editedMarkerMs,
|
|
46
46
|
editedToOriginal,
|
|
47
47
|
effectiveDuration,
|
|
48
|
-
formatMs,
|
|
49
48
|
cutRangesOf,
|
|
50
49
|
isExcluded,
|
|
51
50
|
parseEdits,
|
|
@@ -54,6 +53,8 @@ import {
|
|
|
54
53
|
} from "@/lib/timestamp-mapping";
|
|
55
54
|
import { cn } from "@/lib/utils";
|
|
56
55
|
|
|
56
|
+
import { msToClock } from "./scrubber";
|
|
57
|
+
|
|
57
58
|
/**
|
|
58
59
|
* Parse "m:ss" or "h:mm:ss" into milliseconds. A bare number is refused, so
|
|
59
60
|
* a numbered list ("1 Introduction") or a title starting with a number
|
|
@@ -300,11 +301,11 @@ function ChapterRows({
|
|
|
300
301
|
<button
|
|
301
302
|
type="button"
|
|
302
303
|
onClick={() => onSeek(chapter.startMs)}
|
|
303
|
-
className="group text-start"
|
|
304
|
+
className="group rounded-sm text-start focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
304
305
|
>
|
|
305
|
-
<span className="me-
|
|
306
|
-
{
|
|
307
|
-
</span>
|
|
306
|
+
<span className="me-1 font-mono text-primary group-hover:underline">
|
|
307
|
+
{msToClock(editedMs)}
|
|
308
|
+
</span>{" "}
|
|
308
309
|
<span className="break-words text-foreground">{chapter.title}</span>
|
|
309
310
|
</button>
|
|
310
311
|
</li>
|
|
@@ -432,7 +433,7 @@ function EditableChapterList({
|
|
|
432
433
|
const fresh = visible
|
|
433
434
|
.map(
|
|
434
435
|
({ chapter, editedMs }) =>
|
|
435
|
-
`${
|
|
436
|
+
`${msToClock(editedMs)} ${oneLine(chapter.title)}`,
|
|
436
437
|
)
|
|
437
438
|
.join("\n");
|
|
438
439
|
setDraft(failedSave?.draft ?? fresh);
|
|
@@ -112,7 +112,11 @@ export const CommentComposer = forwardRef<
|
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
useEffect(() => {
|
|
115
|
-
|
|
115
|
+
const textarea = innerRef.current;
|
|
116
|
+
if (!autoFocus || !textarea) return;
|
|
117
|
+
textarea.focus();
|
|
118
|
+
// Editing opens with the comment's text; keep typing after it.
|
|
119
|
+
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
116
120
|
}, [autoFocus]);
|
|
117
121
|
|
|
118
122
|
useAutoResizeCommentTextarea(innerRef, value, rows);
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./scrubber-position";
|
|
18
18
|
|
|
19
19
|
export interface ScrubberProps {
|
|
20
|
+
hoverPreviews?: boolean;
|
|
20
21
|
currentMs: number;
|
|
21
22
|
durationMs: number;
|
|
22
23
|
onSeek: (ms: number) => void;
|
|
@@ -33,6 +34,7 @@ const MARKER_GAP_PX = 2;
|
|
|
33
34
|
|
|
34
35
|
export function Scrubber(props: ScrubberProps) {
|
|
35
36
|
const {
|
|
37
|
+
hoverPreviews = true,
|
|
36
38
|
currentMs,
|
|
37
39
|
durationMs,
|
|
38
40
|
onSeek,
|
|
@@ -235,7 +237,7 @@ export function Scrubber(props: ScrubberProps) {
|
|
|
235
237
|
}}
|
|
236
238
|
>
|
|
237
239
|
{/* Hover bubble */}
|
|
238
|
-
{hoverMs !== null && !tooltip ? (
|
|
240
|
+
{hoverPreviews && hoverMs !== null && !tooltip ? (
|
|
239
241
|
<div
|
|
240
242
|
className="absolute -top-8 -translate-x-1/2 rounded bg-black/90 px-2 py-1 text-[11px] text-white pointer-events-none"
|
|
241
243
|
style={{ left: hoverX }}
|
|
@@ -245,7 +247,7 @@ export function Scrubber(props: ScrubberProps) {
|
|
|
245
247
|
) : null}
|
|
246
248
|
|
|
247
249
|
{/* Tooltip (comment / chapter) */}
|
|
248
|
-
{tooltip ? (
|
|
250
|
+
{hoverPreviews && tooltip ? (
|
|
249
251
|
<div
|
|
250
252
|
data-player-comment-hover
|
|
251
253
|
className={cn(
|
|
@@ -1810,7 +1810,7 @@ const messages = {
|
|
|
1810
1810
|
unmuteMicrophone: "Unmute",
|
|
1811
1811
|
uploadVideo: "Upload video",
|
|
1812
1812
|
takeScreenshot: "Take screenshot",
|
|
1813
|
-
importLoom: "Import
|
|
1813
|
+
importLoom: "Import video",
|
|
1814
1814
|
importing: "Importing...",
|
|
1815
1815
|
import: "Import",
|
|
1816
1816
|
recordNew: "Record new",
|
|
@@ -2057,8 +2057,8 @@ const messages = {
|
|
|
2057
2057
|
"This recording is still processing, so Clips kept your copy here. Wait for it, or upload it again.",
|
|
2058
2058
|
},
|
|
2059
2059
|
importRoute: {
|
|
2060
|
-
pageTitle: "Import
|
|
2061
|
-
title: "Import
|
|
2060
|
+
pageTitle: "Import video — Clips",
|
|
2061
|
+
title: "Import video",
|
|
2062
2062
|
urlPlaceholder: "https://www.loom.com/share/...",
|
|
2063
2063
|
cta: "Import",
|
|
2064
2064
|
helperText:
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export type ClipsExtensionSession = { email: string; token: string };
|
|
2
|
+
|
|
3
|
+
const SESSION_RETRY_DELAYS_MS = [250, 750] as const;
|
|
4
|
+
|
|
1
5
|
export function buildClipsExtensionBaseUrl(
|
|
2
6
|
origin: string,
|
|
3
7
|
appRootPath: string,
|
|
@@ -7,3 +11,69 @@ export function buildClipsExtensionBaseUrl(
|
|
|
7
11
|
url.hash = "";
|
|
8
12
|
return url.toString().replace(/\/+$/, "");
|
|
9
13
|
}
|
|
14
|
+
|
|
15
|
+
export async function fetchClipsExtensionSession(
|
|
16
|
+
fetchSession: () => Promise<Response>,
|
|
17
|
+
shouldContinue: () => boolean,
|
|
18
|
+
pause: (ms: number) => Promise<void> = (ms) =>
|
|
19
|
+
new Promise((resolve) => setTimeout(resolve, ms)),
|
|
20
|
+
): Promise<ClipsExtensionSession | null> {
|
|
21
|
+
let lastError: unknown;
|
|
22
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
23
|
+
if (!shouldContinue()) return null;
|
|
24
|
+
|
|
25
|
+
let response: Response | undefined;
|
|
26
|
+
try {
|
|
27
|
+
response = await fetchSession();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
lastError = error;
|
|
30
|
+
}
|
|
31
|
+
if (!shouldContinue()) return null;
|
|
32
|
+
|
|
33
|
+
if (response?.ok) {
|
|
34
|
+
const session: unknown = await response.json();
|
|
35
|
+
if (
|
|
36
|
+
session &&
|
|
37
|
+
typeof session === "object" &&
|
|
38
|
+
"error" in session &&
|
|
39
|
+
session.error === "Not authenticated"
|
|
40
|
+
) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (
|
|
44
|
+
!session ||
|
|
45
|
+
typeof session !== "object" ||
|
|
46
|
+
!("email" in session) ||
|
|
47
|
+
typeof session.email !== "string" ||
|
|
48
|
+
!session.email ||
|
|
49
|
+
!("token" in session) ||
|
|
50
|
+
typeof session.token !== "string" ||
|
|
51
|
+
!session.token
|
|
52
|
+
) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"The Clips session endpoint returned an invalid session.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return { email: session.email, token: session.token };
|
|
58
|
+
}
|
|
59
|
+
if (response && response.status < 500) {
|
|
60
|
+
if (response.status === 401 || response.status === 403) return null;
|
|
61
|
+
throw new Error(
|
|
62
|
+
`The Clips session endpoint failed with HTTP ${response.status}.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (response) {
|
|
66
|
+
lastError = new Error(
|
|
67
|
+
`The Clips session endpoint failed with HTTP ${response.status}.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!shouldContinue()) return null;
|
|
72
|
+
if (attempt >= SESSION_RETRY_DELAYS_MS.length) {
|
|
73
|
+
throw (
|
|
74
|
+
lastError ?? new Error("The Clips session endpoint is unavailable.")
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
await pause(SESSION_RETRY_DELAYS_MS[attempt]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
useRouteLoaderData,
|
|
32
32
|
} from "react-router";
|
|
33
33
|
import type { LinksFunction, LoaderFunctionArgs } from "react-router";
|
|
34
|
+
import { toast } from "sonner";
|
|
34
35
|
|
|
35
36
|
import { BugReportDialog } from "@/components/bug-report/bug-report-dialog";
|
|
36
37
|
import { ClipsCommandMenu } from "@/components/clips-command-menu";
|
|
@@ -47,7 +48,11 @@ import {
|
|
|
47
48
|
import { Toaster } from "@/components/ui/sonner";
|
|
48
49
|
import { AppToolkitProvider } from "@/components/ui/toolkit-provider";
|
|
49
50
|
import { useNavigationState } from "@/hooks/use-navigation-state";
|
|
50
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
buildClipsExtensionBaseUrl,
|
|
53
|
+
fetchClipsExtensionSession,
|
|
54
|
+
type ClipsExtensionSession,
|
|
55
|
+
} from "@/lib/extension-auth";
|
|
51
56
|
import {
|
|
52
57
|
isLegacyRecordingPath,
|
|
53
58
|
isRecordingSharePath,
|
|
@@ -238,17 +243,24 @@ function ClipsExtensionAuthBridge() {
|
|
|
238
243
|
}
|
|
239
244
|
).chrome?.runtime;
|
|
240
245
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
let session: ClipsExtensionSession | null;
|
|
247
|
+
try {
|
|
248
|
+
session = await fetchClipsExtensionSession(
|
|
249
|
+
() =>
|
|
250
|
+
fetch(appPath("/_agent-native/auth/session"), {
|
|
251
|
+
credentials: "include",
|
|
252
|
+
cache: "no-store",
|
|
253
|
+
}),
|
|
254
|
+
() => !cancelled,
|
|
255
|
+
);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (!cancelled) {
|
|
258
|
+
console.error("Could not complete Clips extension sign-in.", error);
|
|
259
|
+
toast.error(t("recordingPage.tryAgainMoment"));
|
|
260
|
+
}
|
|
250
261
|
return;
|
|
251
262
|
}
|
|
263
|
+
if (cancelled || !session) return;
|
|
252
264
|
|
|
253
265
|
const message = {
|
|
254
266
|
source: "clips-auth-bridge",
|
|
@@ -193,6 +193,9 @@ export default function NotificationsRoute() {
|
|
|
193
193
|
</div>
|
|
194
194
|
<div className="flex items-end gap-2">
|
|
195
195
|
<CommentComposer
|
|
196
|
+
// A reply to another notification gets its own composer, so
|
|
197
|
+
// the cursor moves to it.
|
|
198
|
+
key={replyFor.id}
|
|
196
199
|
value={replyText}
|
|
197
200
|
onChange={setReplyText}
|
|
198
201
|
onMentionAdd={(mention) =>
|
|
@@ -129,6 +129,9 @@ export default function ImportRoute() {
|
|
|
129
129
|
const timeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
|
130
130
|
|
|
131
131
|
const busy = phase !== "form";
|
|
132
|
+
const requiresStorage = !isYouTubeUrl(loomUrl);
|
|
133
|
+
const storageBlocked =
|
|
134
|
+
requiresStorage && (storageQuery.isError || storageConfigured !== true);
|
|
132
135
|
|
|
133
136
|
const importStages = useMemo(
|
|
134
137
|
() => [
|
|
@@ -173,7 +176,7 @@ export default function ImportRoute() {
|
|
|
173
176
|
async (event: FormEvent<HTMLFormElement>) => {
|
|
174
177
|
event.preventDefault();
|
|
175
178
|
const url = loomUrl.trim();
|
|
176
|
-
if (!url || busy) return;
|
|
179
|
+
if (!url || busy || storageBlocked) return;
|
|
177
180
|
const failureTitle = isYouTubeUrl(url)
|
|
178
181
|
? t("recordRoute.couldNotImportVideo")
|
|
179
182
|
: t("recordRoute.couldNotImportLoom");
|
|
@@ -264,6 +267,7 @@ export default function ImportRoute() {
|
|
|
264
267
|
loomUrl,
|
|
265
268
|
navigate,
|
|
266
269
|
spaceIdFromUrl,
|
|
270
|
+
storageBlocked,
|
|
267
271
|
t,
|
|
268
272
|
],
|
|
269
273
|
);
|
|
@@ -304,120 +308,121 @@ export default function ImportRoute() {
|
|
|
304
308
|
</p>
|
|
305
309
|
|
|
306
310
|
<div className="mx-auto w-full max-w-lg">
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
311
|
+
<div
|
|
312
|
+
className={cn(
|
|
313
|
+
"overflow-hidden rounded-2xl border border-border bg-card shadow-lg transition-opacity duration-300",
|
|
314
|
+
phase === "leaving" && "opacity-0",
|
|
315
|
+
)}
|
|
316
|
+
>
|
|
317
|
+
{busy ? (
|
|
318
|
+
<div className="h-1 w-full overflow-hidden bg-muted">
|
|
319
|
+
<div
|
|
320
|
+
className="h-full bg-primary transition-[width] duration-200 ease-out"
|
|
321
|
+
style={{ width: `${Math.min(100, progress)}%` }}
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
) : null}
|
|
325
|
+
<div className="p-6">
|
|
326
|
+
{phase === "importing" ? (
|
|
327
|
+
<div className="flex flex-col items-center gap-2 py-8 text-center">
|
|
328
|
+
<AgentMark className="h-6 w-6 animate-pulse text-primary" />
|
|
329
|
+
<p
|
|
330
|
+
key={stageIndex}
|
|
331
|
+
className="text-base font-semibold text-foreground"
|
|
332
|
+
style={{ animation: "clips-benefit-in 260ms ease-out" }}
|
|
333
|
+
>
|
|
334
|
+
{importStages[stageIndex]}
|
|
335
|
+
</p>
|
|
336
|
+
<p className="max-w-xs text-xs text-muted-foreground">
|
|
337
|
+
{t("importRoute.importingSubtitle")}
|
|
338
|
+
</p>
|
|
324
339
|
</div>
|
|
325
|
-
) :
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
key={stageIndex}
|
|
332
|
-
className="text-base font-semibold text-foreground"
|
|
333
|
-
style={{ animation: "clips-benefit-in 260ms ease-out" }}
|
|
334
|
-
>
|
|
335
|
-
{importStages[stageIndex]}
|
|
340
|
+
) : phase === "done" || phase === "leaving" ? (
|
|
341
|
+
<div className="flex flex-col items-center gap-4 py-6 text-center">
|
|
342
|
+
<div className="flex items-center gap-2 text-primary">
|
|
343
|
+
<AgentMark className="h-6 w-6" />
|
|
344
|
+
<p className="text-base font-semibold text-foreground">
|
|
345
|
+
{t("importRoute.doneHeading")}
|
|
336
346
|
</p>
|
|
337
|
-
<p className="max-w-xs text-xs text-muted-foreground">
|
|
338
|
-
{t("importRoute.importingSubtitle")}
|
|
339
|
-
</p>
|
|
340
|
-
</div>
|
|
341
|
-
) : phase === "done" || phase === "leaving" ? (
|
|
342
|
-
<div className="flex flex-col items-center gap-4 py-6 text-center">
|
|
343
|
-
<div className="flex items-center gap-2 text-primary">
|
|
344
|
-
<AgentMark className="h-6 w-6" />
|
|
345
|
-
<p className="text-base font-semibold text-foreground">
|
|
346
|
-
{t("importRoute.doneHeading")}
|
|
347
|
-
</p>
|
|
348
|
-
</div>
|
|
349
|
-
<ul className="flex flex-col items-start gap-2 text-start">
|
|
350
|
-
{benefits.map((benefit, index) => (
|
|
351
|
-
<li
|
|
352
|
-
key={benefit}
|
|
353
|
-
className="flex items-center gap-2 text-sm font-medium text-foreground"
|
|
354
|
-
style={{
|
|
355
|
-
animation: "clips-benefit-in 220ms ease-out both",
|
|
356
|
-
animationDelay: `${index * 110}ms`,
|
|
357
|
-
}}
|
|
358
|
-
>
|
|
359
|
-
<IconCheck className="h-4 w-4 shrink-0 text-primary" />
|
|
360
|
-
{benefit}
|
|
361
|
-
</li>
|
|
362
|
-
))}
|
|
363
|
-
</ul>
|
|
364
347
|
</div>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
className="h-12 w-full gap-2"
|
|
381
|
-
disabled={!loomUrl.trim()}
|
|
382
|
-
>
|
|
383
|
-
<IconLink className="h-4 w-4" />
|
|
384
|
-
{t("importRoute.cta")}
|
|
385
|
-
</Button>
|
|
386
|
-
{loomError ? (
|
|
387
|
-
<p className="text-xs leading-relaxed text-destructive">
|
|
388
|
-
{loomError}
|
|
389
|
-
</p>
|
|
390
|
-
) : null}
|
|
391
|
-
</form>
|
|
392
|
-
)}
|
|
393
|
-
</div>
|
|
394
|
-
|
|
395
|
-
{!busy ? (
|
|
396
|
-
<div className="flex items-center justify-center gap-4 border-t border-border px-6 py-4">
|
|
397
|
-
<button
|
|
398
|
-
type="button"
|
|
399
|
-
onClick={() => openUploadPicker(recordHref)}
|
|
400
|
-
className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground"
|
|
401
|
-
>
|
|
402
|
-
<IconUpload className="h-3.5 w-3.5" />
|
|
403
|
-
{t("preRecord.uploadVideo")}
|
|
404
|
-
</button>
|
|
405
|
-
<Link
|
|
406
|
-
to={recordHref}
|
|
407
|
-
className="text-xs font-medium text-muted-foreground hover:text-foreground"
|
|
408
|
-
>
|
|
409
|
-
{t("preRecord.recordNew")}
|
|
410
|
-
</Link>
|
|
348
|
+
<ul className="flex flex-col items-start gap-2 text-start">
|
|
349
|
+
{benefits.map((benefit, index) => (
|
|
350
|
+
<li
|
|
351
|
+
key={benefit}
|
|
352
|
+
className="flex items-center gap-2 text-sm font-medium text-foreground"
|
|
353
|
+
style={{
|
|
354
|
+
animation: "clips-benefit-in 220ms ease-out both",
|
|
355
|
+
animationDelay: `${index * 110}ms`,
|
|
356
|
+
}}
|
|
357
|
+
>
|
|
358
|
+
<IconCheck className="h-4 w-4 shrink-0 text-primary" />
|
|
359
|
+
{benefit}
|
|
360
|
+
</li>
|
|
361
|
+
))}
|
|
362
|
+
</ul>
|
|
411
363
|
</div>
|
|
412
|
-
) :
|
|
364
|
+
) : (
|
|
365
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
|
366
|
+
<Input
|
|
367
|
+
autoFocus
|
|
368
|
+
value={loomUrl}
|
|
369
|
+
onChange={(event) => {
|
|
370
|
+
setLoomUrl(event.target.value);
|
|
371
|
+
setLoomError(null);
|
|
372
|
+
}}
|
|
373
|
+
placeholder={t("importRoute.urlPlaceholder")}
|
|
374
|
+
className="h-12 text-base"
|
|
375
|
+
inputMode="url"
|
|
376
|
+
/>
|
|
377
|
+
<Button
|
|
378
|
+
type="submit"
|
|
379
|
+
className="h-12 w-full gap-2"
|
|
380
|
+
disabled={!loomUrl.trim() || storageBlocked}
|
|
381
|
+
>
|
|
382
|
+
<IconLink className="h-4 w-4" />
|
|
383
|
+
{t("importRoute.cta")}
|
|
384
|
+
</Button>
|
|
385
|
+
{loomError ? (
|
|
386
|
+
<p className="text-xs leading-relaxed text-destructive">
|
|
387
|
+
{loomError}
|
|
388
|
+
</p>
|
|
389
|
+
) : null}
|
|
390
|
+
</form>
|
|
391
|
+
)}
|
|
413
392
|
</div>
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
393
|
+
|
|
394
|
+
{!busy ? (
|
|
395
|
+
<div className="flex items-center justify-center gap-4 border-t border-border px-6 py-4">
|
|
396
|
+
<button
|
|
397
|
+
type="button"
|
|
398
|
+
onClick={() => openUploadPicker(recordHref)}
|
|
399
|
+
className="inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground"
|
|
400
|
+
>
|
|
401
|
+
<IconUpload className="h-3.5 w-3.5" />
|
|
402
|
+
{t("preRecord.uploadVideo")}
|
|
403
|
+
</button>
|
|
404
|
+
<Link
|
|
405
|
+
to={recordHref}
|
|
406
|
+
className="text-xs font-medium text-muted-foreground hover:text-foreground"
|
|
407
|
+
>
|
|
408
|
+
{t("preRecord.recordNew")}
|
|
409
|
+
</Link>
|
|
410
|
+
</div>
|
|
411
|
+
) : null}
|
|
412
|
+
</div>
|
|
413
|
+
{loomUrl.trim() && requiresStorage ? (
|
|
414
|
+
storageQuery.isError ? (
|
|
415
|
+
<StorageStatusRetry onRetry={() => void storageQuery.refetch()} />
|
|
416
|
+
) : storageConfigured === null ? (
|
|
417
|
+
<ImportPanelSkeleton />
|
|
418
|
+
) : !storageConfigured ? (
|
|
419
|
+
<StorageSetupCard
|
|
420
|
+
onConfigured={markStorageConfigured}
|
|
421
|
+
connectSource="clips_import_storage_setup_card"
|
|
422
|
+
connectFlow="import"
|
|
423
|
+
/>
|
|
424
|
+
) : null
|
|
425
|
+
) : null}
|
|
421
426
|
</div>
|
|
422
427
|
</div>
|
|
423
428
|
{uploadInput}
|