@agent-native/core 0.157.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 (111) 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/global.css +4 -0
  5. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1 -2
  6. package/corpus/templates/brain/app/routes/sources.tsx +1 -2
  7. package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
  8. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
  9. package/corpus/templates/calendar/server/handlers/bookings.ts +219 -93
  10. package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
  11. package/corpus/templates/clips/actions/create-folder.ts +7 -1
  12. package/corpus/templates/clips/actions/create-meeting.ts +6 -2
  13. package/corpus/templates/clips/actions/create-recording.ts +5 -3
  14. package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
  15. package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
  16. package/corpus/templates/clips/actions/request-recording-access.ts +272 -0
  17. package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
  18. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
  19. package/corpus/templates/clips/app/components/player/share-dialog.tsx +75 -75
  20. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
  21. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
  22. package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
  23. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
  24. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -16
  25. package/corpus/templates/clips/app/routes/share.$shareId.tsx +135 -18
  26. package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
  28. package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
  29. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
  30. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
  31. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
  32. package/corpus/templates/clips/desktop/src/app.tsx +24 -0
  33. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
  34. package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
  35. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
  36. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  37. package/corpus/templates/clips/desktop/src/styles.css +24 -9
  38. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
  39. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
  40. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
  41. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
  42. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
  43. package/corpus/templates/clips/server/db/index.ts +1 -0
  44. package/corpus/templates/clips/server/db/schema.ts +1 -0
  45. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
  46. package/corpus/templates/clips/server/lib/emails.ts +23 -0
  47. package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
  48. package/corpus/templates/clips/server/lib/recordings.ts +2 -1
  49. package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
  50. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
  51. package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
  52. package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
  53. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
  54. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
  55. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
  56. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +19 -0
  58. package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
  59. package/corpus/templates/clips/shared/recording-link.ts +4 -0
  60. package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
  61. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
  62. package/corpus/templates/factory/AGENTS.md +5 -1
  63. package/corpus/templates/factory/README.md +6 -2
  64. package/corpus/templates/factory/actions/view-screen.ts +15 -0
  65. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
  66. package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
  67. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  68. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
  69. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
  70. package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
  71. package/corpus/templates/factory/app/routes/agents.tsx +9 -0
  72. package/corpus/templates/mail/server/plugins/auth.ts +0 -2
  73. package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
  74. package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
  75. package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
  76. package/corpus/templates/slides/app/context/DeckContext.tsx +71 -22
  77. package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
  78. package/corpus/templates/slides/app/pages/DeckEditor.tsx +13 -0
  79. package/dist/client/AgentPanel.d.ts +13 -1
  80. package/dist/client/AgentPanel.js +12 -10
  81. package/dist/client/AssistantChat.js +25 -5
  82. package/dist/client/HighlightedCodeBlock.js +12 -2
  83. package/dist/client/agent-chat/index.d.ts +1 -0
  84. package/dist/client/agent-chat/index.js +1 -0
  85. package/dist/client/chat/markdown-renderer.js +16 -3
  86. package/dist/client/chat-first/apps-rail.js +1 -1
  87. package/dist/client/components/ui/message-scroller.js +1 -1
  88. package/dist/client/settings/AccountSettingsCard.js +48 -19
  89. package/dist/client/sharing/ShareButton.d.ts +1 -2
  90. package/dist/client/sharing/ShareButton.js +5 -41
  91. package/dist/client/sharing/ShareDialog.js +5 -4
  92. package/dist/client/use-chat-threads.js +10 -14
  93. package/dist/collab/struct-routes.d.ts +1 -1
  94. package/dist/localization/default-messages.d.ts +1 -0
  95. package/dist/localization/default-messages.js +1 -0
  96. package/dist/progress/routes.d.ts +1 -1
  97. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  98. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  99. package/dist/server/auth-marketing.d.ts +1 -0
  100. package/dist/server/auth-marketing.js +0 -2
  101. package/dist/server/auth.d.ts +1 -0
  102. package/dist/server/auth.js +18 -0
  103. package/dist/server/onboarding-html.d.ts +1 -0
  104. package/dist/server/onboarding-html.js +2 -88
  105. package/dist/server/realtime-token.d.ts +1 -1
  106. package/dist/shared/markdown-block-split.d.ts +16 -0
  107. package/dist/shared/markdown-block-split.js +81 -5
  108. package/dist/styles/agent-native.css +13 -0
  109. package/dist/voice/voice-cleanup-prompt.js +1 -0
  110. package/package.json +2 -2
  111. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +0 -350
