@agent-native/core 0.155.0 → 0.157.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.
Files changed (95) hide show
  1. package/corpus/templates/assets/app/components/asset/AssetPreviewDialog.tsx +3 -1
  2. package/corpus/templates/assets/app/i18n-data.ts +21 -1
  3. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +17 -13
  4. package/corpus/templates/clips/app/components/player/share-dialog.tsx +102 -59
  5. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +15 -1
  6. package/corpus/templates/clips/app/routes/share.$shareId.tsx +16 -2
  7. package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +98 -29
  8. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +45 -91
  9. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +94 -2
  10. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +17 -29
  11. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +1 -1
  12. package/corpus/templates/slides/app/context/DeckContext.tsx +42 -2
  13. package/corpus/templates/slides/app/global.css +7 -40
  14. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  15. package/corpus/templates/slides/app/pages/DeckEditor.tsx +130 -45
  16. package/dist/agent/index.d.ts +1 -1
  17. package/dist/agent/production-agent.d.ts +30 -0
  18. package/dist/agent/production-agent.js +100 -8
  19. package/dist/agent/types.d.ts +9 -0
  20. package/dist/client/AgentPanel.js +12 -9
  21. package/dist/client/AssistantChat.d.ts +15 -1
  22. package/dist/client/AssistantChat.js +110 -43
  23. package/dist/client/MultiTabAssistantChat.js +44 -27
  24. package/dist/client/chat/markdown-renderer.js +4 -2
  25. package/dist/client/chat/message-components.d.ts +2 -2
  26. package/dist/client/chat/message-components.js +69 -35
  27. package/dist/client/chat/run-recovery.d.ts +4 -1
  28. package/dist/client/chat/run-recovery.js +62 -30
  29. package/dist/client/chat/tool-call-display.d.ts +12 -3
  30. package/dist/client/chat/tool-call-display.js +79 -40
  31. package/dist/client/chat/widgets/DataChartRenderer.js +3 -1
  32. package/dist/client/chat/widgets/DataChartWidget.js +8 -2
  33. package/dist/client/chat/widgets/DataInsightsWidget.js +3 -1
  34. package/dist/client/chat/widgets/DataTableWidget.js +23 -9
  35. package/dist/client/chat/widgets/builtin-tool-renderers.js +3 -1
  36. package/dist/client/components/ui/message-scroller.d.ts +1 -1
  37. package/dist/client/components/ui/message-scroller.js +7 -3
  38. package/dist/client/composer/runtime-adapters.js +3 -2
  39. package/dist/client/context-xray/ContextMeter.js +5 -1
  40. package/dist/client/context-xray/ContextXRayPanel.js +5 -5
  41. package/dist/client/error-format.d.ts +4 -0
  42. package/dist/client/error-format.js +93 -5
  43. package/dist/client/i18n.js +83 -17
  44. package/dist/client/observability/ThumbsFeedback.js +10 -8
  45. package/dist/client/sharing/ShareButton.js +160 -54
  46. package/dist/coding-tools/run-code.js +2 -1
  47. package/dist/coding-tools/sandbox/background.d.ts +1 -0
  48. package/dist/coding-tools/sandbox/background.js +8 -1
  49. package/dist/coding-tools/sandbox/executions-store.d.ts +2 -0
  50. package/dist/coding-tools/sandbox/executions-store.js +41 -3
  51. package/dist/collab/routes.d.ts +1 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/localization/core-messages/ar-SA.d.ts +3 -0
  54. package/dist/localization/core-messages/ar-SA.js +541 -0
  55. package/dist/localization/core-messages/de-DE.d.ts +3 -0
  56. package/dist/localization/core-messages/de-DE.js +513 -0
  57. package/dist/localization/core-messages/en-US.d.ts +513 -0
  58. package/dist/localization/core-messages/en-US.js +513 -0
  59. package/dist/localization/core-messages/es-ES.d.ts +3 -0
  60. package/dist/localization/core-messages/es-ES.js +520 -0
  61. package/dist/localization/core-messages/fr-FR.d.ts +3 -0
  62. package/dist/localization/core-messages/fr-FR.js +520 -0
  63. package/dist/localization/core-messages/hi-IN.d.ts +3 -0
  64. package/dist/localization/core-messages/hi-IN.js +513 -0
  65. package/dist/localization/core-messages/ja-JP.d.ts +3 -0
  66. package/dist/localization/core-messages/ja-JP.js +506 -0
  67. package/dist/localization/core-messages/ko-KR.d.ts +3 -0
  68. package/dist/localization/core-messages/ko-KR.js +506 -0
  69. package/dist/localization/core-messages/pt-BR.d.ts +3 -0
  70. package/dist/localization/core-messages/pt-BR.js +520 -0
  71. package/dist/localization/core-messages/zh-CN.d.ts +3 -0
  72. package/dist/localization/core-messages/zh-CN.js +506 -0
  73. package/dist/localization/core-messages/zh-TW.d.ts +3 -0
  74. package/dist/localization/core-messages/zh-TW.js +506 -0
  75. package/dist/localization/core-messages.d.ts +524 -0
  76. package/dist/localization/core-messages.js +124 -0
  77. package/dist/localization/default-messages.d.ts +2 -0
  78. package/dist/localization/default-messages.js +2 -0
  79. package/dist/notifications/routes.d.ts +1 -1
  80. package/dist/observability/routes.d.ts +5 -5
  81. package/dist/resources/handlers.d.ts +1 -1
  82. package/dist/secrets/routes.d.ts +9 -9
  83. package/dist/server/agent-chat/plugin-options.d.ts +7 -2
  84. package/dist/server/agent-chat-plugin.d.ts +10 -0
  85. package/dist/server/agent-chat-plugin.js +144 -41
  86. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  87. package/dist/server/agent-run-context.d.ts +7 -1
  88. package/dist/server/agent-run-context.js +9 -2
  89. package/dist/server/agent-teams-run-queue.d.ts +3 -0
  90. package/dist/server/agent-teams.js +38 -9
  91. package/dist/server/index.d.ts +1 -1
  92. package/dist/server/request-context.d.ts +7 -0
  93. package/dist/server/request-context.js +15 -0
  94. package/docs/content/agent-surfaces.mdx +30 -0
  95. package/package.json +2 -2
