@agent-native/core 0.157.3 → 0.157.4

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 (152) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  3. package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +21 -0
  4. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +40 -1
  5. package/corpus/templates/assets/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  6. package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +21 -0
  7. package/corpus/templates/brain/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  8. package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +21 -0
  9. package/corpus/templates/calendar/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  10. package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +21 -0
  11. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +3 -1
  12. package/corpus/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  13. package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +21 -0
  14. package/corpus/templates/clips/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  15. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -7
  16. package/corpus/templates/clips/actions/approve-recording-access-request.ts +147 -0
  17. package/corpus/templates/clips/actions/request-recording-access.ts +121 -23
  18. package/corpus/templates/clips/actions/view-screen.ts +1 -1
  19. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +34 -53
  20. package/corpus/templates/clips/app/components/player/request-access-dialog.tsx +104 -0
  21. package/corpus/templates/clips/app/components/player/share-dialog.tsx +36 -45
  22. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +3 -6
  23. package/corpus/templates/clips/app/i18n/en-US.ts +26 -1
  24. package/corpus/templates/clips/app/lib/public-meeting.ts +1 -1
  25. package/corpus/templates/clips/app/routes/access-request.approve.tsx +250 -0
  26. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -32
  27. package/corpus/templates/clips/app/routes/share.$shareId.tsx +123 -37
  28. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +41 -1
  29. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +1 -0
  30. package/corpus/templates/clips/server/lib/emails.ts +40 -11
  31. package/corpus/templates/clips/shared/recording-link.ts +32 -1
  32. package/corpus/templates/clips/shared/share-attribution.ts +13 -0
  33. package/corpus/templates/content/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  34. package/corpus/templates/content/.agents/skills/sharing/SKILL.md +21 -0
  35. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +22 -10
  36. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +2 -1
  37. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +245 -9
  38. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +185 -134
  39. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +61 -45
  40. package/corpus/templates/content/app/components/editor/extensions/ImageNode.ts +6 -0
  41. package/corpus/templates/content/app/components/editor/image-upload.ts +137 -5
  42. package/corpus/templates/content/app/global.css +8 -0
  43. package/corpus/templates/content/changelog/2026-08-12-image-uploads-now-confirm-the-image-can-be-displayed-before-.md +6 -0
  44. package/corpus/templates/content/changelog/2026-08-14-images-fill-the-page-width-by-default-and-remain-visible-whe.md +6 -0
  45. package/corpus/templates/content/changelog/2026-08-14-images-inserted-from-the-slash-command-stay-saved-after-uplo.md +6 -0
  46. package/corpus/templates/content/changelog/2026-08-14-svg-files-can-be-selected-reliably-from-the-image-picker.md +6 -0
  47. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  48. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +21 -0
  49. package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  50. package/corpus/templates/design/.agents/skills/sharing/SKILL.md +21 -0
  51. package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  52. package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +21 -0
  53. package/corpus/templates/factory/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  54. package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +21 -0
  55. package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  56. package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +21 -0
  57. package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  58. package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +21 -0
  59. package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  60. package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +21 -0
  61. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -1
  62. package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  63. package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +21 -0
  64. package/corpus/templates/plan/app/pages/PlansPage.tsx +25 -30
  65. package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  66. package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +21 -0
  67. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +2 -1
  68. package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +124 -180
  69. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +1 -0
  70. package/corpus/templates/slides/app/global.css +99 -3
  71. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  72. package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  73. package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +21 -0
  74. package/dist/chat-threads/schema.d.ts +1 -1
  75. package/dist/cli/code-agent-executor.js +318 -0
  76. package/dist/client/AgentPanel.d.ts +13 -1
  77. package/dist/client/AgentPanel.js +41 -3
  78. package/dist/client/AssistantChat.d.ts +9 -1
  79. package/dist/client/AssistantChat.js +26 -13
  80. package/dist/client/MultiTabAssistantChat.js +18 -5
  81. package/dist/client/agent-chat.d.ts +8 -0
  82. package/dist/client/agent-chat.js +15 -0
  83. package/dist/client/app-chat-sidebar.d.ts +23 -0
  84. package/dist/client/app-chat-sidebar.js +53 -0
  85. package/dist/client/app-providers.js +31 -2
  86. package/dist/client/frame-protocol.d.ts +20 -2
  87. package/dist/client/hooks/index.d.ts +1 -0
  88. package/dist/client/hooks/index.js +1 -0
  89. package/dist/client/sharing/AgentShareSection.d.ts +8 -0
  90. package/dist/client/sharing/AgentShareSection.js +50 -0
  91. package/dist/client/sharing/ShareButton.js +2 -1
  92. package/dist/client/sharing/ShareDialog.d.ts +2 -2
  93. package/dist/client/sharing/ShareDialog.js +21 -29
  94. package/dist/client/sharing/index.d.ts +1 -0
  95. package/dist/client/sharing/index.js +1 -0
  96. package/dist/client/sharing/useShareButtonController.d.ts +1 -0
  97. package/dist/client/sharing/useShareDialogController.d.ts +3 -0
  98. package/dist/client/sharing/useShareDialogController.js +2 -1
  99. package/dist/client/theme.d.ts +21 -0
  100. package/dist/client/theme.js +60 -0
  101. package/dist/client/ui/index.d.ts +1 -1
  102. package/dist/client/ui/index.js +1 -1
  103. package/dist/client/use-session.js +23 -0
  104. package/dist/collab/awareness.d.ts +2 -2
  105. package/dist/collab/struct-routes.d.ts +1 -1
  106. package/dist/dashboard-storage/schema.d.ts +1 -1
  107. package/dist/data-programs/schema.d.ts +1 -1
  108. package/dist/extensions/schema.d.ts +1 -1
  109. package/dist/localization/core-messages/ar-SA.js +6 -0
  110. package/dist/localization/core-messages/de-DE.js +6 -0
  111. package/dist/localization/core-messages/en-US.d.ts +6 -0
  112. package/dist/localization/core-messages/en-US.js +6 -0
  113. package/dist/localization/core-messages/es-ES.js +6 -0
  114. package/dist/localization/core-messages/fr-FR.js +6 -0
  115. package/dist/localization/core-messages/hi-IN.js +6 -0
  116. package/dist/localization/core-messages/ja-JP.js +6 -0
  117. package/dist/localization/core-messages/ko-KR.js +6 -0
  118. package/dist/localization/core-messages/pt-BR.js +6 -0
  119. package/dist/localization/core-messages/zh-CN.js +6 -0
  120. package/dist/localization/core-messages/zh-TW.js +6 -0
  121. package/dist/localization/core-messages.d.ts +6 -0
  122. package/dist/localization/default-messages.d.ts +1 -0
  123. package/dist/localization/default-messages.js +1 -0
  124. package/dist/notifications/routes.d.ts +3 -3
  125. package/dist/progress/routes.d.ts +1 -1
  126. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  127. package/dist/provider-api/actions/provider-api.d.ts +14 -14
  128. package/dist/server/auth.js +8 -5
  129. package/dist/server/identity-sso-store.d.ts +6 -1
  130. package/dist/server/identity-sso-store.js +57 -14
  131. package/dist/server/identity-sso.d.ts +7 -8
  132. package/dist/server/identity-sso.js +15 -12
  133. package/dist/sharing/actions/list-resource-shares.d.ts +2 -0
  134. package/dist/sharing/actions/list-resource-shares.js +1 -0
  135. package/dist/sharing/schema.d.ts +1 -1
  136. package/dist/sharing/schema.js +4 -1
  137. package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  138. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +21 -0
  139. package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  140. package/dist/templates/default/.agents/skills/sharing/SKILL.md +21 -0
  141. package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  142. package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  143. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +21 -0
  144. package/docs/content/cross-app-sso.mdx +24 -17
  145. package/package.json +2 -2
  146. package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  147. package/src/templates/chat/.agents/skills/sharing/SKILL.md +21 -0
  148. package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  149. package/src/templates/default/.agents/skills/sharing/SKILL.md +21 -0
  150. package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  151. package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
  152. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +21 -0