@@ -144,7 +144,7 @@ export default defineAction({
144
144
  description:
145
145
  "Import a public Loom share URL, or a direct link to a video file, into Clips as a playable recording. Loom links create the recording immediately and download/reupload Loom's public MP4 plus import Loom's public transcript in the background, since Loom's CDN plus a reupload can take longer than a single request should block on. Other direct video links (e.g. an MP4/WebM/MOV hosted by another screen recorder) are downloaded and reuploaded synchronously without transcript metadata — use request-transcript afterward. If storage is not connected, creates a waiting recording that can be retried after storage setup.",
146
146
  schema: ImportLoomRecordingSchema,
147
- run: async (args) => {
147
+ run: async (args, actionContext) => {
148
148
  const loomId = extractLoomVideoId(args.url);
149
149
  const isLoom = Boolean(loomId);
150
150
  const loomShareUrl = isLoom ? normalizeLoomShareUrl(args.url) : null;
@@ -214,8 +214,10 @@ export default defineAction({
214
214
  const { organizationId } = await requireOrganizationAccess(
215
215
  existingRecording?.organizationId ?? args.organizationId,
216
216
  );
217
- const defaultVisibility =
218
- await getDefaultRecordingVisibility(organizationId);
217
+ const defaultVisibility = await getDefaultRecordingVisibility(
218
+ organizationId,
219
+ actionContext?.userEmail ?? ownerEmail,
220
+ );
219
221
 
220
222
  const now = new Date().toISOString();
221
223
  const id = existingRecording?.id ?? nanoid();
@@ -0,0 +1,272 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { notify } from "@agent-native/core/notifications";
3
+ import {
4
+ emailStrong,
5
+ getAppProductionUrl,
6
+ isEmailConfigured,
7
+ renderEmail,
8
+ sendEmail,
9
+ verifyScopedAgentAccessToken,
10
+ withConfiguredAppBasePath,
11
+ } from "@agent-native/core/server";
12
+ import {
13
+ getRequestUserEmail,
14
+ getRequestOrgId,
15
+ getRequestUserName,
16
+ } from "@agent-native/core/server/request-context";
17
+ import { resolveAccess } from "@agent-native/core/sharing";
18
+ import { and, eq } from "drizzle-orm";
19
+ import { z } from "zod";
20
+
21
+ import { getDb, schema } from "../server/db/index.js";
22
+ import { nanoid, normalizeOwnerEmail } from "../server/lib/recordings.js";
23
+ import {
24
+ CLIPS_ACCESS_REQUEST_TOKEN_PREFIX,
25
+ recordingSharePath,
26
+ } from "../shared/recording-link.js";
27
+
28
+ export const CLIPS_ACCESS_REQUEST_EMAIL_ID = "clips.access-request";
29
+
30
+ function httpError(message: string, statusCode: number): Error {
31
+ return Object.assign(new Error(message), { statusCode });
32
+ }
33
+
34
+ function displayNameForEmail(email: string): string {
35
+ const local = email.replace(/@.*/, "");
36
+ const parts = local
37
+ .split(/[._+-]+/)
38
+ .map((part) => part.trim())
39
+ .filter(Boolean);
40
+ if (parts.length === 0) return email;
41
+ return parts
42
+ .slice(0, 2)
43
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
44
+ .join(" ");
45
+ }
46
+
47
+ function absoluteRecordingUrl(recordingId: string): string {
48
+ return `${withConfiguredAppBasePath(getAppProductionUrl()).replace(/\/+$/, "")}${recordingSharePath(recordingId)}`;
49
+ }
50
+
51
+ export function renderRecordingAccessRequestEmail(input: {
52
+ requesterName: string;
53
+ requesterEmail: string;
54
+ recordingTitle: string;
55
+ url: string;
56
+ }) {
57
+ const subject = `${input.requesterName} requested access to "${input.recordingTitle}"`;
58
+ return {
59
+ subject,
60
+ ...renderEmail({
61
+ brandName: "Clips",
62
+ preheader: subject,
63
+ heading: "Access request",
64
+ paragraphs: [
65
+ `${emailStrong(input.requesterName)} (${emailStrong(input.requesterEmail)}) requested access to ${emailStrong(input.recordingTitle)}.`,
66
+ "Open the Clip and use Share to grant access if this request should be approved.",
67
+ ],
68
+ cta: { label: "Open Clip", url: input.url },
69
+ footer: "You received this because you own this Clip.",
70
+ }),
71
+ };
72
+ }
73
+
74
+ async function notifyOwner(input: {
75
+ recordingId: string;
76
+ recordingTitle: string;
77
+ ownerEmail: string | null;
78
+ requesterEmail: string;
79
+ requesterName: string;
80
+ }): Promise<boolean> {
81
+ if (!input.ownerEmail || input.ownerEmail === input.requesterEmail) {
82
+ return false;
83
+ }
84
+ if (!(await isEmailConfigured())) return false;
85
+
86
+ await sendEmail({
87
+ ...renderRecordingAccessRequestEmail({
88
+ requesterName: input.requesterName,
89
+ requesterEmail: input.requesterEmail,
90
+ recordingTitle: input.recordingTitle,
91
+ url: absoluteRecordingUrl(input.recordingId),
92
+ }),
93
+ to: input.ownerEmail,
94
+ replyTo: input.requesterEmail,
95
+ templateId: CLIPS_ACCESS_REQUEST_EMAIL_ID,
96
+ });
97
+ return true;
98
+ }
99
+
100
+ export default defineAction({
101
+ description:
102
+ "Request access to a private Clips recording. Records the request and notifies the owner in-app and by email when configured.",
103
+ schema: z.object({
104
+ recordingId: z
105
+ .string()
106
+ .min(1)
107
+ .describe("Recording ID to request access to."),
108
+ accessRequestToken: z
109
+ .string()
110
+ .trim()
111
+ .min(1)
112
+ .optional()
113
+ .describe("Short-lived capability from the private share page."),
114
+ }),
115
+ agentTool: false,
116
+ run: async ({ recordingId, accessRequestToken }) => {
117
+ const requesterEmail = getRequestUserEmail();
118
+ if (!requesterEmail) {
119
+ throw httpError("Sign in to request access to this clip.", 401);
120
+ }
121
+
122
+ const normalizedRequesterEmail = normalizeOwnerEmail(requesterEmail);
123
+ const access = await resolveAccess("recording", recordingId, {
124
+ userEmail: normalizedRequesterEmail,
125
+ orgId: getRequestOrgId() ?? undefined,
126
+ });
127
+
128
+ if (access) {
129
+ const recording = access.resource as {
130
+ trashedAt?: string | null;
131
+ };
132
+ if (recording.trashedAt) {
133
+ throw httpError(`Recording ${recordingId} not found`, 404);
134
+ }
135
+ return {
136
+ ok: true as const,
137
+ alreadyHasAccess: true,
138
+ alreadyRequested: false,
139
+ notifiedOwner: false,
140
+ message: "You already have access to this clip.",
141
+ };
142
+ }
143
+
144
+ if (
145
+ !accessRequestToken ||
146
+ !verifyScopedAgentAccessToken(accessRequestToken, {
147
+ resourceKind: CLIPS_ACCESS_REQUEST_TOKEN_PREFIX,
148
+ resourceId: recordingId,
149
+ }).ok
150
+ ) {
151
+ throw httpError(`Recording ${recordingId} not found`, 404);
152
+ }
153
+
154
+ const db = getDb();
155
+ const [recording] = await db
156
+ .select({
157
+ id: schema.recordings.id,
158
+ title: schema.recordings.title,
159
+ ownerEmail: schema.recordings.ownerEmail,
160
+ visibility: schema.recordings.visibility,
161
+ trashedAt: schema.recordings.trashedAt,
162
+ })
163
+ .from(schema.recordings)
164
+ .where(eq(schema.recordings.id, recordingId))
165
+ .limit(1);
166
+
167
+ if (
168
+ !recording ||
169
+ recording.trashedAt ||
170
+ recording.visibility === "public"
171
+ ) {
172
+ throw httpError(`Recording ${recordingId} not found`, 404);
173
+ }
174
+
175
+ const previousRequests = await db
176
+ .select({ payload: schema.recordingEvents.payload })
177
+ .from(schema.recordingEvents)
178
+ .where(
179
+ and(
180
+ eq(schema.recordingEvents.recordingId, recordingId),
181
+ eq(schema.recordingEvents.kind, "access-request"),
182
+ ),
183
+ );
184
+ const alreadyRequested = previousRequests.some((event) => {
185
+ try {
186
+ const payload = JSON.parse(event.payload) as {
187
+ requesterEmail?: string;
188
+ };
189
+ return (
190
+ typeof payload.requesterEmail === "string" &&
191
+ normalizeOwnerEmail(payload.requesterEmail) ===
192
+ normalizedRequesterEmail
193
+ );
194
+ } catch {
195
+ // coercion-ok: malformed historical event payload cannot represent a matching requester.
196
+ return false;
197
+ }
198
+ });
199
+
200
+ if (alreadyRequested) {
201
+ return {
202
+ ok: true as const,
203
+ alreadyHasAccess: false,
204
+ alreadyRequested: true,
205
+ notifiedOwner: false,
206
+ message: "Your access request is already with the clip owner.",
207
+ };
208
+ }
209
+
210
+ const requesterName =
211
+ getRequestUserName()?.trim() ||
212
+ displayNameForEmail(normalizedRequesterEmail);
213
+ const requestedAt = new Date().toISOString();
214
+ await db.insert(schema.recordingEvents).values({
215
+ id: nanoid(),
216
+ recordingId,
217
+ viewerId: null,
218
+ kind: "access-request",
219
+ timestampMs: 0,
220
+ payload: JSON.stringify({
221
+ requesterEmail: normalizedRequesterEmail,
222
+ requesterName,
223
+ requestedAt,
224
+ }),
225
+ createdAt: requestedAt,
226
+ });
227
+
228
+ const ownerEmail = recording.ownerEmail
229
+ ? normalizeOwnerEmail(recording.ownerEmail)
230
+ : null;
231
+ let notifiedOwner = false;
232
+ try {
233
+ if (ownerEmail) {
234
+ await notify(
235
+ {
236
+ severity: "info",
237
+ title: "Clip access requested",
238
+ body: `${requesterName} requested access to “${recording.title}”.`,
239
+ metadata: {
240
+ recordingId,
241
+ requesterEmail: normalizedRequesterEmail,
242
+ url: absoluteRecordingUrl(recordingId),
243
+ },
244
+ },
245
+ { owner: ownerEmail },
246
+ );
247
+ }
248
+ notifiedOwner = await notifyOwner({
249
+ recordingId,
250
+ recordingTitle: recording.title,
251
+ ownerEmail,
252
+ requesterEmail: normalizedRequesterEmail,
253
+ requesterName,
254
+ });
255
+ } catch (error) {
256
+ console.warn(
257
+ "[recording-access] access request notification failed:",
258
+ error,
259
+ );
260
+ }
261
+
262
+ return {
263
+ ok: true as const,
264
+ alreadyHasAccess: false,
265
+ alreadyRequested: false,
266
+ notifiedOwner,
267
+ message: notifiedOwner
268
+ ? "Access request sent to the clip owner."
269
+ : "Access request recorded for the clip owner.",
270
+ };
271
+ },
272
+ });
@@ -84,7 +84,7 @@ export default defineAction({
84
84
  height: z.coerce.number().int().optional(),
85
85
  folderId: z.string().nullish(),
86
86
  }),
87
- run: async (args) => {
87
+ run: async (args, actionContext) => {
88
88
  const db = getDb();
89
89
  const ownerEmail = getCurrentOwnerEmail();
90
90
  const videoUrl = args.videoUrl?.trim() || null;
@@ -134,8 +134,10 @@ export default defineAction({
134
134
  assertNativeRecordingMedia(source);
135
135
  }
136
136
  const organizationId = ordered[0].organizationId;
137
- const defaultVisibility =
138
- await getDefaultRecordingVisibility(organizationId);
137
+ const defaultVisibility = await getDefaultRecordingVisibility(
138
+ organizationId,
139
+ actionContext?.userEmail ?? ownerEmail,
140
+ );
139
141
 
140
142
  const totalDuration =
141
143
  args.durationMs ??
@@ -4,7 +4,8 @@ import {
4
4
  useActionQuery,
5
5
  } from "@agent-native/core/client/hooks";
6
6
  import { useT } from "@agent-native/core/client/i18n";
7
- import { IconChevronDown, IconLink, IconMail } from "@tabler/icons-react";
7
+ import { ShareAgentsSection } from "@agent-native/toolkit/sharing";
8
+ import { IconLink, IconMail } from "@tabler/icons-react";
8
9
  import {
9
10
  useCallback,
10
11
  useEffect,
@@ -27,11 +28,6 @@ import {
27
28
  type Visibility,
28
29
  } from "@/components/sharing/share-ui";
29
30
  import { Button } from "@/components/ui/button";
30
- import {
31
- Collapsible,
32
- CollapsibleContent,
33
- CollapsibleTrigger,
34
- } from "@/components/ui/collapsible";
35
31
  import {
36
32
  Popover,
37
33
  PopoverContent,
@@ -220,6 +216,7 @@ function LinkTab({
220
216
  sharesQuery.isLoading ||
221
217
  !sharesLoaded ||
222
218
  (!isPublic && (createAgentLink.isPending || !agentLink));
219
+ const visibleAgentLink = isPublic ? shareUrl : agentLink;
223
220
 
224
221
  useEffect(() => {
225
222
  setIncludeTranscript(shareTranscript);
@@ -301,66 +298,44 @@ function LinkTab({
301
298
  />
302
299
  ) : null}
303
300
 
304
- {!isPublic ? (
305
- <Collapsible
306
- open={agentDetailsOpen}
307
- onOpenChange={setAgentDetailsOpen}
308
- className="overflow-hidden rounded-md border border-border"
309
- >
310
- <CollapsibleTrigger asChild>
311
- <button
312
- type="button"
313
- className="flex min-h-10 w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm font-medium transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
314
- >
315
- <span className="flex min-w-0 items-center gap-2 truncate">
316
- <IconLink
317
- aria-hidden="true"
318
- className="size-3.5 shrink-0 text-muted-foreground"
319
- strokeWidth={1.8}
320
- />
321
- {t("shareDialog.shareWithAgents")}
322
- </span>
323
- <IconChevronDown
324
- aria-hidden="true"
325
- className={`size-4 shrink-0 text-muted-foreground transition-transform ${agentDetailsOpen ? "rotate-180" : ""}`}
326
- />
327
- </button>
328
- </CollapsibleTrigger>
329
- <CollapsibleContent className="clips-collapsible-content border-t border-border px-3 py-3">
330
- <div className="space-y-2">
331
- <CopyField
332
- label={t("shareDialog.shareWithAgents")}
333
- value={agentLink}
334
- disabled={agentShareDisabled}
335
- />
336
- {sharesLoaded ? (
337
- <>
301
+ <ShareAgentsSection
302
+ label={t("shareDialog.shareWithAgents")}
303
+ open={agentDetailsOpen}
304
+ onOpenChange={setAgentDetailsOpen}
305
+ contentClassName="clips-collapsible-content"
306
+ >
307
+ <div className="space-y-2">
308
+ <CopyField
309
+ label={t("shareDialog.shareLink")}
310
+ value={visibleAgentLink}
311
+ disabled={agentShareDisabled}
312
+ />
313
+ {sharesLoaded ? (
314
+ <>
315
+ <p className="text-xs text-muted-foreground">
316
+ {t("shareMeeting.agentLinkDescription")}
317
+ </p>
318
+ {agentLinkError ? (
319
+ <div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
338
320
  <p className="text-xs text-muted-foreground">
339
- {t("shareMeeting.agentLinkDescription")}
321
+ {t("shareDialog.agentLinkUnavailable")}
340
322
  </p>
341
- {agentLinkError ? (
342
- <div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
343
- <p className="text-xs text-muted-foreground">
344
- {t("shareDialog.agentLinkUnavailable")}
345
- </p>
346
- <Button
347
- type="button"
348
- variant="ghost"
349
- size="sm"
350
- className="h-7"
351
- onClick={() => void loadAgentLink()}
352
- disabled={createAgentLink.isPending}
353
- >
354
- {t("shareDialog.retryAgentLink")}
355
- </Button>
356
- </div>
357
- ) : null}
358
- </>
323
+ <Button
324
+ type="button"
325
+ variant="ghost"
326
+ size="sm"
327
+ className="h-7"
328
+ onClick={() => void loadAgentLink()}
329
+ disabled={createAgentLink.isPending}
330
+ >
331
+ {t("shareDialog.retryAgentLink")}
332
+ </Button>
333
+ </div>
359
334
  ) : null}
360
- </div>
361
- </CollapsibleContent>
362
- </Collapsible>
363
- ) : null}
335
+ </>
336
+ ) : null}
337
+ </div>
338
+ </ShareAgentsSection>
364
339
  </div>
365
340
  );
366
341
  }
@@ -6,8 +6,8 @@ import {
6
6
  useSession,
7
7
  } from "@agent-native/core/client/hooks";
8
8
  import { useT } from "@agent-native/core/client/i18n";
9
+ import { ShareAgentsSection } from "@agent-native/toolkit/sharing";
9
10
  import {
10
- IconChevronDown,
11
11
  IconCode,
12
12
  IconExternalLink,
13
13
  IconLink,
@@ -35,11 +35,6 @@ import {
35
35
  type Visibility,
36
36
  } from "@/components/sharing/share-ui";
37
37
  import { Button } from "@/components/ui/button";
38
- import {
39
- Collapsible,
40
- CollapsibleContent,
41
- CollapsibleTrigger,
42
- } from "@/components/ui/collapsible";
43
38
  import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
44
39
  import { Input } from "@/components/ui/input";
45
40
  import { Label } from "@/components/ui/label";
@@ -52,6 +47,7 @@ import { Switch } from "@/components/ui/switch";
52
47
  import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
53
48
  import { cn } from "@/lib/utils";
54
49
 
50
+ import { buildAgentApiUrls } from "../../../shared/agent-context";
55
51
  import { buildEmailPreviewMarkup } from "../../../shared/email-preview";
56
52
  import { isLoomEmbedUrl } from "../../../shared/loom";
57
53
  import { withShareAttribution } from "../../../shared/share-attribution";
@@ -62,6 +58,14 @@ function absoluteAppUrl(path: string): string {
62
58
  return new URL(appPath(path), window.location.origin).toString();
63
59
  }
64
60
 
61
+ function absolutePublicAgentContextUrl(recordingId: string): string {
62
+ if (typeof window === "undefined") return "";
63
+ return buildAgentApiUrls(recordingId, {
64
+ origin: window.location.origin,
65
+ basePath: appPath("/").replace(/\/$/, ""),
66
+ }).contextUrl;
67
+ }
68
+
65
69
  export interface ShareRecordingPopoverProps {
66
70
  recordingId: string;
67
71
  recordingTitle?: string;
@@ -375,6 +379,14 @@ function LinkTab({
375
379
  const sharesLoaded = visibility !== null;
376
380
  const visibilityPending = isPending || sharesQuery.isLoading;
377
381
  const isLoomRecording = isLoomRecordingProp || isLoomEmbedUrl(videoUrl);
382
+ const needsScopedAgentContext = !isPublic || hasPassword !== false;
383
+ const publicAgentContextUrl = useMemo(
384
+ () =>
385
+ isPublic && hasPassword === false
386
+ ? absolutePublicAgentContextUrl(recordingId)
387
+ : "",
388
+ [hasPassword, isPublic, recordingId],
389
+ );
378
390
  const emailPreviewThumbnailUrl = useMemo(() => {
379
391
  if (!isPublic || hasPassword !== false || !sharesLoaded) return null;
380
392
  const variant = preferredThumbnailVariant({
@@ -438,10 +450,10 @@ function LinkTab({
438
450
  try {
439
451
  const result = (await createAgentLinkAsyncRef.current({
440
452
  recordingId,
441
- })) as { url?: string };
453
+ })) as { contextUrl?: string };
442
454
  if (agentLinkRequestIdRef.current !== requestId) return;
443
- if (result?.url) {
444
- setAgentContextUrl(result.url);
455
+ if (result?.contextUrl) {
456
+ setAgentContextUrl(result.contextUrl);
445
457
  } else {
446
458
  setAgentLinkError(true);
447
459
  }
@@ -457,20 +469,30 @@ function LinkTab({
457
469
  setAgentLinkError(false);
458
470
  if (!sharesLoaded) return;
459
471
 
460
- if (!isPublic) {
472
+ if (needsScopedAgentContext) {
461
473
  void loadAgentContextUrl();
462
474
  }
463
475
 
464
476
  return () => {
465
477
  agentLinkRequestIdRef.current += 1;
466
478
  };
467
- }, [isPublic, loadAgentContextUrl, recordingId, sharesLoaded, visibility]);
479
+ }, [
480
+ loadAgentContextUrl,
481
+ needsScopedAgentContext,
482
+ recordingId,
483
+ sharesLoaded,
484
+ visibility,
485
+ ]);
468
486
 
469
- const agentLink = isPublic ? shareUrl : agentContextUrl;
487
+ const agentLink = isPublic
488
+ ? publicAgentContextUrl || agentContextUrl
489
+ : agentContextUrl;
470
490
  const agentShareDisabled =
471
491
  visibilityPending ||
472
492
  !sharesLoaded ||
473
- (!isPublic && (isPending || createAgentLink.isPending || !agentContextUrl));
493
+ !agentLink ||
494
+ (needsScopedAgentContext &&
495
+ (isPending || createAgentLink.isPending || !agentContextUrl));
474
496
  const agentPrompt = agentLink
475
497
  ? t("shareDialog.agentPrompt", { agentContextUrl: agentLink })
476
498
  : "";
@@ -529,71 +551,49 @@ function LinkTab({
529
551
  disabled={visibilityPending || !sharesLoaded}
530
552
  />
531
553
 
532
- {!isPublic ? (
533
- <Collapsible
534
- open={agentDetailsOpen}
535
- onOpenChange={setAgentDetailsOpen}
536
- className="overflow-hidden rounded-md border border-border"
537
- >
538
- <CollapsibleTrigger asChild>
539
- <button
540
- type="button"
541
- className="flex min-h-10 w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm font-medium transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
542
- >
543
- <span className="flex min-w-0 items-center gap-2 truncate">
544
- <IconLink
545
- aria-hidden="true"
546
- className="size-3.5 shrink-0 text-muted-foreground"
547
- strokeWidth={1.8}
548
- />
549
- {t("shareDialog.shareWithAgents")}
550
- </span>
551
- <IconChevronDown
552
- aria-hidden="true"
553
- className={`size-4 shrink-0 text-muted-foreground transition-transform ${agentDetailsOpen ? "rotate-180" : ""}`}
554
- />
555
- </button>
556
- </CollapsibleTrigger>
557
- <CollapsibleContent className="clips-collapsible-content border-t border-border px-3 py-3">
558
- <div className="space-y-2">
559
- <CopyField
560
- label={t("shareDialog.shareWithAgents")}
561
- value={agentLink}
562
- disabled={agentShareDisabled}
563
- />
564
- {sharesLoaded ? (
565
- <>
554
+ <ShareAgentsSection
555
+ label={t("shareDialog.shareWithAgents")}
556
+ open={agentDetailsOpen}
557
+ onOpenChange={setAgentDetailsOpen}
558
+ contentClassName="clips-collapsible-content"
559
+ >
560
+ <div className="space-y-2">
561
+ <CopyField
562
+ label={t("shareDialog.shareLink")}
563
+ value={agentLink}
564
+ disabled={agentShareDisabled}
565
+ />
566
+ {sharesLoaded ? (
567
+ <>
568
+ <p className="text-xs text-muted-foreground">
569
+ {t("shareDialog.agentTokenDescription")}
570
+ </p>
571
+ {agentLinkError ? (
572
+ <div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
566
573
  <p className="text-xs text-muted-foreground">
567
- {t("shareDialog.agentTokenDescription")}
574
+ {t("shareDialog.agentLinkUnavailable")}
568
575
  </p>
569
- {agentLinkError ? (
570
- <div className="flex items-center justify-between gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">
571
- <p className="text-xs text-muted-foreground">
572
- {t("shareDialog.agentLinkUnavailable")}
573
- </p>
574
- <Button
575
- type="button"
576
- variant="ghost"
577
- size="sm"
578
- className="h-7"
579
- onClick={() => void loadAgentContextUrl()}
580
- disabled={createAgentLink.isPending}
581
- >
582
- {t("shareDialog.retryAgentLink")}
583
- </Button>
584
- </div>
585
- ) : null}
586
- <CopyField
587
- label={t("shareDialog.copyAgentPrompt")}
588
- value={agentPrompt}
589
- disabled={agentShareDisabled}
590
- />
591
- </>
576
+ <Button
577
+ type="button"
578
+ variant="ghost"
579
+ size="sm"
580
+ className="h-7"
581
+ onClick={() => void loadAgentContextUrl()}
582
+ disabled={createAgentLink.isPending}
583
+ >
584
+ {t("shareDialog.retryAgentLink")}
585
+ </Button>
586
+ </div>
592
587
  ) : null}
593
- </div>
594
- </CollapsibleContent>
595
- </Collapsible>
596
- ) : null}
588
+ <CopyField
589
+ label={t("shareDialog.copyAgentPrompt")}
590
+ value={agentPrompt}
591
+ disabled={agentShareDisabled}
592
+ />
593
+ </>
594
+ ) : null}
595
+ </div>
596
+ </ShareAgentsSection>
597
597
 
598
598
  {showMakePublic ? (
599
599
  <MakePublicCard