@agent-native/core 0.155.0 → 0.157.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.
Files changed (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +19 -4
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +7 -2
  4. package/corpus/templates/assets/app/components/asset/AssetPreviewDialog.tsx +3 -1
  5. package/corpus/templates/assets/app/global.css +4 -0
  6. package/corpus/templates/assets/app/i18n-data.ts +21 -1
  7. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +18 -15
  8. package/corpus/templates/brain/app/routes/sources.tsx +1 -2
  9. package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
  10. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
  11. package/corpus/templates/calendar/server/handlers/bookings.ts +219 -93
  12. package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
  13. package/corpus/templates/clips/actions/create-folder.ts +7 -1
  14. package/corpus/templates/clips/actions/create-meeting.ts +6 -2
  15. package/corpus/templates/clips/actions/create-recording.ts +5 -3
  16. package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
  17. package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
  18. package/corpus/templates/clips/actions/request-recording-access.ts +272 -0
  19. package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
  20. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
  21. package/corpus/templates/clips/app/components/player/share-dialog.tsx +177 -134
  22. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
  23. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
  24. package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
  25. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
  26. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +24 -17
  27. package/corpus/templates/clips/app/routes/share.$shareId.tsx +151 -20
  28. package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
  29. package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
  30. package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
  31. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
  32. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
  33. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
  34. package/corpus/templates/clips/desktop/src/app.tsx +24 -0
  35. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
  36. package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
  37. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
  38. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  39. package/corpus/templates/clips/desktop/src/styles.css +24 -9
  40. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
  41. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
  42. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
  43. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
  44. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
  45. package/corpus/templates/clips/server/db/index.ts +1 -0
  46. package/corpus/templates/clips/server/db/schema.ts +1 -0
  47. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
  48. package/corpus/templates/clips/server/lib/emails.ts +23 -0
  49. package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
  50. package/corpus/templates/clips/server/lib/recordings.ts +2 -1
  51. package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
  52. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
  53. package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
  54. package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
  55. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
  56. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
  58. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
  59. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +19 -0
  60. package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
  61. package/corpus/templates/clips/shared/recording-link.ts +4 -0
  62. package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
  63. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
  64. package/corpus/templates/factory/AGENTS.md +5 -1
  65. package/corpus/templates/factory/README.md +6 -2
  66. package/corpus/templates/factory/actions/view-screen.ts +15 -0
  67. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
  68. package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
  69. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  70. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
  71. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
  72. package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
  73. package/corpus/templates/factory/app/routes/agents.tsx +9 -0
  74. package/corpus/templates/mail/server/plugins/auth.ts +0 -2
  75. package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
  76. package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
  77. package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +98 -29
  78. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +45 -91
  79. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +94 -2
  80. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +17 -29
  81. package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
  82. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +1 -1
  83. package/corpus/templates/slides/app/context/DeckContext.tsx +108 -19
  84. package/corpus/templates/slides/app/global.css +7 -40
  85. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  86. package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
  87. package/corpus/templates/slides/app/pages/DeckEditor.tsx +143 -45
  88. package/dist/agent/index.d.ts +1 -1
  89. package/dist/agent/production-agent.d.ts +30 -0
  90. package/dist/agent/production-agent.js +100 -8
  91. package/dist/agent/types.d.ts +9 -0
  92. package/dist/client/AgentPanel.d.ts +13 -1
  93. package/dist/client/AgentPanel.js +22 -17
  94. package/dist/client/AssistantChat.d.ts +15 -1
  95. package/dist/client/AssistantChat.js +135 -48
  96. package/dist/client/HighlightedCodeBlock.js +12 -2
  97. package/dist/client/MultiTabAssistantChat.js +44 -27
  98. package/dist/client/agent-chat/index.d.ts +1 -0
  99. package/dist/client/agent-chat/index.js +1 -0
  100. package/dist/client/chat/markdown-renderer.js +20 -5
  101. package/dist/client/chat/message-components.d.ts +2 -2
  102. package/dist/client/chat/message-components.js +69 -35
  103. package/dist/client/chat/run-recovery.d.ts +4 -1
  104. package/dist/client/chat/run-recovery.js +62 -30
  105. package/dist/client/chat/tool-call-display.d.ts +12 -3
  106. package/dist/client/chat/tool-call-display.js +79 -40
  107. package/dist/client/chat/widgets/DataChartRenderer.js +3 -1
  108. package/dist/client/chat/widgets/DataChartWidget.js +8 -2
  109. package/dist/client/chat/widgets/DataInsightsWidget.js +3 -1
  110. package/dist/client/chat/widgets/DataTableWidget.js +23 -9
  111. package/dist/client/chat/widgets/builtin-tool-renderers.js +3 -1
  112. package/dist/client/chat-first/apps-rail.js +1 -1
  113. package/dist/client/components/ui/message-scroller.d.ts +1 -1
  114. package/dist/client/components/ui/message-scroller.js +7 -3
  115. package/dist/client/composer/runtime-adapters.js +3 -2
  116. package/dist/client/context-xray/ContextMeter.js +5 -1
  117. package/dist/client/context-xray/ContextXRayPanel.js +5 -5
  118. package/dist/client/error-format.d.ts +4 -0
  119. package/dist/client/error-format.js +93 -5
  120. package/dist/client/i18n.js +83 -17
  121. package/dist/client/observability/ThumbsFeedback.js +10 -8
  122. package/dist/client/settings/AccountSettingsCard.js +48 -19
  123. package/dist/client/sharing/ShareButton.d.ts +1 -2
  124. package/dist/client/sharing/ShareButton.js +151 -81
  125. package/dist/client/sharing/ShareDialog.js +5 -4
  126. package/dist/client/use-chat-threads.js +10 -14
  127. package/dist/coding-tools/run-code.js +2 -1
  128. package/dist/coding-tools/sandbox/background.d.ts +1 -0
  129. package/dist/coding-tools/sandbox/background.js +8 -1
  130. package/dist/coding-tools/sandbox/executions-store.d.ts +2 -0
  131. package/dist/coding-tools/sandbox/executions-store.js +41 -3
  132. package/dist/collab/routes.d.ts +1 -1
  133. package/dist/collab/struct-routes.d.ts +1 -1
  134. package/dist/index.d.ts +1 -1
  135. package/dist/localization/core-messages/ar-SA.d.ts +3 -0
  136. package/dist/localization/core-messages/ar-SA.js +541 -0
  137. package/dist/localization/core-messages/de-DE.d.ts +3 -0
  138. package/dist/localization/core-messages/de-DE.js +513 -0
  139. package/dist/localization/core-messages/en-US.d.ts +513 -0
  140. package/dist/localization/core-messages/en-US.js +513 -0
  141. package/dist/localization/core-messages/es-ES.d.ts +3 -0
  142. package/dist/localization/core-messages/es-ES.js +520 -0
  143. package/dist/localization/core-messages/fr-FR.d.ts +3 -0
  144. package/dist/localization/core-messages/fr-FR.js +520 -0
  145. package/dist/localization/core-messages/hi-IN.d.ts +3 -0
  146. package/dist/localization/core-messages/hi-IN.js +513 -0
  147. package/dist/localization/core-messages/ja-JP.d.ts +3 -0
  148. package/dist/localization/core-messages/ja-JP.js +506 -0
  149. package/dist/localization/core-messages/ko-KR.d.ts +3 -0
  150. package/dist/localization/core-messages/ko-KR.js +506 -0
  151. package/dist/localization/core-messages/pt-BR.d.ts +3 -0
  152. package/dist/localization/core-messages/pt-BR.js +520 -0
  153. package/dist/localization/core-messages/zh-CN.d.ts +3 -0
  154. package/dist/localization/core-messages/zh-CN.js +506 -0
  155. package/dist/localization/core-messages/zh-TW.d.ts +3 -0
  156. package/dist/localization/core-messages/zh-TW.js +506 -0
  157. package/dist/localization/core-messages.d.ts +524 -0
  158. package/dist/localization/core-messages.js +124 -0
  159. package/dist/localization/default-messages.d.ts +3 -0
  160. package/dist/localization/default-messages.js +3 -0
  161. package/dist/notifications/routes.d.ts +1 -1
  162. package/dist/observability/routes.d.ts +5 -5
  163. package/dist/progress/routes.d.ts +1 -1
  164. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  165. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  166. package/dist/resources/handlers.d.ts +1 -1
  167. package/dist/secrets/routes.d.ts +9 -9
  168. package/dist/server/agent-chat/plugin-options.d.ts +7 -2
  169. package/dist/server/agent-chat-plugin.d.ts +10 -0
  170. package/dist/server/agent-chat-plugin.js +144 -41
  171. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  172. package/dist/server/agent-run-context.d.ts +7 -1
  173. package/dist/server/agent-run-context.js +9 -2
  174. package/dist/server/agent-teams-run-queue.d.ts +3 -0
  175. package/dist/server/agent-teams.js +38 -9
  176. package/dist/server/auth-marketing.d.ts +1 -0
  177. package/dist/server/auth-marketing.js +0 -2
  178. package/dist/server/auth.d.ts +1 -0
  179. package/dist/server/auth.js +18 -0
  180. package/dist/server/index.d.ts +1 -1
  181. package/dist/server/onboarding-html.d.ts +1 -0
  182. package/dist/server/onboarding-html.js +2 -88
  183. package/dist/server/realtime-token.d.ts +1 -1
  184. package/dist/server/request-context.d.ts +7 -0
  185. package/dist/server/request-context.js +15 -0
  186. package/dist/shared/markdown-block-split.d.ts +16 -0
  187. package/dist/shared/markdown-block-split.js +81 -5
  188. package/dist/styles/agent-native.css +13 -0
  189. package/dist/voice/voice-cleanup-prompt.js +1 -0
  190. package/docs/content/agent-surfaces.mdx +30 -0
  191. package/package.json +2 -2
  192. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +0 -350
@@ -16,6 +16,7 @@ import {
16
16
  isHumanReadableDocumentTitle,
17
17
  normalizeDocumentTitle,
18
18
  } from "@agent-native/core/shared";
19
+ import { ShareTrigger } from "@agent-native/toolkit/sharing";
19
20
  import {
20
21
  BUILDER_CREDITS_UPGRADE_URL,
21
22
  type BuilderCreditsStatus,
@@ -33,7 +34,6 @@ import {
33
34
  } from "@shared/share-attribution";
34
35
  import type { WorkflowKind } from "@shared/workflow";
35
36
  import {
36
- IconShare3,
37
37
  IconArrowLeft,
38
38
  IconChevronDown,
39
39
  IconCalendar,
@@ -529,6 +529,18 @@ export default function RecordingPage() {
529
529
  const canDownloadRecording = Boolean(
530
530
  recording?.enableDownloads && recording.videoUrl && !isLoomEmbedBacked,
531
531
  );
532
+ // Mirrors the /share/:shareId reshare restriction (same public/org scope):
533
+ // a plain viewer of a public or org clip must not trigger
534
+ // `list-resource-shares` (any read access is enough to call it, and its
535
+ // response includes every individually-shared principal's email) or see a
536
+ // raw video download/open action independent of `enableDownloads`.
537
+ const viewerReshareOnly =
538
+ (role === "viewer" || role === "commenter") &&
539
+ (recording?.visibility === "public" || recording?.visibility === "org");
540
+ const shareVideoUrl =
541
+ canDownloadRecording || isLoomEmbedBacked
542
+ ? (recording?.videoUrl ?? null)
543
+ : null;
532
544
  const downloadRecording = useCallback(async () => {
533
545
  if (!recording?.videoUrl) return;
534
546
  setDownloading(true);
@@ -988,13 +1000,12 @@ export default function RecordingPage() {
988
1000
  initialVisibility={recording.visibility}
989
1001
  initialRole={role}
990
1002
  hasPassword={Boolean(recording.hasPassword)}
1003
+ viewerReshareOnly={viewerReshareOnly}
991
1004
  >
992
- <Button className="shrink-0 gap-1.5" size="sm">
993
- {recording.visibility !== "public" ? (
994
- <IconShare3 className="h-4 w-4" />
995
- ) : null}
996
- {t("recordingPage.share")}
997
- </Button>
1005
+ <ShareTrigger
1006
+ label={t("recordingPage.share")}
1007
+ className="shrink-0"
1008
+ />
998
1009
  </ShareRecordingPopover>
999
1010
  </header>
1000
1011
 
@@ -1550,21 +1561,17 @@ export default function RecordingPage() {
1550
1561
  recordingTitle={recording.title}
1551
1562
  initialVisibility={recording.visibility}
1552
1563
  initialRole={role}
1553
- videoUrl={recording.videoUrl}
1564
+ videoUrl={shareVideoUrl}
1554
1565
  thumbnailUrl={recording.thumbnailUrl}
1555
1566
  animatedThumbnailUrl={recording.animatedThumbnailUrl}
1556
1567
  isLoomRecording={isLoomEmbedBacked}
1557
1568
  hasPassword={Boolean(recording.hasPassword)}
1569
+ viewerReshareOnly={viewerReshareOnly}
1558
1570
  >
1559
- <Button
1560
- className="shrink-0 gap-1.5 bg-primary text-primary-foreground hover:bg-primary/90"
1561
- size="sm"
1562
- >
1563
- {recording.visibility !== "public" ? (
1564
- <IconShare3 className="h-4 w-4" />
1565
- ) : null}
1566
- {t("recordingPage.share")}
1567
- </Button>
1571
+ <ShareTrigger
1572
+ label={t("recordingPage.share")}
1573
+ className="shrink-0"
1574
+ />
1568
1575
  </ShareRecordingPopover>
1569
1576
  ) : null}
1570
1577
 
@@ -5,10 +5,15 @@ import {
5
5
  appBasePath,
6
6
  appPath,
7
7
  } from "@agent-native/core/client/api-path";
8
- import { useSession, getBrowserTabId } from "@agent-native/core/client/hooks";
8
+ import {
9
+ useActionMutation,
10
+ useSession,
11
+ getBrowserTabId,
12
+ } from "@agent-native/core/client/hooks";
9
13
  import { useT } from "@agent-native/core/client/i18n";
10
14
  import { buildSignInReturnHref } from "@agent-native/core/client/ui";
11
15
  import { docsUrl } from "@agent-native/core/shared";
16
+ import { ShareTrigger } from "@agent-native/toolkit/sharing";
12
17
  import {
13
18
  IconAlertTriangle,
14
19
  IconArrowLeft,
@@ -16,8 +21,8 @@ import {
16
21
  IconDownload,
17
22
  IconDots,
18
23
  IconExternalLink,
24
+ IconLock,
19
25
  IconLogin2,
20
- IconShare3,
21
26
  } from "@tabler/icons-react";
22
27
  import { useQuery } from "@tanstack/react-query";
23
28
  import { eq } from "drizzle-orm";
@@ -87,6 +92,10 @@ import {
87
92
  isLoomEmbedBackedRecording,
88
93
  isLoomRecordingSource,
89
94
  } from "../../shared/loom";
95
+ import {
96
+ CLIPS_ACCESS_REQUEST_TOKEN_PREFIX,
97
+ CLIPS_ACCESS_REQUEST_TOKEN_TTL_SECONDS,
98
+ } from "../../shared/recording-link";
90
99
  import {
91
100
  buildSignupAttributionQuery,
92
101
  readShareAttribution,
@@ -116,16 +125,22 @@ type SharePageLoaderData = {
116
125
  agentContextUrl: string | null;
117
126
  origin: string | null;
118
127
  shareUrl: string | null;
128
+ accessDeniedStatus?: 401 | 403;
129
+ accessRequestToken?: string;
119
130
  };
120
131
 
121
132
  const CLIPS_AGENT_ACCESS_TTL_SECONDS = 2 * 60 * 60;
122
133
 
123
- function emptyLoaderData(url: URL): SharePageLoaderData {
134
+ function emptyLoaderData(
135
+ url: URL,
136
+ accessDeniedStatus?: 401 | 403,
137
+ ): SharePageLoaderData {
124
138
  return {
125
139
  recording: null,
126
140
  agentContextUrl: null,
127
141
  origin: url.origin,
128
142
  shareUrl: null,
143
+ accessDeniedStatus,
129
144
  };
130
145
  }
131
146
 
@@ -224,7 +239,19 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
224
239
  if (rec.visibility !== "public" && !tokenGrantsAgentAccess) {
225
240
  const userEmail = getRequestUserEmail();
226
241
  const access = userEmail ? await resolveAccess("recording", id) : null;
227
- if (!access) return privateShareLoaderData(emptyLoaderData(url));
242
+ if (!access) {
243
+ const status = userEmail ? 403 : 401;
244
+ const deniedData = emptyLoaderData(url, status);
245
+ if (userEmail) {
246
+ deniedData.accessRequestToken = signScopedAgentAccessToken({
247
+ resourceKind: CLIPS_ACCESS_REQUEST_TOKEN_PREFIX,
248
+ resourceId: id,
249
+ viewerEmail: userEmail,
250
+ ttlSeconds: CLIPS_ACCESS_REQUEST_TOKEN_TTL_SECONDS,
251
+ });
252
+ }
253
+ return privateShareLoaderData(deniedData, status);
254
+ }
228
255
  }
229
256
 
230
257
  const recording: SharePageMetaRecording = {
@@ -418,6 +445,16 @@ export default function ShareRoute() {
418
445
  const [pwError, setPwError] = useState<string | null>(null);
419
446
  const [currentMs, setCurrentMs] = useState(0);
420
447
  const { session, isLoading: sessionLoading } = useSession();
448
+ const requestAccess = useActionMutation<
449
+ {
450
+ alreadyHasAccess: boolean;
451
+ alreadyRequested?: boolean;
452
+ message: string;
453
+ notifiedOwner: boolean;
454
+ ok: true;
455
+ },
456
+ { accessRequestToken?: string; recordingId: string }
457
+ >("request-recording-access");
421
458
  const [signInIntent, setSignInIntent] = useState<"comment" | "react" | null>(
422
459
  null,
423
460
  );
@@ -429,6 +466,7 @@ export default function ShareRoute() {
429
466
  );
430
467
  const [downloading, setDownloading] = useState(false);
431
468
  const [downloadMenuOpen, setDownloadMenuOpen] = useState(false);
469
+ const [accessRequestSent, setAccessRequestSent] = useState(false);
432
470
  const agentAccessToken = useMemo(() => {
433
471
  if (typeof window === "undefined") return "";
434
472
  return (
@@ -518,6 +556,16 @@ export default function ShareRoute() {
518
556
  const transcriptFailureReason =
519
557
  dataQ.data?.data?.transcript?.failureReason ?? null;
520
558
  const ctas = dataQ.data?.data?.ctas ?? [];
559
+ const apiStatus = dataQ.data?.status;
560
+ const apiAccessDeniedStatus =
561
+ apiStatus === 401 || apiStatus === 403 ? apiStatus : null;
562
+ const accessDeniedStatus =
563
+ apiAccessDeniedStatus ??
564
+ (!recording && loaderData.accessDeniedStatus
565
+ ? session
566
+ ? 403
567
+ : 401
568
+ : null);
521
569
  const firstCta = ctas[0] ?? null;
522
570
  const viewerRole = dataQ.data?.data?.viewer?.role as
523
571
  | "owner"
@@ -537,6 +585,15 @@ export default function ShareRoute() {
537
585
  viewerRole === "editor" ||
538
586
  viewerRole === "commenter";
539
587
  const viewerIsOwner = Boolean(dataQ.data?.data?.viewer?.isOwner);
588
+ const canReshareLink =
589
+ (viewerRole === "viewer" || viewerRole === "commenter") &&
590
+ (recording?.visibility === "public" || recording?.visibility === "org");
591
+ // A plain viewer only gets a copy-link control: it must not trigger
592
+ // `list-resource-shares` (any read access is enough to call it, and its
593
+ // response includes every individually-shared principal's email) and must
594
+ // not surface the raw video download/open action independent of
595
+ // `enableDownloads`.
596
+ const viewerReshareOnly = canReshareLink && !viewerCanEdit;
540
597
  const viewerCanOpenDashboard = Boolean(
541
598
  dataQ.data?.data?.viewer?.canOpenDashboard,
542
599
  );
@@ -742,18 +799,58 @@ export default function ShareRoute() {
742
799
  );
743
800
  }
744
801
 
745
- if (dataQ.data?.status === 401 || dataQ.data?.status === 404) {
802
+ if (accessDeniedStatus === 401 || accessDeniedStatus === 403) {
803
+ const canRequestAccess = accessDeniedStatus === 403 && Boolean(session);
804
+ const requestSent =
805
+ accessRequestSent || Boolean(dataQ.data?.data?.alreadyRequested);
806
+
746
807
  return (
747
808
  <>
748
809
  {agentDiscovery}
749
810
  <EndState
750
- title={t("sharePage.clipUnavailable")}
751
- message={t("sharePage.clipUnavailableMessage")}
811
+ icon={<IconLock className="h-5 w-5" aria-hidden="true" />}
812
+ title={t("sharePage.privateClip")}
813
+ message={t(
814
+ canRequestAccess
815
+ ? "sharePage.privateClipMessage"
816
+ : "sharePage.privateClipSignedOutMessage",
817
+ )}
752
818
  action={
753
- shareId ? (
819
+ canRequestAccess ? (
820
+ <Button
821
+ size="sm"
822
+ disabled={requestAccess.isPending || requestSent}
823
+ onClick={() => {
824
+ if (!shareId || requestSent) return;
825
+ requestAccess.mutate(
826
+ {
827
+ accessRequestToken: loaderData.accessRequestToken,
828
+ recordingId: shareId,
829
+ },
830
+ {
831
+ onSuccess: () => {
832
+ setAccessRequestSent(true);
833
+ toast.success(t("sharePage.accessRequestSent"));
834
+ },
835
+ onError: () => {
836
+ toast.error(t("sharePage.accessRequestFailed"));
837
+ },
838
+ },
839
+ );
840
+ }}
841
+ >
842
+ {requestSent
843
+ ? t("sharePage.accessRequested")
844
+ : requestAccess.isPending
845
+ ? t("sharePage.requestingAccess")
846
+ : t("sharePage.requestAccess")}
847
+ </Button>
848
+ ) : shareId ? (
754
849
  <Button asChild size="sm">
755
850
  <a
756
- href={buildSignInReturnHref({ returnTo: `/r/${shareId}` })}
851
+ href={buildSignInReturnHref({
852
+ returnTo: `/share/${shareId}`,
853
+ })}
757
854
  className="gap-1.5"
758
855
  >
759
856
  <IconLogin2 className="h-4 w-4 rtl:-scale-x-100" />
@@ -767,6 +864,18 @@ export default function ShareRoute() {
767
864
  );
768
865
  }
769
866
 
867
+ if (dataQ.data?.status === 404) {
868
+ return (
869
+ <>
870
+ {agentDiscovery}
871
+ <EndState
872
+ title={t("sharePage.clipUnavailable")}
873
+ message={t("sharePage.clipUnavailableMessage")}
874
+ />
875
+ </>
876
+ );
877
+ }
878
+
770
879
  if (!recording) {
771
880
  return (
772
881
  <>
@@ -899,6 +1008,10 @@ export default function ShareRoute() {
899
1008
  const canDownloadRecording = Boolean(
900
1009
  recording.enableDownloads && recording.videoUrl && !isLoomEmbedBacked,
901
1010
  );
1011
+ // Loom-backed clips only ever get an "open player" link (not a raw
1012
+ // download), so they're exempt from the enableDownloads gate here.
1013
+ const shareVideoUrl =
1014
+ canDownloadRecording || isLoomEmbedBacked ? recording.videoUrl : null;
902
1015
 
903
1016
  return (
904
1017
  <div className="flex min-h-screen max-w-full flex-col overflow-x-hidden bg-background text-foreground lg:h-screen lg:flex-row lg:overflow-hidden">
@@ -917,14 +1030,26 @@ export default function ShareRoute() {
917
1030
  </Link>
918
1031
  </Button>
919
1032
  ) : null}
920
- <div className="min-w-0 flex-1">
1033
+ <div className="flex min-w-0 flex-1 items-center gap-2">
1034
+ {recording.visibility === "private" ? (
1035
+ <span
1036
+ className="inline-flex shrink-0 text-muted-foreground"
1037
+ role="img"
1038
+ title={t("sharePage.privateClip")}
1039
+ >
1040
+ <IconLock className="h-3.5 w-3.5" aria-hidden="true" />
1041
+ <span className="sr-only">{t("sharePage.privateClip")}</span>
1042
+ </span>
1043
+ ) : null}
921
1044
  {showTitleSkeleton ? (
922
1045
  <Skeleton
923
1046
  aria-label={t("sharePage.generatingTitle")}
924
- className="h-4 w-56 max-w-full"
1047
+ className="h-4 w-56 max-w-full flex-1"
925
1048
  />
926
1049
  ) : (
927
- <h1 className="truncate text-sm font-medium">{visibleTitle}</h1>
1050
+ <h1 className="min-w-0 flex-1 truncate text-sm font-medium">
1051
+ {visibleTitle}
1052
+ </h1>
928
1053
  )}
929
1054
  </div>
930
1055
 
@@ -995,24 +1120,23 @@ export default function ShareRoute() {
995
1120
  onDeleted={() => navigate("/library", { replace: true })}
996
1121
  />
997
1122
  ) : null}
998
- {viewerCanEdit ? (
1123
+ {viewerCanEdit || canReshareLink ? (
999
1124
  <ShareRecordingPopover
1000
1125
  recordingId={recording.id}
1001
1126
  recordingTitle={recording.title}
1002
1127
  initialVisibility={recording.visibility}
1003
1128
  initialRole={viewerIsOwner ? "owner" : undefined}
1004
- videoUrl={recording.videoUrl}
1129
+ videoUrl={shareVideoUrl}
1005
1130
  thumbnailUrl={recording.thumbnailUrl}
1006
1131
  animatedThumbnailUrl={recording.animatedThumbnailUrl}
1007
1132
  isLoomRecording={isLoomEmbedBacked}
1008
1133
  hasPassword={Boolean(recording.hasPassword)}
1134
+ viewerReshareOnly={viewerReshareOnly}
1009
1135
  >
1010
- <Button size="sm" className="shrink-0 gap-1.5">
1011
- {recording.visibility !== "public" ? (
1012
- <IconShare3 className="h-4 w-4" />
1013
- ) : null}
1014
- {t("sharePage.share")}
1015
- </Button>
1136
+ <ShareTrigger
1137
+ label={t("sharePage.share")}
1138
+ className="shrink-0"
1139
+ />
1016
1140
  </ShareRecordingPopover>
1017
1141
  ) : null}
1018
1142
  </div>
@@ -1337,10 +1461,12 @@ function PublicAgentEmptyState({
1337
1461
  }
1338
1462
 
1339
1463
  function EndState({
1464
+ icon,
1340
1465
  title,
1341
1466
  message,
1342
1467
  action,
1343
1468
  }: {
1469
+ icon?: ReactNode;
1344
1470
  title: string;
1345
1471
  message: string;
1346
1472
  action?: ReactNode;
@@ -1349,6 +1475,11 @@ function EndState({
1349
1475
 
1350
1476
  return (
1351
1477
  <div className="flex flex-col items-center justify-center min-h-screen bg-background text-foreground px-6">
1478
+ {icon ? (
1479
+ <div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full border border-border bg-muted text-muted-foreground">
1480
+ {icon}
1481
+ </div>
1482
+ ) : null}
1352
1483
  <h1 className="text-2xl font-semibold mb-2">{title}</h1>
1353
1484
  <p className="mb-6 max-w-md text-center text-sm text-muted-foreground">
1354
1485
  {message}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-14
4
+ ---
5
+
6
+ Long desktop recordings now complete or abort resumable uploads cleanly, preserve local recovery copies, and show the actual upload error when saving fails.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-14
4
+ ---
5
+
6
+ Clips now uses consistent text-only share controls with compact copy actions and an expandable Share with agents section across recordings and meetings.
@@ -3,7 +3,7 @@
3
3
  "name": "Agent-Native Clips",
4
4
  "short_name": "Clips",
5
5
  "version": "0.1.18",
6
- "description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub.",
6
+ "description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub and Jira.",
7
7
  "minimum_chrome_version": "116",
8
8
  "action": {
9
9
  "default_title": "Record with Clips",
@@ -35,7 +35,11 @@
35
35
  ],
36
36
  "content_scripts": [
37
37
  {
38
- "matches": ["https://github.com/*"],
38
+ "matches": [
39
+ "https://github.com/*",
40
+ "https://*.atlassian.net/*",
41
+ "https://*.jira.com/*"
42
+ ],
39
43
  "js": ["assets/github-preview-content.js"],
40
44
  "run_at": "document_idle",
41
45
  "all_frames": false
@@ -48,7 +52,11 @@
48
52
  },
49
53
  {
50
54
  "resources": ["src/github-preview.html"],
51
- "matches": ["https://github.com/*"]
55
+ "matches": [
56
+ "https://github.com/*",
57
+ "https://*.atlassian.net/*",
58
+ "https://*.jira.com/*"
59
+ ]
52
60
  }
53
61
  ],
54
62
  "externally_connectable": {
@@ -1,13 +1,14 @@
1
- // GitHub Clips link previews. This is a narrow declarative content script for
2
- // github.com only; the recording overlay remains activeTab-injected.
3
- (function clipsGithubPreviewContent() {
4
- const ROOT_CLASS = "clips-github-preview";
5
- const PROCESSED_ATTR = "data-clips-github-preview";
1
+ // Clips link previews for GitHub and Jira Cloud. The recording overlay remains
2
+ // activeTab-injected so this content script only handles rendered issue text.
3
+ (function clipsLinkPreviewContent() {
4
+ const ROOT_CLASS = "clips-link-preview";
5
+ const PROCESSED_ATTR = "data-clips-link-preview";
6
6
  const SOURCE_ATTR = "data-clips-source-url";
7
7
  const EXTENSION_ORIGIN = chrome.runtime.getURL("").replace(/\/$/, "");
8
8
  const CLIPS_HOSTS = new Set(["clips.agent-native.com"]);
9
9
  const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1"]);
10
10
  const ROUTE_SEGMENTS = new Set(["r", "share", "embed"]);
11
+ const JIRA_HOST_SUFFIXES = [".atlassian.net", ".jira.com"];
11
12
  const MIN_HEIGHT = 120;
12
13
  const MAX_HEIGHT = 720;
13
14
 
@@ -115,6 +116,34 @@
115
116
  );
116
117
  }
117
118
 
119
+ function isJiraPage(): boolean {
120
+ const hostname = location.hostname.toLowerCase();
121
+ return JIRA_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix));
122
+ }
123
+
124
+ function isJiraIssueContentAnchor(anchor: HTMLAnchorElement): boolean {
125
+ if (anchor.closest(`.${ROOT_CLASS}`)) return false;
126
+ return Boolean(
127
+ anchor.closest(
128
+ [
129
+ '[data-testid="issue.views.issue-base.foundation.description.text"]',
130
+ '[data-testid^="issue.views.issue-base.foundation.description"]',
131
+ '[data-testid^="issue.views.issue-base.common.comment"]',
132
+ '[data-testid^="issue.views.issue-base.common.comments"]',
133
+ '[data-testid*="comment"]',
134
+ ".ak-renderer-document",
135
+ ".ak-renderer-document-container",
136
+ ].join(","),
137
+ ),
138
+ );
139
+ }
140
+
141
+ function isSupportedContentAnchor(anchor: HTMLAnchorElement): boolean {
142
+ return isJiraPage()
143
+ ? isJiraIssueContentAnchor(anchor)
144
+ : isGitHubMarkdownAnchor(anchor);
145
+ }
146
+
118
147
  function previewInsertionTarget(anchor: HTMLAnchorElement): HTMLElement {
119
148
  const block = anchor.closest(
120
149
  [
@@ -145,7 +174,7 @@
145
174
  return;
146
175
  }
147
176
 
148
- const frameId = `clips-github-preview-${++frameCounter}`;
177
+ const frameId = `clips-link-preview-${++frameCounter}`;
149
178
  const wrapper = document.createElement("div");
150
179
  wrapper.className = ROOT_CLASS;
151
180
  wrapper.id = `${frameId}-wrapper`;
@@ -195,7 +224,7 @@
195
224
  for (const anchor of document.querySelectorAll<HTMLAnchorElement>(
196
225
  `a[href]:not([${PROCESSED_ATTR}])`,
197
226
  )) {
198
- if (!isGitHubMarkdownAnchor(anchor)) continue;
227
+ if (!isSupportedContentAnchor(anchor)) continue;
199
228
  const link = parseClipsLink(anchor.href);
200
229
  if (!link) continue;
201
230
  mountPreview(anchor, link);
@@ -212,7 +241,7 @@
212
241
  const data = event.data as
213
242
  | { source?: string; frameId?: string; height?: number }
214
243
  | undefined;
215
- if (data?.source !== "clips-github-preview") return;
244
+ if (data?.source !== "clips-link-preview") return;
216
245
  if (!data.frameId || typeof data.height !== "number") return;
217
246
  const frame = document.getElementById(data.frameId);
218
247
  if (!(frame instanceof HTMLIFrameElement)) return;
@@ -1,6 +1,6 @@
1
1
  import { captureExtensionError, initExtensionSentry } from "./sentry";
2
2
 
3
- initExtensionSentry("github-preview");
3
+ initExtensionSentry("link-preview");
4
4
 
5
5
  const DEFAULT_HOST = "clips.agent-native.com";
6
6
  const CLIPS_HOSTS = new Set(["clips.agent-native.com"]);
@@ -80,7 +80,7 @@ function postHeight(frameId: string): void {
80
80
  const height = document.documentElement.scrollHeight;
81
81
  window.parent.postMessage(
82
82
  {
83
- source: "clips-github-preview",
83
+ source: "clips-link-preview",
84
84
  frameId,
85
85
  height,
86
86
  },
@@ -415,7 +415,7 @@ async function main(): Promise<void> {
415
415
  renderPreview(root, preview, await checkPublicPlayable(preview));
416
416
  } catch (error) {
417
417
  captureExtensionError(error, {
418
- tags: { surface: "github-preview" },
418
+ tags: { surface: "link-preview" },
419
419
  extra: { pageUrl: location.href },
420
420
  });
421
421
  }
@@ -22,7 +22,6 @@ import {
22
22
  isLikelyPhoneMicLabel,
23
23
  type AudioInputFallback,
24
24
  } from "@shared/media-device-selection";
25
- import { createMicAudioCleanup } from "@shared/mic-audio-cleanup";
26
25
  import { scheduleReadyChime } from "@shared/recording-audio";
27
26
  import {
28
27
  SCREEN_CAPTURE_FRAME_RATE,
@@ -435,14 +434,9 @@ async function getCameraStream(deviceId: string): Promise<MediaStream> {
435
434
  }
436
435
  }
437
436
 
438
- // Phones/Continuity-style mics already apply their own echo cancellation,
439
- // noise suppression, and gain control before the audio ever reaches Chrome.
440
- // Stacking Chrome's versions of the same processing on top double-processes
441
- // the signal and audibly degrades it. We can't detect "is this device doing
442
- // its own processing" directly, so we use the device label as a heuristic and
443
- // ask the browser to SKIP its own noise suppression / AGC for those devices
444
- // (`{ ideal: false }` — a best-effort request, not a hard requirement). Echo
445
- // cancellation stays on for everything.
437
+ // Let the browser apply its built-in voice processing. Device labels are not a
438
+ // reliable signal for whether a hardware path already processed its audio, so
439
+ // do not branch capture quality on a label heuristic.
446
440
  async function chooseFallbackMicDevice(
447
441
  requestedLabel: string,
448
442
  avoidDeviceIds: string[] = [],
@@ -459,13 +453,12 @@ async function chooseFallbackMicDevice(
459
453
 
460
454
  function voiceFocusedAudioConstraints(
461
455
  deviceId: string,
462
- deviceLabel = "",
456
+ _deviceLabel = "",
463
457
  ): MediaTrackConstraints {
464
- const skipRedundantProcessing = isLikelyPhoneMicLabel(deviceLabel);
465
458
  const audio: MediaTrackConstraints = {
466
459
  echoCancellation: true,
467
- noiseSuppression: skipRedundantProcessing ? { ideal: false } : true,
468
- autoGainControl: skipRedundantProcessing ? { ideal: false } : true,
460
+ noiseSuppression: true,
461
+ autoGainControl: true,
469
462
  channelCount: 1,
470
463
  };
471
464
  if (deviceId) audio.deviceId = { exact: deviceId };
@@ -811,20 +804,11 @@ async function createMixedAudio(
811
804
  const audioContext = new AudioContext();
812
805
  await audioContext.resume().catch(() => undefined);
813
806
  const destination = audioContext.createMediaStreamDestination();
814
- const micCleanup: Array<{ stop: () => void }> = [];
815
807
  for (const input of audioInputs) {
816
- let track = input.track;
817
- if (input.isMicrophone) {
818
- const cleanup = createMicAudioCleanup(new MediaStream([track]), {
819
- audioContext,
820
- });
821
- micCleanup.push(cleanup);
822
- track = cleanup.stream.getAudioTracks()[0] ?? track;
823
- }
824
808
  // One source per track (not per stream) so each input is isolated in the
825
809
  // mix graph and can be detached independently.
826
810
  const source = audioContext.createMediaStreamSource(
827
- new MediaStream([track]),
811
+ new MediaStream([input.track]),
828
812
  );
829
813
  source.connect(destination);
830
814
  // A mixed input track can end mid-recording — most commonly the shared
@@ -832,7 +816,7 @@ async function createMixedAudio(
832
816
  // just that dead source so the mixed output keeps carrying the surviving
833
817
  // inputs (the microphone). Without this the whole mixed destination can go
834
818
  // silent, dropping both the tab audio and the mic for the rest of the clip.
835
- track.addEventListener("ended", () => {
819
+ input.track.addEventListener("ended", () => {
836
820
  try {
837
821
  source.disconnect();
838
822
  } catch {
@@ -847,7 +831,6 @@ async function createMixedAudio(
847
831
  cleanup() {
848
832
  if (cleanedUp) return;
849
833
  cleanedUp = true;
850
- for (const cleanup of micCleanup) cleanup.stop();
851
834
  },
852
835
  };
853
836
  }