@@ -0,0 +1,250 @@
1
+ import { callAction, useSession } from "@agent-native/core/client/hooks";
2
+ import { useT } from "@agent-native/core/client/i18n";
3
+ import { buildSignInReturnHref } from "@agent-native/core/client/ui";
4
+ import { IconAlertTriangle, IconCheck, IconLock } from "@tabler/icons-react";
5
+ import { useEffect, useMemo, useState } from "react";
6
+ import { Link, useSearchParams } from "react-router";
7
+
8
+ import { Button } from "@/components/ui/button";
9
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
10
+ import { Skeleton } from "@/components/ui/skeleton";
11
+ import enMessages from "@/i18n/en-US";
12
+
13
+ import {
14
+ recordingAccessApprovalContinuationPath,
15
+ recordingAccessApprovalSessionKey,
16
+ } from "../../shared/recording-link";
17
+
18
+ interface ApprovalResult {
19
+ ok: true;
20
+ alreadyAllowed: boolean;
21
+ requesterEmail: string;
22
+ recordingId: string;
23
+ recordingTitle: string;
24
+ shareId: string;
25
+ message: string;
26
+ }
27
+
28
+ type ApprovalState =
29
+ | { kind: "loading" }
30
+ | { kind: "sign-in" }
31
+ | { kind: "success"; result: ApprovalResult }
32
+ | { kind: "error"; message: string };
33
+
34
+ export function meta() {
35
+ return [{ title: enMessages.sharePage.privateClip }];
36
+ }
37
+
38
+ export default function ApproveRecordingAccessRequestRoute() {
39
+ const t = useT();
40
+ const [searchParams] = useSearchParams();
41
+ const { session, isLoading: sessionLoading } = useSession();
42
+ const recordingId = searchParams.get("recordingId") ?? "";
43
+ const approvalTokenFromUrl = searchParams.get("token") ?? "";
44
+ const approvalTokenStorageKey =
45
+ recordingAccessApprovalSessionKey(recordingId);
46
+ const [approvalToken, setApprovalToken] = useState(() => {
47
+ if (approvalTokenFromUrl) return approvalTokenFromUrl;
48
+ if (typeof window === "undefined" || !recordingId) return "";
49
+ try {
50
+ return sessionStorage.getItem(approvalTokenStorageKey) ?? "";
51
+ } catch {
52
+ // coercion-ok: unavailable tab storage is an absent continuation.
53
+ return "";
54
+ }
55
+ });
56
+ const [state, setState] = useState<ApprovalState>({ kind: "loading" });
57
+
58
+ useEffect(() => {
59
+ if (!recordingId) {
60
+ setApprovalToken("");
61
+ return;
62
+ }
63
+ if (!approvalTokenFromUrl) {
64
+ try {
65
+ setApprovalToken(sessionStorage.getItem(approvalTokenStorageKey) ?? "");
66
+ } catch {
67
+ setApprovalToken("");
68
+ }
69
+ return;
70
+ }
71
+ try {
72
+ sessionStorage.setItem(approvalTokenStorageKey, approvalTokenFromUrl);
73
+ } catch {
74
+ // coercion-ok: never fall back to putting the bearer token in the URL.
75
+ // Session storage may be unavailable in hardened browser contexts.
76
+ }
77
+ setApprovalToken(approvalTokenFromUrl);
78
+
79
+ // The email link must contain the capability, but it should not remain in
80
+ // the address bar or be copied into the sign-in continuation URL.
81
+ const params = new URLSearchParams(window.location.search);
82
+ params.delete("token");
83
+ const nextSearch = params.toString();
84
+ window.history.replaceState(
85
+ window.history.state,
86
+ "",
87
+ `${window.location.pathname}${nextSearch ? `?${nextSearch}` : ""}${window.location.hash}`,
88
+ );
89
+ }, [approvalTokenFromUrl, approvalTokenStorageKey, recordingId]);
90
+
91
+ const signInReturnTo = useMemo(() => {
92
+ return recordingAccessApprovalContinuationPath(recordingId);
93
+ }, [recordingId]);
94
+
95
+ useEffect(() => {
96
+ if (sessionLoading) return;
97
+
98
+ let cancelled = false;
99
+ if (!session?.email) {
100
+ setState({ kind: "sign-in" });
101
+ return () => {
102
+ cancelled = true;
103
+ };
104
+ }
105
+ if (!recordingId || !approvalToken) {
106
+ setState({
107
+ kind: "error",
108
+ message: t("sharePage.accessApprovalInvalid"),
109
+ });
110
+ return () => {
111
+ cancelled = true;
112
+ };
113
+ }
114
+
115
+ setState({ kind: "loading" });
116
+ void callAction<ApprovalResult>("approve-recording-access-request", {
117
+ recordingId,
118
+ approvalToken,
119
+ })
120
+ .then((result) => {
121
+ try {
122
+ sessionStorage.removeItem(approvalTokenStorageKey);
123
+ } catch {
124
+ // coercion-ok: cleanup failure cannot authorize or expose the token.
125
+ // Session storage may be unavailable in hardened browser contexts.
126
+ }
127
+ if (!cancelled) setState({ kind: "success", result });
128
+ })
129
+ .catch((error: unknown) => {
130
+ if (cancelled) return;
131
+ setState({
132
+ kind: "error",
133
+ message:
134
+ error instanceof Error
135
+ ? error.message
136
+ : t("sharePage.accessApprovalInvalid"),
137
+ });
138
+ });
139
+
140
+ return () => {
141
+ cancelled = true;
142
+ };
143
+ }, [
144
+ approvalToken,
145
+ approvalTokenStorageKey,
146
+ recordingId,
147
+ session?.email,
148
+ sessionLoading,
149
+ t,
150
+ ]);
151
+
152
+ const signInHref = buildSignInReturnHref({ returnTo: signInReturnTo });
153
+
154
+ return (
155
+ <div className="flex min-h-screen items-center justify-center bg-background px-6 py-12 text-foreground">
156
+ <Card className="w-full max-w-md">
157
+ <CardHeader>
158
+ <CardTitle className="flex items-center gap-2">
159
+ <IconLock
160
+ className="size-5 text-muted-foreground"
161
+ aria-hidden="true"
162
+ />
163
+ {state.kind === "success"
164
+ ? state.result.alreadyAllowed
165
+ ? t("sharePage.accessApprovalAlreadyTitle")
166
+ : t("sharePage.accessApprovalTitle")
167
+ : state.kind === "error"
168
+ ? t("sharePage.accessApprovalErrorTitle")
169
+ : state.kind === "sign-in"
170
+ ? t("sharePage.accessApprovalSignInTitle")
171
+ : t("sharePage.accessApprovalLoading")}
172
+ </CardTitle>
173
+ </CardHeader>
174
+ <CardContent aria-live="polite">
175
+ {state.kind === "loading" ? (
176
+ <div className="space-y-3 py-2">
177
+ <Skeleton className="h-4 w-full" />
178
+ <Skeleton className="h-4 w-4/5" />
179
+ </div>
180
+ ) : state.kind === "sign-in" ? (
181
+ <div className="space-y-4">
182
+ <p className="text-sm text-muted-foreground">
183
+ {t("sharePage.accessApprovalSignInMessage")}
184
+ </p>
185
+ <Button asChild>
186
+ <a
187
+ href={signInHref}
188
+ onClick={() => {
189
+ if (!approvalToken) return;
190
+ try {
191
+ sessionStorage.setItem(
192
+ approvalTokenStorageKey,
193
+ approvalToken,
194
+ );
195
+ } catch {
196
+ // coercion-ok: never fall back to putting the bearer token in the URL.
197
+ // Session storage may be unavailable in hardened browser contexts.
198
+ }
199
+ }}
200
+ >
201
+ {t("sharePage.accessApprovalSignIn")}
202
+ </a>
203
+ </Button>
204
+ </div>
205
+ ) : state.kind === "error" ? (
206
+ <div className="space-y-4">
207
+ <div className="flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive">
208
+ <IconAlertTriangle
209
+ className="mt-0.5 size-4 shrink-0"
210
+ aria-hidden="true"
211
+ />
212
+ <span>{state.message}</span>
213
+ </div>
214
+ {recordingId ? (
215
+ <Button asChild variant="outline">
216
+ <Link to={`/r/${encodeURIComponent(recordingId)}`}>
217
+ {t("sharePage.accessApprovalOpenClip")}
218
+ </Link>
219
+ </Button>
220
+ ) : null}
221
+ </div>
222
+ ) : (
223
+ <div className="space-y-4">
224
+ <div className="flex items-start gap-3 rounded-md border border-primary/30 bg-primary/10 p-3 text-sm">
225
+ <IconCheck
226
+ className="mt-0.5 size-4 shrink-0 text-primary"
227
+ aria-hidden="true"
228
+ />
229
+ <span>
230
+ {state.result.alreadyAllowed
231
+ ? t("sharePage.accessApprovalAlreadyMessage", {
232
+ email: state.result.requesterEmail,
233
+ })
234
+ : t("sharePage.accessApprovalMessage", {
235
+ email: state.result.requesterEmail,
236
+ })}
237
+ </span>
238
+ </div>
239
+ <Button asChild variant="outline">
240
+ <Link to={`/r/${encodeURIComponent(state.result.recordingId)}`}>
241
+ {t("sharePage.accessApprovalOpenClip")}
242
+ </Link>
243
+ </Button>
244
+ </div>
245
+ )}
246
+ </CardContent>
247
+ </Card>
248
+ </div>
249
+ );
250
+ }
@@ -16,7 +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
+ import { ShareCopyRow, ShareTrigger } from "@agent-native/toolkit/sharing";
20
20
  import {
21
21
  BUILDER_CREDITS_UPGRADE_URL,
22
22
  type BuilderCreditsStatus,
@@ -277,7 +277,6 @@ export default function RecordingPage() {
277
277
  const [processingTimeout, setProcessingTimeout] = useState(false);
278
278
  const [retryingFinalize, setRetryingFinalize] = useState(false);
279
279
  const [downloading, setDownloading] = useState(false);
280
- const [pendingLinkCopied, setPendingLinkCopied] = useState(false);
281
280
  const browserTabId = useMemo(() => getBrowserTabId(), []);
282
281
  const recordingScope = useMemo(
283
282
  () =>
@@ -445,14 +444,6 @@ export default function RecordingPage() {
445
444
  if (!recordingId || typeof window === "undefined") return "";
446
445
  return recordingShareUrl(recordingId, shareViaId);
447
446
  }, [recordingId, shareViaId]);
448
- const copyPendingShareLink = useCallback(async () => {
449
- if (!pendingShareUrl) return;
450
- // The full Share popover remains available when clipboard permission is
451
- // unavailable, so a denied clipboard write does not block the page.
452
- if (!(await writeClipboardText(pendingShareUrl))) return;
453
- setPendingLinkCopied(true);
454
- window.setTimeout(() => setPendingLinkCopied(false), 1400);
455
- }, [pendingShareUrl]);
456
447
  useEffect(() => {
457
448
  if (!recording?.id) return;
458
449
  const now = Date.now();
@@ -1033,28 +1024,14 @@ export default function RecordingPage() {
1033
1024
  />
1034
1025
  </div>
1035
1026
  ) : null}
1036
- <div className="flex min-w-0 items-center gap-2 rounded-lg border border-border bg-muted/30 p-2 ps-3">
1037
- <div className="min-w-0 flex-1">
1038
- <p className="text-[11px] font-medium text-muted-foreground">
1039
- {t("shareDialog.shareLink")}
1040
- </p>
1041
- <p className="truncate text-sm text-foreground">
1042
- {pendingShareUrl}
1043
- </p>
1044
- </div>
1045
- <Button
1046
- type="button"
1047
- variant="outline"
1048
- size="sm"
1049
- className="shrink-0 gap-1.5"
1050
- onClick={() => void copyPendingShareLink()}
1051
- >
1052
- <IconClipboardCopy className="size-4" />
1053
- {pendingLinkCopied
1054
- ? t("bugReportRoute.copied")
1055
- : t("shareUi.copy")}
1056
- </Button>
1057
- </div>
1027
+ <ShareCopyRow
1028
+ value={pendingShareUrl}
1029
+ label={t("shareDialog.shareLink")}
1030
+ copyLabel={t("shareUi.copy")}
1031
+ copiedLabel={t("bugReportRoute.copied")}
1032
+ onCopy={writeClipboardText}
1033
+ className="rounded-lg border border-border bg-muted/30 p-2 ps-3"
1034
+ />
1058
1035
  </div>
1059
1036
  </main>
1060
1037
  </div>
@@ -32,6 +32,7 @@ import {
32
32
  useMemo,
33
33
  useRef,
34
34
  useState,
35
+ type FormEvent,
35
36
  type ReactNode,
36
37
  } from "react";
37
38
  import {
@@ -55,6 +56,7 @@ import { RecordingOptionsMenu } from "@/components/player/delete-recording-menu"
55
56
  import { InsightsPanel } from "@/components/player/insights-panel";
56
57
  import { ReactionsTray } from "@/components/player/reactions-tray";
57
58
  import { RecordingViewsBadge } from "@/components/player/recording-views-badge";
59
+ import { RequestAccessDialog } from "@/components/player/request-access-dialog";
58
60
  import { ShareRecordingPopover } from "@/components/player/share-dialog";
59
61
  import { SignInPromptDialog } from "@/components/player/sign-in-prompt-dialog";
60
62
  import { TranscriptPanel } from "@/components/player/transcript-panel";
@@ -97,6 +99,7 @@ import {
97
99
  CLIPS_ACCESS_REQUEST_TOKEN_TTL_SECONDS,
98
100
  } from "../../shared/recording-link";
99
101
  import {
102
+ buildShareContinuationQuery,
100
103
  buildSignupAttributionQuery,
101
104
  readShareAttribution,
102
105
  } from "../../shared/share-attribution";
@@ -242,14 +245,12 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
242
245
  if (!access) {
243
246
  const status = userEmail ? 403 : 401;
244
247
  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
- }
248
+ deniedData.accessRequestToken = signScopedAgentAccessToken({
249
+ resourceKind: CLIPS_ACCESS_REQUEST_TOKEN_PREFIX,
250
+ resourceId: id,
251
+ ...(userEmail ? { viewerEmail: userEmail } : {}),
252
+ ttlSeconds: CLIPS_ACCESS_REQUEST_TOKEN_TTL_SECONDS,
253
+ });
253
254
  return privateShareLoaderData(deniedData, status);
254
255
  }
255
256
  }
@@ -453,7 +454,11 @@ export default function ShareRoute() {
453
454
  notifiedOwner: boolean;
454
455
  ok: true;
455
456
  },
456
- { accessRequestToken?: string; recordingId: string }
457
+ {
458
+ accessRequestToken?: string;
459
+ recordingId: string;
460
+ requesterEmail?: string;
461
+ }
457
462
  >("request-recording-access");
458
463
  const [signInIntent, setSignInIntent] = useState<"comment" | "react" | null>(
459
464
  null,
@@ -467,6 +472,14 @@ export default function ShareRoute() {
467
472
  const [downloading, setDownloading] = useState(false);
468
473
  const [downloadMenuOpen, setDownloadMenuOpen] = useState(false);
469
474
  const [accessRequestSent, setAccessRequestSent] = useState(false);
475
+ const [accessRequestError, setAccessRequestError] = useState<string | null>(
476
+ null,
477
+ );
478
+ const [requestAccessDialogOpen, setRequestAccessDialogOpen] = useState(false);
479
+ const [requesterEmail, setRequesterEmail] = useState("");
480
+ const [requestAccessDialogError, setRequestAccessDialogError] = useState<
481
+ string | null
482
+ >(null);
470
483
  const agentAccessToken = useMemo(() => {
471
484
  if (typeof window === "undefined") return "";
472
485
  return (
@@ -476,6 +489,71 @@ export default function ShareRoute() {
476
489
  );
477
490
  }, []);
478
491
 
492
+ const shareReturnTo = useMemo(() => {
493
+ const path = `/share/${encodeURIComponent(recordingId)}`;
494
+ if (typeof window === "undefined") return path;
495
+ const query = buildShareContinuationQuery(attribution);
496
+ return query ? `${path}?${query}` : path;
497
+ }, [attribution, recordingId]);
498
+ const signInHref = buildSignInReturnHref({ returnTo: shareReturnTo });
499
+
500
+ const submitAccessRequest = useCallback(
501
+ (email?: string) => {
502
+ if (!shareId || accessRequestSent || requestAccess.isPending) return;
503
+ const normalizedEmail = email?.trim() || undefined;
504
+ setAccessRequestError(null);
505
+ setRequestAccessDialogError(null);
506
+ requestAccess.mutate(
507
+ {
508
+ accessRequestToken: loaderData.accessRequestToken,
509
+ recordingId: shareId,
510
+ ...(normalizedEmail ? { requesterEmail: normalizedEmail } : {}),
511
+ },
512
+ {
513
+ onSuccess: () => {
514
+ setAccessRequestSent(true);
515
+ setRequestAccessDialogOpen(false);
516
+ toast.success(
517
+ normalizedEmail
518
+ ? t("sharePage.accessRequestSentWithEmail", {
519
+ email: normalizedEmail,
520
+ })
521
+ : t("sharePage.accessRequestSent"),
522
+ );
523
+ },
524
+ onError: (error: unknown) => {
525
+ const message =
526
+ error instanceof Error && error.message
527
+ ? error.message
528
+ : t("sharePage.accessRequestFailed");
529
+ setAccessRequestError(message);
530
+ setRequestAccessDialogError(message);
531
+ },
532
+ },
533
+ );
534
+ },
535
+ [
536
+ accessRequestSent,
537
+ loaderData.accessRequestToken,
538
+ requestAccess,
539
+ shareId,
540
+ t,
541
+ ],
542
+ );
543
+
544
+ const submitGuestAccessRequest = useCallback(
545
+ (event: FormEvent<HTMLFormElement>) => {
546
+ event.preventDefault();
547
+ const email = requesterEmail.trim();
548
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
549
+ setRequestAccessDialogError(t("sharePage.requestAccessEmailRequired"));
550
+ return;
551
+ }
552
+ submitAccessRequest(email);
553
+ },
554
+ [requesterEmail, submitAccessRequest, t],
555
+ );
556
+
479
557
  const dataQ = useQuery({
480
558
  queryKey: ["public-recording", shareId, password, agentAccessToken],
481
559
  queryFn: async () => {
@@ -815,29 +893,13 @@ export default function ShareRoute() {
815
893
  ? "sharePage.privateClipMessage"
816
894
  : "sharePage.privateClipSignedOutMessage",
817
895
  )}
896
+ error={canRequestAccess ? accessRequestError : null}
818
897
  action={
819
898
  canRequestAccess ? (
820
899
  <Button
821
900
  size="sm"
822
901
  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
- }}
902
+ onClick={() => submitAccessRequest()}
841
903
  >
842
904
  {requestSent
843
905
  ? t("sharePage.accessRequested")
@@ -846,20 +908,34 @@ export default function ShareRoute() {
846
908
  : t("sharePage.requestAccess")}
847
909
  </Button>
848
910
  ) : shareId ? (
849
- <Button asChild size="sm">
850
- <a
851
- href={buildSignInReturnHref({
852
- returnTo: `/share/${shareId}`,
853
- })}
854
- className="gap-1.5"
855
- >
856
- <IconLogin2 className="h-4 w-4 rtl:-scale-x-100" />
857
- {t("sharePage.signIn")}
858
- </a>
911
+ <Button
912
+ size="sm"
913
+ onClick={() => {
914
+ setAccessRequestError(null);
915
+ setRequestAccessDialogError(null);
916
+ setRequestAccessDialogOpen(true);
917
+ }}
918
+ >
919
+ {t("sharePage.requestAccess")}
859
920
  </Button>
860
921
  ) : null
861
922
  }
862
923
  />
924
+ {!canRequestAccess && shareId ? (
925
+ <RequestAccessDialog
926
+ open={requestAccessDialogOpen}
927
+ onOpenChange={setRequestAccessDialogOpen}
928
+ signInHref={signInHref}
929
+ email={requesterEmail}
930
+ onEmailChange={(value) => {
931
+ setRequesterEmail(value);
932
+ setRequestAccessDialogError(null);
933
+ }}
934
+ onSubmit={submitGuestAccessRequest}
935
+ isSubmitting={requestAccess.isPending}
936
+ error={requestAccessDialogError}
937
+ />
938
+ ) : null}
863
939
  </>
864
940
  );
865
941
  }
@@ -1464,11 +1540,13 @@ function EndState({
1464
1540
  icon,
1465
1541
  title,
1466
1542
  message,
1543
+ error,
1467
1544
  action,
1468
1545
  }: {
1469
1546
  icon?: ReactNode;
1470
1547
  title: string;
1471
1548
  message: string;
1549
+ error?: string | null;
1472
1550
  action?: ReactNode;
1473
1551
  }) {
1474
1552
  const t = useT();
@@ -1484,6 +1562,14 @@ function EndState({
1484
1562
  <p className="mb-6 max-w-md text-center text-sm text-muted-foreground">
1485
1563
  {message}
1486
1564
  </p>
1565
+ {error ? (
1566
+ <p
1567
+ className="mb-6 max-w-md text-center text-sm text-destructive"
1568
+ role="alert"
1569
+ >
1570
+ {error}
1571
+ </p>
1572
+ ) : null}
1487
1573
  <div className="flex flex-wrap items-center justify-center gap-2">
1488
1574
  {action}
1489
1575
  <Button asChild variant="ghost" size="sm">
@@ -45,6 +45,11 @@ import {
45
45
  import { getDb, schema } from "../../server/db";
46
46
  import { CLIPS_MEETING_AGENT_RESOURCE_KIND } from "../../shared/meeting-agent-access";
47
47
  import { privateShareLoaderData } from "../../shared/share-loader-response";
48
+ import {
49
+ normalizeTranscriptSegments,
50
+ parseTranscriptSegments,
51
+ } from "../../shared/transcript-segments";
52
+ import { resolveTranscriptPresentation } from "../../shared/transcript-status";
48
53
 
49
54
  type LoaderData = { meeting: PublicMeeting | null };
50
55
 
@@ -84,6 +89,8 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
84
89
  actualStart: schema.meetings.actualStart,
85
90
  actualEnd: schema.meetings.actualEnd,
86
91
  transcriptStatus: schema.meetings.transcriptStatus,
92
+ recordingId: schema.meetings.recordingId,
93
+ shareTranscript: schema.meetings.shareTranscript,
87
94
  visibility: schema.meetings.visibility,
88
95
  })
89
96
  .from(schema.meetings)
@@ -104,7 +111,7 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
104
111
  return shareMeetingLoaderData({ meeting: null }, hasAgentAccessToken);
105
112
  }
106
113
 
107
- const [participants, actionItems] = await Promise.all([
114
+ const [participants, actionItems, transcriptRows] = await Promise.all([
108
115
  getDb()
109
116
  .select({
110
117
  email: schema.meetingParticipants.email,
@@ -122,8 +129,33 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
122
129
  })
123
130
  .from(schema.meetingActionItems)
124
131
  .where(eq(schema.meetingActionItems.meetingId, meetingId)),
132
+ meeting.shareTranscript && meeting.recordingId
133
+ ? getDb()
134
+ .select({
135
+ status: schema.recordingTranscripts.status,
136
+ language: schema.recordingTranscripts.language,
137
+ fullText: schema.recordingTranscripts.fullText,
138
+ failureReason: schema.recordingTranscripts.failureReason,
139
+ segmentsJson: schema.recordingTranscripts.segmentsJson,
140
+ updatedAt: schema.recordingTranscripts.updatedAt,
141
+ })
142
+ .from(schema.recordingTranscripts)
143
+ .where(
144
+ eq(schema.recordingTranscripts.recordingId, meeting.recordingId),
145
+ )
146
+ .limit(1)
147
+ : Promise.resolve([]),
125
148
  ]);
126
149
 
150
+ const transcript = transcriptRows[0] ?? null;
151
+ const transcriptPresentation = resolveTranscriptPresentation(transcript);
152
+ const transcriptSegments = transcript
153
+ ? normalizeTranscriptSegments({
154
+ segments: parseTranscriptSegments(transcript.segmentsJson),
155
+ fullText: transcript.fullText,
156
+ })
157
+ : [];
158
+
127
159
  let bullets: PublicMeeting["bullets"] = [];
128
160
  try {
129
161
  const parsed = JSON.parse(meeting.bulletsJson);
@@ -150,6 +182,14 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
150
182
  actualStart: meeting.actualStart,
151
183
  actualEnd: meeting.actualEnd,
152
184
  transcriptStatus: meeting.transcriptStatus,
185
+ transcript: transcript
186
+ ? {
187
+ status: transcriptPresentation.status ?? transcript.status,
188
+ language: transcript.language,
189
+ fullText: transcript.fullText,
190
+ segments: transcriptSegments,
191
+ }
192
+ : null,
153
193
  },
154
194
  },
155
195
  hasAgentAccessToken,
@@ -504,6 +504,7 @@ export async function materializeCalendarMeetingFromVirtualId(
504
504
  calendarEventId: snapshot.id,
505
505
  recordingId: null,
506
506
  transcriptStatus: "idle",
507
+ shareTranscript: false,
507
508
  summaryMd: "",
508
509
  bulletsJson: "[]",
509
510
  actionItemsJson: "[]",