@@ -159,7 +159,9 @@ export function AssetPreviewDialog({
159
159
  onClick={() => setShowDetails((value) => !value)}
160
160
  >
161
161
  <IconInfoCircle className="h-4 w-4" />
162
- {t("library.viewDetails")}
162
+ {showDetails
163
+ ? t("library.hideDetails")
164
+ : t("library.viewDetails")}
163
165
  </Button>
164
166
  <DialogClose
165
167
  aria-label={t("library.closePreview")}
@@ -844,10 +844,11 @@ const enUS = {
844
844
  checking: "Checking",
845
845
  checkingUpload: "Checking upload",
846
846
  clearAll: "Clear all",
847
+ clearAllWithCount: "Clear all candidates ({{count}})",
847
848
  clearCandidates: "Clear candidates",
848
849
  clearCandidatesTitle: "Clear {{count}} candidates?",
849
850
  clearCandidatesDescription:
850
- "Clears the live stage and deletes unsaved draft candidates.",
851
+ "Clears the live stage and deletes every unsaved draft candidate. Saved library assets stay untouched.",
851
852
  clearGeneratedCandidatesTitle: "Clear generated candidates?",
852
853
  clearGeneratedCandidatesDescription:
853
854
  "This removes every unsaved candidate from the thread and deletes the generated asset rows behind them. Saved library assets are not touched.",
@@ -986,6 +987,7 @@ const enUS = {
986
987
  open: "Open",
987
988
  openAssets: "Open Assets",
988
989
  openChat: "Open chat",
990
+ hideDetails: "Hide details",
989
991
  openDetails: "Open details",
990
992
  preparing: "Preparing...",
991
993
  previousImage: "Previous image",
@@ -3728,7 +3730,9 @@ export const messagesByLocale = {
3728
3730
  checkingUpload: "正在检查上传",
3729
3731
  clear: "清除",
3730
3732
  clearAll: "全部清除",
3733
+ clearAllWithCount: "清除所有候选项({{count}})",
3731
3734
  clearCandidates: "清除候选项",
3735
+ hideDetails: "隐藏详情",
3732
3736
  clearCandidatesDescription: "清除实时阶段并删除未保存的草稿候选项。",
3733
3737
  clearCandidatesTitle: "清除 {{count}} 个候选项?",
3734
3738
  clearGeneratedCandidatesDescription:
@@ -4552,7 +4556,9 @@ export const messagesByLocale = {
4552
4556
  checkingImageLibraries: "Comprobando bibliotecas de imágenes...",
4553
4557
  checkingUpload: "Comprobando carga",
4554
4558
  clearAll: "Borrar todo",
4559
+ clearAllWithCount: "Borrar todos los candidatos ({{count}})",
4555
4560
  clearCandidates: "Limpiar candidatos",
4561
+ hideDetails: "Ocultar detalles",
4556
4562
  clearCandidatesDescription:
4557
4563
  "Limpia la etapa en vivo y elimina los candidatos de borrador no guardados.",
4558
4564
  clearGeneratedCandidatesDescription:
@@ -5026,7 +5032,9 @@ export const messagesByLocale = {
5026
5032
  checkingImageLibraries: "Vérification des bibliothèques d'images...",
5027
5033
  checkingUpload: "Vérification du téléchargement",
5028
5034
  clearAll: "Tout effacer",
5035
+ clearAllWithCount: "Effacer tous les candidats ({{count}})",
5029
5036
  clearCandidates: "Effacer les candidats",
5037
+ hideDetails: "Masquer les détails",
5030
5038
  clearCandidatesDescription:
5031
5039
  "Efface l'étape en direct et supprime les candidats de brouillon non enregistrés.",
5032
5040
  clearGeneratedCandidatesDescription:
@@ -5479,7 +5487,9 @@ export const messagesByLocale = {
5479
5487
  checkingImageLibraries: "Bildbibliotheken werden geprüft...",
5480
5488
  checkingUpload: "Upload wird überprüft",
5481
5489
  clearAll: "Alles löschen",
5490
+ clearAllWithCount: "Alle Kandidaten löschen ({{count}})",
5482
5491
  clearCandidates: "Kandidaten löschen",
5492
+ hideDetails: "Details ausblenden",
5483
5493
  clearCandidatesDescription:
5484
5494
  "Löscht die Live-Phase und entfernt nicht gespeicherte Entwurfskandidaten.",
5485
5495
  clearGeneratedCandidatesDescription:
@@ -5837,7 +5847,9 @@ export const messagesByLocale = {
5837
5847
  checkingImageLibraries: "画像ライブラリを確認中...",
5838
5848
  checkingUpload: "アップロードを確認しています",
5839
5849
  clearAll: "すべてクリア",
5850
+ clearAllWithCount: "候補をすべてクリア({{count}})",
5840
5851
  clearCandidates: "候補をクリアする",
5852
+ hideDetails: "詳細を非表示",
5841
5853
  clearCandidatesDescription:
5842
5854
  "ライブ ステージをクリアし、保存されていないドラフト候補を削除します。",
5843
5855
  clearGeneratedCandidatesDescription:
@@ -6186,7 +6198,9 @@ export const messagesByLocale = {
6186
6198
  checkingImageLibraries: "이미지 라이브러리 확인 중...",
6187
6199
  checkingUpload: "업로드 확인 중",
6188
6200
  clearAll: "모두 지우기",
6201
+ clearAllWithCount: "후보 모두 지우기({{count}})",
6189
6202
  clearCandidates: "후보 제거",
6203
+ hideDetails: "세부정보 숨기기",
6190
6204
  clearCandidatesDescription:
6191
6205
  "라이브 단계를 지우고 저장되지 않은 초안 후보를 삭제합니다.",
6192
6206
  clearGeneratedCandidatesDescription:
@@ -6534,7 +6548,9 @@ export const messagesByLocale = {
6534
6548
  checkingImageLibraries: "Verificando bibliotecas de imagens...",
6535
6549
  checkingUpload: "Verificando upload",
6536
6550
  clearAll: "Limpar tudo",
6551
+ clearAllWithCount: "Limpar todos os candidatos ({{count}})",
6537
6552
  clearCandidates: "Limpar candidatos",
6553
+ hideDetails: "Ocultar detalhes",
6538
6554
  clearCandidatesDescription:
6539
6555
  "Limpa o estágio ao vivo e exclui candidatos de rascunho não salvos.",
6540
6556
  clearGeneratedCandidatesDescription:
@@ -6985,7 +7001,9 @@ export const messagesByLocale = {
6985
7001
  checkingImageLibraries: "Image libraries जांची जा रही हैं...",
6986
7002
  checkingUpload: "अपलोड की जाँच की जा रही है",
6987
7003
  clearAll: "सभी साफ करें",
7004
+ clearAllWithCount: "सभी उम्मीदवार साफ करें ({{count}})",
6988
7005
  clearCandidates: "उम्मीदवारों को साफ करें",
7006
+ hideDetails: "विवरण छिपाएं",
6989
7007
  clearCandidatesDescription:
6990
7008
  "लाइव चरण को साफ करता है और अ सहेजे गए ड्राफ्ट उम्मीदवारों को हटाता है।",
6991
7009
  clearGeneratedCandidatesDescription:
@@ -7331,7 +7349,9 @@ export const messagesByLocale = {
7331
7349
  checkingImageLibraries: "جارٍ فحص مكتبات الصور...",
7332
7350
  checkingUpload: "التحقق من التحميل",
7333
7351
  clearAll: "مسح الكل",
7352
+ clearAllWithCount: "مسح كل المرشحين ({{count}})",
7334
7353
  clearCandidates: "امسح المرشحين",
7354
+ hideDetails: "إخفاء التفاصيل",
7335
7355
  clearCandidatesDescription:
7336
7356
  "مسح مرحلة المرشح وحذف المرشحين الذين تم إنشاؤهم غير المحفوظة. أصول المكتبة المحفوظة تبقى دون تغيير.",
7337
7357
  clearGeneratedCandidatesDescription:
@@ -4533,7 +4533,9 @@ function LiveCandidatesActions({
4533
4533
  const hasFailed = failedCount > 0;
4534
4534
  const isClearing = dismissSlots.isPending || deleteAssets.isPending;
4535
4535
  const actionLabel =
4536
- pending === "failed" ? t("library.dismissFailed") : t("library.clearAll");
4536
+ pending === "failed"
4537
+ ? t("library.dismissFailed")
4538
+ : t("library.clearAllWithCount", { count: totalCount });
4537
4539
  const busyLabel =
4538
4540
  pending === "failed" ? t("library.dismissing") : t("library.clearing");
4539
4541
 
@@ -4645,26 +4647,28 @@ function LiveCandidatesActions({
4645
4647
  </Button>
4646
4648
  </DropdownMenuTrigger>
4647
4649
  <DropdownMenuContent align="end">
4648
- <DropdownMenuItem
4649
- disabled={!hasFailed || isClearing}
4650
- onSelect={(event) => {
4651
- event.preventDefault();
4652
- setPending("failed");
4653
- }}
4654
- >
4655
- <IconTrash className="mr-2 h-4 w-4 shrink-0" />
4656
- {t("library.dismissFailedWithCount", { count: failedCount })}
4657
- </DropdownMenuItem>
4650
+ {hasFailed ? (
4651
+ <DropdownMenuItem
4652
+ disabled={isClearing}
4653
+ onSelect={(event) => {
4654
+ event.preventDefault();
4655
+ setPending("failed");
4656
+ }}
4657
+ >
4658
+ <IconTrash className="mr-2 h-4 w-4 shrink-0" />
4659
+ {t("library.dismissFailedWithCount", { count: failedCount })}
4660
+ </DropdownMenuItem>
4661
+ ) : null}
4658
4662
  <DropdownMenuItem
4659
4663
  className="text-destructive focus:bg-destructive/10 focus:text-destructive"
4660
- disabled={isClearing}
4664
+ disabled={isClearing || totalCount === 0}
4661
4665
  onSelect={(event) => {
4662
4666
  event.preventDefault();
4663
4667
  setPending("all");
4664
4668
  }}
4665
4669
  >
4666
4670
  <IconTrash className="mr-2 h-4 w-4 shrink-0" />
4667
- {t("library.clearAll")}
4671
+ {t("library.clearAllWithCount", { count: totalCount })}
4668
4672
  </DropdownMenuItem>
4669
4673
  </DropdownMenuContent>
4670
4674
  </DropdownMenu>
@@ -72,6 +72,13 @@ export interface ShareRecordingPopoverProps {
72
72
  animatedThumbnailUrl?: string | null;
73
73
  isLoomRecording?: boolean;
74
74
  hasPassword?: boolean;
75
+ /**
76
+ * Restricts the dialog to a bare copy-link control for viewers who can
77
+ * reshare a public/org clip's link but have no edit access: it skips
78
+ * `list-resource-shares` (which returns every individually-shared
79
+ * principal's email to any reader) and hides the Invite tab entirely.
80
+ */
81
+ viewerReshareOnly?: boolean;
75
82
  /** Trigger element rendered as the popover anchor (usually the Share button). */
76
83
  children: ReactNode;
77
84
  open?: boolean;
@@ -103,6 +110,7 @@ export function ShareRecordingPopover({
103
110
  animatedThumbnailUrl,
104
111
  isLoomRecording = false,
105
112
  hasPassword,
113
+ viewerReshareOnly = false,
106
114
  children,
107
115
  open,
108
116
  onOpenChange,
@@ -125,6 +133,7 @@ export function ShareRecordingPopover({
125
133
  animatedThumbnailUrl={animatedThumbnailUrl}
126
134
  isLoomRecording={isLoomRecording}
127
135
  hasPassword={hasPassword}
136
+ viewerReshareOnly={viewerReshareOnly}
128
137
  />
129
138
  </PopoverContent>
130
139
  </Popover>
@@ -148,6 +157,7 @@ export function ShareRecordingDialog({
148
157
  open,
149
158
  onOpenChange,
150
159
  hasPassword,
160
+ viewerReshareOnly = false,
151
161
  }: ShareRecordingDialogProps) {
152
162
  const t = useT();
153
163
  return (
@@ -168,6 +178,7 @@ export function ShareRecordingDialog({
168
178
  animatedThumbnailUrl={animatedThumbnailUrl}
169
179
  isLoomRecording={isLoomRecording}
170
180
  hasPassword={hasPassword}
181
+ viewerReshareOnly={viewerReshareOnly}
171
182
  reserveCloseButton
172
183
  />
173
184
  </DialogContent>
@@ -186,6 +197,7 @@ function ShareRecordingContent({
186
197
  isLoomRecording = false,
187
198
  reserveCloseButton = false,
188
199
  hasPassword,
200
+ viewerReshareOnly = false,
189
201
  }: {
190
202
  recordingId: string;
191
203
  recordingTitle?: string;
@@ -197,20 +209,34 @@ function ShareRecordingContent({
197
209
  isLoomRecording?: boolean;
198
210
  reserveCloseButton?: boolean;
199
211
  hasPassword?: boolean;
212
+ viewerReshareOnly?: boolean;
200
213
  }) {
201
214
  const t = useT();
202
- const sharesQuery = useActionQuery<SharesResponse>("list-resource-shares", {
203
- resourceType: "recording",
204
- resourceId: recordingId,
205
- });
215
+ const sharesQuery = useActionQuery<SharesResponse>(
216
+ "list-resource-shares",
217
+ { resourceType: "recording", resourceId: recordingId },
218
+ { enabled: !viewerReshareOnly },
219
+ );
206
220
 
207
- const data = sharesQuery.data;
221
+ const data = viewerReshareOnly ? undefined : sharesQuery.data;
208
222
  const role = data?.role ?? initialRole;
209
223
  const canManage = role === "owner" || role === "admin";
224
+ // Editors could always see (read-only) who a clip is shared with; only
225
+ // gate the Invite tab's mutation controls behind canManage. Commenters are
226
+ // grouped with plain viewers here -- neither can manage shares.
227
+ const canViewShares =
228
+ role === "owner" || role === "admin" || role === "editor";
210
229
  const visibility =
211
230
  (data?.visibility as Visibility | null | undefined) ??
212
231
  initialVisibility ??
213
232
  null;
233
+ // A plain viewer/commenter can't produce a working embed for a non-public
234
+ // clip (they have no way to make it public), so don't dangle the tab in
235
+ // front of them only to show an "ask the owner" dead end. Owner/admin/
236
+ // editor keep it regardless of visibility since they can flip to public
237
+ // from inside it.
238
+ const canEmbed = canViewShares || visibility === "public";
239
+ const tabCount = 1 + (canViewShares ? 1 : 0) + (canEmbed ? 1 : 0);
214
240
 
215
241
  // Attribution `via` must be a stable non-PII id, never an email. The only
216
242
  // owner id available client-side is the *current* session's userId, which is
@@ -234,20 +260,28 @@ function ShareRecordingContent({
234
260
  defaultValue="link"
235
261
  className={cn("min-w-0 px-4 py-3", reserveCloseButton && "pe-12")}
236
262
  >
237
- <TabsList className="grid w-full grid-cols-3">
238
- <TabsTrigger value="link" className="gap-1.5">
239
- <IconLink size={14} />
240
- {t("shareDialog.link")}
241
- </TabsTrigger>
242
- <TabsTrigger value="invite" className="gap-1.5">
243
- <IconMail size={14} />
244
- {t("shareDialog.invite")}
245
- </TabsTrigger>
246
- <TabsTrigger value="embed" className="gap-1.5">
247
- <IconCode size={14} />
248
- {t("shareDialog.embed")}
249
- </TabsTrigger>
250
- </TabsList>
263
+ {tabCount > 1 ? (
264
+ <TabsList
265
+ className={`grid w-full ${tabCount === 3 ? "grid-cols-3" : "grid-cols-2"}`}
266
+ >
267
+ <TabsTrigger value="link" className="gap-1.5">
268
+ <IconLink size={14} />
269
+ {t("shareDialog.link")}
270
+ </TabsTrigger>
271
+ {canViewShares ? (
272
+ <TabsTrigger value="invite" className="gap-1.5">
273
+ <IconMail size={14} />
274
+ {t("shareDialog.invite")}
275
+ </TabsTrigger>
276
+ ) : null}
277
+ {canEmbed ? (
278
+ <TabsTrigger value="embed" className="gap-1.5">
279
+ <IconCode size={14} />
280
+ {t("shareDialog.embed")}
281
+ </TabsTrigger>
282
+ ) : null}
283
+ </TabsList>
284
+ ) : null}
251
285
 
252
286
  <TabsContent value="link" className="mt-3">
253
287
  <LinkTab
@@ -262,34 +296,39 @@ function ShareRecordingContent({
262
296
  animatedThumbnailUrl={animatedThumbnailUrl}
263
297
  isLoomRecording={isLoomRecording}
264
298
  hasPassword={hasPassword}
299
+ canViewShares={canViewShares}
265
300
  />
266
301
  </TabsContent>
267
302
 
268
- <TabsContent value="invite" className="mt-3">
269
- <SharePeopleTab
270
- resourceType="recording"
271
- resourceId={recordingId}
272
- resourceUrl={absoluteAppUrl(`/r/${recordingId}`)}
273
- sharesQuery={sharesQuery}
274
- canManage={canManage}
275
- roleCopy={{
276
- commenter: {
277
- label: t("shareUi.recordingCommenter.label"),
278
- description: t("shareUi.recordingCommenter.description"),
279
- },
280
- }}
281
- />
282
- </TabsContent>
283
-
284
- <TabsContent value="embed" className="mt-3">
285
- <ClipsEmbedConfigurator
286
- recordingId={recordingId}
287
- sharesQuery={sharesQuery}
288
- visibility={visibility}
289
- canManage={canManage}
290
- ownerViaId={ownerViaId}
291
- />
292
- </TabsContent>
303
+ {canViewShares ? (
304
+ <TabsContent value="invite" className="mt-3">
305
+ <SharePeopleTab
306
+ resourceType="recording"
307
+ resourceId={recordingId}
308
+ resourceUrl={absoluteAppUrl(`/r/${recordingId}`)}
309
+ sharesQuery={sharesQuery}
310
+ canManage={canManage}
311
+ roleCopy={{
312
+ commenter: {
313
+ label: t("shareUi.recordingCommenter.label"),
314
+ description: t("shareUi.recordingCommenter.description"),
315
+ },
316
+ }}
317
+ />
318
+ </TabsContent>
319
+ ) : null}
320
+
321
+ {canEmbed ? (
322
+ <TabsContent value="embed" className="mt-3">
323
+ <ClipsEmbedConfigurator
324
+ recordingId={recordingId}
325
+ sharesQuery={sharesQuery}
326
+ visibility={visibility}
327
+ canManage={canManage}
328
+ ownerViaId={ownerViaId}
329
+ />
330
+ </TabsContent>
331
+ ) : null}
293
332
  </Tabs>
294
333
  </>
295
334
  );
@@ -311,6 +350,7 @@ function LinkTab({
311
350
  animatedThumbnailUrl,
312
351
  isLoomRecording: isLoomRecordingProp,
313
352
  hasPassword,
353
+ canViewShares,
314
354
  }: {
315
355
  recordingId: string;
316
356
  recordingTitle?: string;
@@ -323,6 +363,7 @@ function LinkTab({
323
363
  animatedThumbnailUrl?: string | null;
324
364
  isLoomRecording?: boolean;
325
365
  hasPassword?: boolean;
366
+ canViewShares: boolean;
326
367
  }) {
327
368
  const t = useT();
328
369
  const { setResourceVisibility, isPending } = useResourceVisibilityMutation(
@@ -460,21 +501,23 @@ function LinkTab({
460
501
 
461
502
  return (
462
503
  <div className="space-y-4">
463
- {visibility ? (
464
- <GeneralAccessSelect
465
- visibility={visibility}
466
- canManage={canManage}
467
- isPending={visibilityPending}
468
- onChange={(next) => setResourceVisibility(next)}
469
- publicDescription={t("shareDialog.publicDescription")}
470
- showDescription={false}
471
- />
472
- ) : (
473
- <div className="space-y-2" aria-hidden>
474
- <div className="h-3 w-24 animate-pulse rounded bg-muted" />
475
- <div className="h-12 w-full animate-pulse rounded bg-muted" />
476
- </div>
477
- )}
504
+ {canViewShares ? (
505
+ visibility ? (
506
+ <GeneralAccessSelect
507
+ visibility={visibility}
508
+ canManage={canManage}
509
+ isPending={visibilityPending}
510
+ onChange={(next) => setResourceVisibility(next)}
511
+ publicDescription={t("shareDialog.publicDescription")}
512
+ showDescription={false}
513
+ />
514
+ ) : (
515
+ <div className="space-y-2" aria-hidden>
516
+ <div className="h-3 w-24 animate-pulse rounded bg-muted" />
517
+ <div className="h-12 w-full animate-pulse rounded bg-muted" />
518
+ </div>
519
+ )
520
+ ) : null}
478
521
 
479
522
  <CopyField
480
523
  label={
@@ -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,6 +1000,7 @@ 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
1005
  <Button className="shrink-0 gap-1.5" size="sm">
993
1006
  {recording.visibility !== "public" ? (
@@ -1550,11 +1563,12 @@ export default function RecordingPage() {
1550
1563
  recordingTitle={recording.title}
1551
1564
  initialVisibility={recording.visibility}
1552
1565
  initialRole={role}
1553
- videoUrl={recording.videoUrl}
1566
+ videoUrl={shareVideoUrl}
1554
1567
  thumbnailUrl={recording.thumbnailUrl}
1555
1568
  animatedThumbnailUrl={recording.animatedThumbnailUrl}
1556
1569
  isLoomRecording={isLoomEmbedBacked}
1557
1570
  hasPassword={Boolean(recording.hasPassword)}
1571
+ viewerReshareOnly={viewerReshareOnly}
1558
1572
  >
1559
1573
  <Button
1560
1574
  className="shrink-0 gap-1.5 bg-primary text-primary-foreground hover:bg-primary/90"
@@ -537,6 +537,15 @@ export default function ShareRoute() {
537
537
  viewerRole === "editor" ||
538
538
  viewerRole === "commenter";
539
539
  const viewerIsOwner = Boolean(dataQ.data?.data?.viewer?.isOwner);
540
+ const canReshareLink =
541
+ (viewerRole === "viewer" || viewerRole === "commenter") &&
542
+ (recording?.visibility === "public" || recording?.visibility === "org");
543
+ // A plain viewer only gets a copy-link control: it must not trigger
544
+ // `list-resource-shares` (any read access is enough to call it, and its
545
+ // response includes every individually-shared principal's email) and must
546
+ // not surface the raw video download/open action independent of
547
+ // `enableDownloads`.
548
+ const viewerReshareOnly = canReshareLink && !viewerCanEdit;
540
549
  const viewerCanOpenDashboard = Boolean(
541
550
  dataQ.data?.data?.viewer?.canOpenDashboard,
542
551
  );
@@ -899,6 +908,10 @@ export default function ShareRoute() {
899
908
  const canDownloadRecording = Boolean(
900
909
  recording.enableDownloads && recording.videoUrl && !isLoomEmbedBacked,
901
910
  );
911
+ // Loom-backed clips only ever get an "open player" link (not a raw
912
+ // download), so they're exempt from the enableDownloads gate here.
913
+ const shareVideoUrl =
914
+ canDownloadRecording || isLoomEmbedBacked ? recording.videoUrl : null;
902
915
 
903
916
  return (
904
917
  <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">
@@ -995,17 +1008,18 @@ export default function ShareRoute() {
995
1008
  onDeleted={() => navigate("/library", { replace: true })}
996
1009
  />
997
1010
  ) : null}
998
- {viewerCanEdit ? (
1011
+ {viewerCanEdit || canReshareLink ? (
999
1012
  <ShareRecordingPopover
1000
1013
  recordingId={recording.id}
1001
1014
  recordingTitle={recording.title}
1002
1015
  initialVisibility={recording.visibility}
1003
1016
  initialRole={viewerIsOwner ? "owner" : undefined}
1004
- videoUrl={recording.videoUrl}
1017
+ videoUrl={shareVideoUrl}
1005
1018
  thumbnailUrl={recording.thumbnailUrl}
1006
1019
  animatedThumbnailUrl={recording.animatedThumbnailUrl}
1007
1020
  isLoomRecording={isLoomEmbedBacked}
1008
1021
  hasPassword={Boolean(recording.hasPassword)}
1022
+ viewerReshareOnly={viewerReshareOnly}
1009
1023
  >
1010
1024
  <Button size="sm" className="shrink-0 gap-1.5">
1011
1025
  {recording.visibility !== "public" ? (