@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
@@ -8,10 +8,6 @@ import {
8
8
  isLikelyPhoneMicLabel,
9
9
  type AudioInputFallback,
10
10
  } from "@shared/media-device-selection";
11
- import {
12
- createMicAudioCleanup,
13
- type MicAudioCleanupHandle,
14
- } from "@shared/mic-audio-cleanup";
15
11
  import {
16
12
  SCREEN_CAPTURE_FRAME_RATE,
17
13
  screenCaptureDisplayOptions,
@@ -509,7 +505,6 @@ export class RecorderEngine {
509
505
  private cameraComposite: CameraCompositeHandle | null = null;
510
506
  private audioMixCtx: AudioContext | null = null;
511
507
  private audioMixSources: MediaStreamAudioSourceNode[] = [];
512
- private micAudioCleanup: MicAudioCleanupHandle[] = [];
513
508
  private recorder: MediaRecorder | null = null;
514
509
  private mimeType: string = "video/webm";
515
510
 
@@ -1360,7 +1355,6 @@ export class RecorderEngine {
1360
1355
  // Full stream teardown happens in the finally block below.
1361
1356
  this.cameraLive = false;
1362
1357
  this.audioMixSources = [];
1363
- this.stopMicAudioCleanup();
1364
1358
  this.audioMixCtx?.close().catch(() => {});
1365
1359
  this.audioMixCtx = null;
1366
1360
  for (const s of [this.cameraStream, this.rawCameraStream, this.micStream]) {
@@ -1828,26 +1822,15 @@ export class RecorderEngine {
1828
1822
  this.audioMixSources = [];
1829
1823
  const dest = ctx.createMediaStreamDestination();
1830
1824
  for (const input of audioInputs) {
1831
- let track = input.track;
1832
- if (input.isMicrophone) {
1833
- const cleanup = createMicAudioCleanup(new MediaStream([track]), {
1834
- audioContext: ctx,
1835
- });
1836
- this.micAudioCleanup.push(cleanup);
1837
- track = cleanup.stream.getAudioTracks()[0] ?? track;
1838
- }
1839
- const source = ctx.createMediaStreamSource(new MediaStream([track]));
1825
+ const source = ctx.createMediaStreamSource(
1826
+ new MediaStream([input.track]),
1827
+ );
1840
1828
  source.connect(dest);
1841
1829
  this.audioMixSources.push(source);
1842
1830
  }
1843
1831
  return dest.stream.getAudioTracks()[0];
1844
1832
  }
1845
1833
 
1846
- private stopMicAudioCleanup(): void {
1847
- for (const cleanup of this.micAudioCleanup) cleanup.stop();
1848
- this.micAudioCleanup = [];
1849
- }
1850
-
1851
1834
  private buildCombinedStream(): MediaStream {
1852
1835
  if (this.opts.mode === "screen") {
1853
1836
  return this.buildDisplayRecordingStream();
@@ -2382,7 +2365,6 @@ export class RecorderEngine {
2382
2365
  // aren't mistaken for a disconnect.
2383
2366
  this.cameraLive = false;
2384
2367
  this.audioMixSources = [];
2385
- this.stopMicAudioCleanup();
2386
2368
  this.audioMixCtx?.close().catch(() => {});
2387
2369
  this.audioMixCtx = null;
2388
2370
  this.cameraComposite?.cleanup();
@@ -4,9 +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 { ShareCopyRow } from "@agent-native/toolkit/sharing";
7
8
  import {
8
- IconCheck,
9
- IconLink,
10
9
  IconLock,
11
10
  IconSend2,
12
11
  IconTrash,
@@ -281,35 +280,19 @@ export function CopyField({
281
280
  description?: string;
282
281
  }) {
283
282
  const t = useT();
284
- const [copied, setCopied] = useState(false);
285
- const copy = () => {
286
- if (disabled) return;
287
- copyToClipboard(value);
288
- setCopied(true);
289
- setTimeout(() => setCopied(false), 1400);
290
- };
291
283
  return (
292
- <div className="flex items-center justify-between gap-3">
293
- <div className="min-w-0">
294
- {label ? <div className="text-sm font-medium">{label}</div> : null}
295
- {description ? (
296
- <div className="text-xs text-muted-foreground">{description}</div>
297
- ) : null}
298
- </div>
299
- <div aria-live="polite" className="shrink-0">
300
- <Button
301
- type="button"
302
- variant="outline"
303
- size="sm"
304
- onClick={copy}
305
- disabled={disabled}
306
- className="h-9"
307
- >
308
- {copied ? <IconCheck size={14} /> : <IconLink size={14} />}
309
- {copied ? t("recordRoute.linkCopied") : t("shareUi.copy")}
310
- </Button>
311
- </div>
312
- </div>
284
+ <ShareCopyRow
285
+ label={label}
286
+ value={value}
287
+ description={description}
288
+ disabled={disabled}
289
+ copyLabel={t("shareUi.copy")}
290
+ copiedLabel={t("recordRoute.linkCopied")}
291
+ onCopy={(nextValue) => {
292
+ copyToClipboard(nextValue);
293
+ return true;
294
+ }}
295
+ />
313
296
  );
314
297
  }
315
298
 
@@ -256,6 +256,16 @@ const messages = {
256
256
  passwordProtected: "This clip is password-protected",
257
257
  linkExpired: "Link expired",
258
258
  linkExpiredMessage: "The creator set an expiry on this share link.",
259
+ privateClip: "Private clip",
260
+ privateClipMessage:
261
+ "This clip is private. Request access and the owner will be notified.",
262
+ privateClipSignedOutMessage:
263
+ "This clip is private. Sign in to request access from the owner.",
264
+ requestAccess: "Request access",
265
+ requestingAccess: "Requesting access...",
266
+ accessRequested: "Access requested",
267
+ accessRequestSent: "The clip owner was notified.",
268
+ accessRequestFailed: "Couldn't request access. Try again.",
259
269
  clipUnavailable: "Clip unavailable",
260
270
  clipUnavailableMessage:
261
271
  "This recording isn't public, or the link is invalid. If it's your clip, sign in to check access.",
@@ -5,6 +5,7 @@ import {
5
5
  useActionQuery,
6
6
  } from "@agent-native/core/client/hooks";
7
7
  import { useT } from "@agent-native/core/client/i18n";
8
+ import { ShareTrigger } from "@agent-native/toolkit/sharing";
8
9
  import {
9
10
  IconArrowLeft,
10
11
  IconCheck,
@@ -18,7 +19,6 @@ import {
18
19
  IconNotes,
19
20
  IconPlayerStop,
20
21
  IconRefresh,
21
- IconShare3,
22
22
  IconTrash,
23
23
  IconUsers,
24
24
  } from "@tabler/icons-react";
@@ -711,10 +711,10 @@ export default function MeetingDetailRoute() {
711
711
  Boolean(data?.transcript?.fullText?.trim()))
712
712
  }
713
713
  >
714
- <Button size="sm" className="shrink-0 gap-1.5">
715
- <IconShare3 className="h-4 w-4" />
716
- {t("meetingDetail.share")}
717
- </Button>
714
+ <ShareTrigger
715
+ label={t("meetingDetail.share")}
716
+ className="shrink-0"
717
+ />
718
718
  </ShareMeetingPopover>
719
719
  {canEdit && (
720
720
  <AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
@@ -16,6 +16,7 @@ import {
16
16
  isHumanReadableDocumentTitle,
17
17
  normalizeDocumentTitle,
18
18
  } from "@agent-native/core/shared";
19
+ import { ShareTrigger } from "@agent-native/toolkit/sharing";
19
20
  import {
20
21
  BUILDER_CREDITS_UPGRADE_URL,
21
22
  type BuilderCreditsStatus,
@@ -33,7 +34,6 @@ import {
33
34
  } from "@shared/share-attribution";
34
35
  import type { WorkflowKind } from "@shared/workflow";
35
36
  import {
36
- IconShare3,
37
37
  IconArrowLeft,
38
38
  IconChevronDown,
39
39
  IconCalendar,
@@ -1002,12 +1002,10 @@ export default function RecordingPage() {
1002
1002
  hasPassword={Boolean(recording.hasPassword)}
1003
1003
  viewerReshareOnly={viewerReshareOnly}
1004
1004
  >
1005
- <Button className="shrink-0 gap-1.5" size="sm">
1006
- {recording.visibility !== "public" ? (
1007
- <IconShare3 className="h-4 w-4" />
1008
- ) : null}
1009
- {t("recordingPage.share")}
1010
- </Button>
1005
+ <ShareTrigger
1006
+ label={t("recordingPage.share")}
1007
+ className="shrink-0"
1008
+ />
1011
1009
  </ShareRecordingPopover>
1012
1010
  </header>
1013
1011
 
@@ -1570,15 +1568,10 @@ export default function RecordingPage() {
1570
1568
  hasPassword={Boolean(recording.hasPassword)}
1571
1569
  viewerReshareOnly={viewerReshareOnly}
1572
1570
  >
1573
- <Button
1574
- className="shrink-0 gap-1.5 bg-primary text-primary-foreground hover:bg-primary/90"
1575
- size="sm"
1576
- >
1577
- {recording.visibility !== "public" ? (
1578
- <IconShare3 className="h-4 w-4" />
1579
- ) : null}
1580
- {t("recordingPage.share")}
1581
- </Button>
1571
+ <ShareTrigger
1572
+ label={t("recordingPage.share")}
1573
+ className="shrink-0"
1574
+ />
1582
1575
  </ShareRecordingPopover>
1583
1576
  ) : null}
1584
1577
 
@@ -5,10 +5,15 @@ import {
5
5
  appBasePath,
6
6
  appPath,
7
7
  } from "@agent-native/core/client/api-path";
8
- import { useSession, getBrowserTabId } from "@agent-native/core/client/hooks";
8
+ import {
9
+ useActionMutation,
10
+ useSession,
11
+ getBrowserTabId,
12
+ } from "@agent-native/core/client/hooks";
9
13
  import { useT } from "@agent-native/core/client/i18n";
10
14
  import { buildSignInReturnHref } from "@agent-native/core/client/ui";
11
15
  import { docsUrl } from "@agent-native/core/shared";
16
+ import { ShareTrigger } from "@agent-native/toolkit/sharing";
12
17
  import {
13
18
  IconAlertTriangle,
14
19
  IconArrowLeft,
@@ -16,8 +21,8 @@ import {
16
21
  IconDownload,
17
22
  IconDots,
18
23
  IconExternalLink,
24
+ IconLock,
19
25
  IconLogin2,
20
- IconShare3,
21
26
  } from "@tabler/icons-react";
22
27
  import { useQuery } from "@tanstack/react-query";
23
28
  import { eq } from "drizzle-orm";
@@ -87,6 +92,10 @@ import {
87
92
  isLoomEmbedBackedRecording,
88
93
  isLoomRecordingSource,
89
94
  } from "../../shared/loom";
95
+ import {
96
+ CLIPS_ACCESS_REQUEST_TOKEN_PREFIX,
97
+ CLIPS_ACCESS_REQUEST_TOKEN_TTL_SECONDS,
98
+ } from "../../shared/recording-link";
90
99
  import {
91
100
  buildSignupAttributionQuery,
92
101
  readShareAttribution,
@@ -116,16 +125,22 @@ type SharePageLoaderData = {
116
125
  agentContextUrl: string | null;
117
126
  origin: string | null;
118
127
  shareUrl: string | null;
128
+ accessDeniedStatus?: 401 | 403;
129
+ accessRequestToken?: string;
119
130
  };
120
131
 
121
132
  const CLIPS_AGENT_ACCESS_TTL_SECONDS = 2 * 60 * 60;
122
133
 
123
- function emptyLoaderData(url: URL): SharePageLoaderData {
134
+ function emptyLoaderData(
135
+ url: URL,
136
+ accessDeniedStatus?: 401 | 403,
137
+ ): SharePageLoaderData {
124
138
  return {
125
139
  recording: null,
126
140
  agentContextUrl: null,
127
141
  origin: url.origin,
128
142
  shareUrl: null,
143
+ accessDeniedStatus,
129
144
  };
130
145
  }
131
146
 
@@ -224,7 +239,19 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
224
239
  if (rec.visibility !== "public" && !tokenGrantsAgentAccess) {
225
240
  const userEmail = getRequestUserEmail();
226
241
  const access = userEmail ? await resolveAccess("recording", id) : null;
227
- if (!access) return privateShareLoaderData(emptyLoaderData(url));
242
+ if (!access) {
243
+ const status = userEmail ? 403 : 401;
244
+ const deniedData = emptyLoaderData(url, status);
245
+ if (userEmail) {
246
+ deniedData.accessRequestToken = signScopedAgentAccessToken({
247
+ resourceKind: CLIPS_ACCESS_REQUEST_TOKEN_PREFIX,
248
+ resourceId: id,
249
+ viewerEmail: userEmail,
250
+ ttlSeconds: CLIPS_ACCESS_REQUEST_TOKEN_TTL_SECONDS,
251
+ });
252
+ }
253
+ return privateShareLoaderData(deniedData, status);
254
+ }
228
255
  }
229
256
 
230
257
  const recording: SharePageMetaRecording = {
@@ -418,6 +445,16 @@ export default function ShareRoute() {
418
445
  const [pwError, setPwError] = useState<string | null>(null);
419
446
  const [currentMs, setCurrentMs] = useState(0);
420
447
  const { session, isLoading: sessionLoading } = useSession();
448
+ const requestAccess = useActionMutation<
449
+ {
450
+ alreadyHasAccess: boolean;
451
+ alreadyRequested?: boolean;
452
+ message: string;
453
+ notifiedOwner: boolean;
454
+ ok: true;
455
+ },
456
+ { accessRequestToken?: string; recordingId: string }
457
+ >("request-recording-access");
421
458
  const [signInIntent, setSignInIntent] = useState<"comment" | "react" | null>(
422
459
  null,
423
460
  );
@@ -429,6 +466,7 @@ export default function ShareRoute() {
429
466
  );
430
467
  const [downloading, setDownloading] = useState(false);
431
468
  const [downloadMenuOpen, setDownloadMenuOpen] = useState(false);
469
+ const [accessRequestSent, setAccessRequestSent] = useState(false);
432
470
  const agentAccessToken = useMemo(() => {
433
471
  if (typeof window === "undefined") return "";
434
472
  return (
@@ -518,6 +556,16 @@ export default function ShareRoute() {
518
556
  const transcriptFailureReason =
519
557
  dataQ.data?.data?.transcript?.failureReason ?? null;
520
558
  const ctas = dataQ.data?.data?.ctas ?? [];
559
+ const apiStatus = dataQ.data?.status;
560
+ const apiAccessDeniedStatus =
561
+ apiStatus === 401 || apiStatus === 403 ? apiStatus : null;
562
+ const accessDeniedStatus =
563
+ apiAccessDeniedStatus ??
564
+ (!recording && loaderData.accessDeniedStatus
565
+ ? session
566
+ ? 403
567
+ : 401
568
+ : null);
521
569
  const firstCta = ctas[0] ?? null;
522
570
  const viewerRole = dataQ.data?.data?.viewer?.role as
523
571
  | "owner"
@@ -751,18 +799,58 @@ export default function ShareRoute() {
751
799
  );
752
800
  }
753
801
 
754
- if (dataQ.data?.status === 401 || dataQ.data?.status === 404) {
802
+ if (accessDeniedStatus === 401 || accessDeniedStatus === 403) {
803
+ const canRequestAccess = accessDeniedStatus === 403 && Boolean(session);
804
+ const requestSent =
805
+ accessRequestSent || Boolean(dataQ.data?.data?.alreadyRequested);
806
+
755
807
  return (
756
808
  <>
757
809
  {agentDiscovery}
758
810
  <EndState
759
- title={t("sharePage.clipUnavailable")}
760
- message={t("sharePage.clipUnavailableMessage")}
811
+ icon={<IconLock className="h-5 w-5" aria-hidden="true" />}
812
+ title={t("sharePage.privateClip")}
813
+ message={t(
814
+ canRequestAccess
815
+ ? "sharePage.privateClipMessage"
816
+ : "sharePage.privateClipSignedOutMessage",
817
+ )}
761
818
  action={
762
- shareId ? (
819
+ canRequestAccess ? (
820
+ <Button
821
+ size="sm"
822
+ disabled={requestAccess.isPending || requestSent}
823
+ onClick={() => {
824
+ if (!shareId || requestSent) return;
825
+ requestAccess.mutate(
826
+ {
827
+ accessRequestToken: loaderData.accessRequestToken,
828
+ recordingId: shareId,
829
+ },
830
+ {
831
+ onSuccess: () => {
832
+ setAccessRequestSent(true);
833
+ toast.success(t("sharePage.accessRequestSent"));
834
+ },
835
+ onError: () => {
836
+ toast.error(t("sharePage.accessRequestFailed"));
837
+ },
838
+ },
839
+ );
840
+ }}
841
+ >
842
+ {requestSent
843
+ ? t("sharePage.accessRequested")
844
+ : requestAccess.isPending
845
+ ? t("sharePage.requestingAccess")
846
+ : t("sharePage.requestAccess")}
847
+ </Button>
848
+ ) : shareId ? (
763
849
  <Button asChild size="sm">
764
850
  <a
765
- href={buildSignInReturnHref({ returnTo: `/r/${shareId}` })}
851
+ href={buildSignInReturnHref({
852
+ returnTo: `/share/${shareId}`,
853
+ })}
766
854
  className="gap-1.5"
767
855
  >
768
856
  <IconLogin2 className="h-4 w-4 rtl:-scale-x-100" />
@@ -776,6 +864,18 @@ export default function ShareRoute() {
776
864
  );
777
865
  }
778
866
 
867
+ if (dataQ.data?.status === 404) {
868
+ return (
869
+ <>
870
+ {agentDiscovery}
871
+ <EndState
872
+ title={t("sharePage.clipUnavailable")}
873
+ message={t("sharePage.clipUnavailableMessage")}
874
+ />
875
+ </>
876
+ );
877
+ }
878
+
779
879
  if (!recording) {
780
880
  return (
781
881
  <>
@@ -930,14 +1030,26 @@ export default function ShareRoute() {
930
1030
  </Link>
931
1031
  </Button>
932
1032
  ) : null}
933
- <div className="min-w-0 flex-1">
1033
+ <div className="flex min-w-0 flex-1 items-center gap-2">
1034
+ {recording.visibility === "private" ? (
1035
+ <span
1036
+ className="inline-flex shrink-0 text-muted-foreground"
1037
+ role="img"
1038
+ title={t("sharePage.privateClip")}
1039
+ >
1040
+ <IconLock className="h-3.5 w-3.5" aria-hidden="true" />
1041
+ <span className="sr-only">{t("sharePage.privateClip")}</span>
1042
+ </span>
1043
+ ) : null}
934
1044
  {showTitleSkeleton ? (
935
1045
  <Skeleton
936
1046
  aria-label={t("sharePage.generatingTitle")}
937
- className="h-4 w-56 max-w-full"
1047
+ className="h-4 w-56 max-w-full flex-1"
938
1048
  />
939
1049
  ) : (
940
- <h1 className="truncate text-sm font-medium">{visibleTitle}</h1>
1050
+ <h1 className="min-w-0 flex-1 truncate text-sm font-medium">
1051
+ {visibleTitle}
1052
+ </h1>
941
1053
  )}
942
1054
  </div>
943
1055
 
@@ -1021,12 +1133,10 @@ export default function ShareRoute() {
1021
1133
  hasPassword={Boolean(recording.hasPassword)}
1022
1134
  viewerReshareOnly={viewerReshareOnly}
1023
1135
  >
1024
- <Button size="sm" className="shrink-0 gap-1.5">
1025
- {recording.visibility !== "public" ? (
1026
- <IconShare3 className="h-4 w-4" />
1027
- ) : null}
1028
- {t("sharePage.share")}
1029
- </Button>
1136
+ <ShareTrigger
1137
+ label={t("sharePage.share")}
1138
+ className="shrink-0"
1139
+ />
1030
1140
  </ShareRecordingPopover>
1031
1141
  ) : null}
1032
1142
  </div>
@@ -1351,10 +1461,12 @@ function PublicAgentEmptyState({
1351
1461
  }
1352
1462
 
1353
1463
  function EndState({
1464
+ icon,
1354
1465
  title,
1355
1466
  message,
1356
1467
  action,
1357
1468
  }: {
1469
+ icon?: ReactNode;
1358
1470
  title: string;
1359
1471
  message: string;
1360
1472
  action?: ReactNode;
@@ -1363,6 +1475,11 @@ function EndState({
1363
1475
 
1364
1476
  return (
1365
1477
  <div className="flex flex-col items-center justify-center min-h-screen bg-background text-foreground px-6">
1478
+ {icon ? (
1479
+ <div className="mb-3 flex h-10 w-10 items-center justify-center rounded-full border border-border bg-muted text-muted-foreground">
1480
+ {icon}
1481
+ </div>
1482
+ ) : null}
1366
1483
  <h1 className="text-2xl font-semibold mb-2">{title}</h1>
1367
1484
  <p className="mb-6 max-w-md text-center text-sm text-muted-foreground">
1368
1485
  {message}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-14
4
+ ---
5
+
6
+ Long desktop recordings now complete or abort resumable uploads cleanly, preserve local recovery copies, and show the actual upload error when saving fails.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-14
4
+ ---
5
+
6
+ Clips now uses consistent text-only share controls with compact copy actions and an expandable Share with agents section across recordings and meetings.
@@ -3,7 +3,7 @@
3
3
  "name": "Agent-Native Clips",
4
4
  "short_name": "Clips",
5
5
  "version": "0.1.18",
6
- "description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub.",
6
+ "description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub and Jira.",
7
7
  "minimum_chrome_version": "116",
8
8
  "action": {
9
9
  "default_title": "Record with Clips",
@@ -35,7 +35,11 @@
35
35
  ],
36
36
  "content_scripts": [
37
37
  {
38
- "matches": ["https://github.com/*"],
38
+ "matches": [
39
+ "https://github.com/*",
40
+ "https://*.atlassian.net/*",
41
+ "https://*.jira.com/*"
42
+ ],
39
43
  "js": ["assets/github-preview-content.js"],
40
44
  "run_at": "document_idle",
41
45
  "all_frames": false
@@ -48,7 +52,11 @@
48
52
  },
49
53
  {
50
54
  "resources": ["src/github-preview.html"],
51
- "matches": ["https://github.com/*"]
55
+ "matches": [
56
+ "https://github.com/*",
57
+ "https://*.atlassian.net/*",
58
+ "https://*.jira.com/*"
59
+ ]
52
60
  }
53
61
  ],
54
62
  "externally_connectable": {
@@ -1,13 +1,14 @@
1
- // GitHub Clips link previews. This is a narrow declarative content script for
2
- // github.com only; the recording overlay remains activeTab-injected.
3
- (function clipsGithubPreviewContent() {
4
- const ROOT_CLASS = "clips-github-preview";
5
- const PROCESSED_ATTR = "data-clips-github-preview";
1
+ // Clips link previews for GitHub and Jira Cloud. The recording overlay remains
2
+ // activeTab-injected so this content script only handles rendered issue text.
3
+ (function clipsLinkPreviewContent() {
4
+ const ROOT_CLASS = "clips-link-preview";
5
+ const PROCESSED_ATTR = "data-clips-link-preview";
6
6
  const SOURCE_ATTR = "data-clips-source-url";
7
7
  const EXTENSION_ORIGIN = chrome.runtime.getURL("").replace(/\/$/, "");
8
8
  const CLIPS_HOSTS = new Set(["clips.agent-native.com"]);
9
9
  const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1"]);
10
10
  const ROUTE_SEGMENTS = new Set(["r", "share", "embed"]);
11
+ const JIRA_HOST_SUFFIXES = [".atlassian.net", ".jira.com"];
11
12
  const MIN_HEIGHT = 120;
12
13
  const MAX_HEIGHT = 720;
13
14
 
@@ -115,6 +116,34 @@
115
116
  );
116
117
  }
117
118
 
119
+ function isJiraPage(): boolean {
120
+ const hostname = location.hostname.toLowerCase();
121
+ return JIRA_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix));
122
+ }
123
+
124
+ function isJiraIssueContentAnchor(anchor: HTMLAnchorElement): boolean {
125
+ if (anchor.closest(`.${ROOT_CLASS}`)) return false;
126
+ return Boolean(
127
+ anchor.closest(
128
+ [
129
+ '[data-testid="issue.views.issue-base.foundation.description.text"]',
130
+ '[data-testid^="issue.views.issue-base.foundation.description"]',
131
+ '[data-testid^="issue.views.issue-base.common.comment"]',
132
+ '[data-testid^="issue.views.issue-base.common.comments"]',
133
+ '[data-testid*="comment"]',
134
+ ".ak-renderer-document",
135
+ ".ak-renderer-document-container",
136
+ ].join(","),
137
+ ),
138
+ );
139
+ }
140
+
141
+ function isSupportedContentAnchor(anchor: HTMLAnchorElement): boolean {
142
+ return isJiraPage()
143
+ ? isJiraIssueContentAnchor(anchor)
144
+ : isGitHubMarkdownAnchor(anchor);
145
+ }
146
+
118
147
  function previewInsertionTarget(anchor: HTMLAnchorElement): HTMLElement {
119
148
  const block = anchor.closest(
120
149
  [
@@ -145,7 +174,7 @@
145
174
  return;
146
175
  }
147
176
 
148
- const frameId = `clips-github-preview-${++frameCounter}`;
177
+ const frameId = `clips-link-preview-${++frameCounter}`;
149
178
  const wrapper = document.createElement("div");
150
179
  wrapper.className = ROOT_CLASS;
151
180
  wrapper.id = `${frameId}-wrapper`;
@@ -195,7 +224,7 @@
195
224
  for (const anchor of document.querySelectorAll<HTMLAnchorElement>(
196
225
  `a[href]:not([${PROCESSED_ATTR}])`,
197
226
  )) {
198
- if (!isGitHubMarkdownAnchor(anchor)) continue;
227
+ if (!isSupportedContentAnchor(anchor)) continue;
199
228
  const link = parseClipsLink(anchor.href);
200
229
  if (!link) continue;
201
230
  mountPreview(anchor, link);
@@ -212,7 +241,7 @@
212
241
  const data = event.data as
213
242
  | { source?: string; frameId?: string; height?: number }
214
243
  | undefined;
215
- if (data?.source !== "clips-github-preview") return;
244
+ if (data?.source !== "clips-link-preview") return;
216
245
  if (!data.frameId || typeof data.height !== "number") return;
217
246
  const frame = document.getElementById(data.frameId);
218
247
  if (!(frame instanceof HTMLIFrameElement)) return;
@@ -1,6 +1,6 @@
1
1
  import { captureExtensionError, initExtensionSentry } from "./sentry";
2
2
 
3
- initExtensionSentry("github-preview");
3
+ initExtensionSentry("link-preview");
4
4
 
5
5
  const DEFAULT_HOST = "clips.agent-native.com";
6
6
  const CLIPS_HOSTS = new Set(["clips.agent-native.com"]);
@@ -80,7 +80,7 @@ function postHeight(frameId: string): void {
80
80
  const height = document.documentElement.scrollHeight;
81
81
  window.parent.postMessage(
82
82
  {
83
- source: "clips-github-preview",
83
+ source: "clips-link-preview",
84
84
  frameId,
85
85
  height,
86
86
  },
@@ -415,7 +415,7 @@ async function main(): Promise<void> {
415
415
  renderPreview(root, preview, await checkPublicPlayable(preview));
416
416
  } catch (error) {
417
417
  captureExtensionError(error, {
418
- tags: { surface: "github-preview" },
418
+ tags: { surface: "link-preview" },
419
419
  extra: { pageUrl: location.href },
420
420
  });
421
421
  }