@agent-native/core 0.178.0 → 0.178.1-nightly-20260909221943

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 (88) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +1 -0
  3. package/corpus/templates/clips/DESIGN.md +10 -7
  4. package/corpus/templates/clips/actions/list-organization-state.ts +56 -3
  5. package/corpus/templates/clips/actions/list-recordings.ts +2 -0
  6. package/corpus/templates/clips/actions/search-dictations.ts +81 -0
  7. package/corpus/templates/clips/app/components/agent-destination-logos.tsx +50 -0
  8. package/corpus/templates/clips/app/components/bug-report/bug-report-dialog.tsx +60 -0
  9. package/corpus/templates/clips/app/components/bug-report/bug-report-form.tsx +206 -0
  10. package/corpus/templates/clips/app/components/clips-command-menu.tsx +653 -0
  11. package/corpus/templates/clips/app/components/library/empty-state.tsx +52 -17
  12. package/corpus/templates/clips/app/components/library/folder-card.tsx +34 -0
  13. package/corpus/templates/clips/app/components/library/folder-tree.tsx +109 -64
  14. package/corpus/templates/clips/app/components/library/library-action-hrefs.ts +27 -0
  15. package/corpus/templates/clips/app/components/library/library-grid.tsx +295 -103
  16. package/corpus/templates/clips/app/components/library/library-layout.tsx +381 -203
  17. package/corpus/templates/clips/app/components/library/library-primary-actions.tsx +48 -0
  18. package/corpus/templates/clips/app/components/library/page-header.tsx +48 -8
  19. package/corpus/templates/clips/app/components/library/recording-card.tsx +58 -55
  20. package/corpus/templates/clips/app/components/library/search-bar.tsx +5 -19
  21. package/corpus/templates/clips/app/components/library/sidebar-feedback-button.tsx +48 -0
  22. package/corpus/templates/clips/app/components/player/comments-panel.tsx +2 -2
  23. package/corpus/templates/clips/app/components/player/share-dialog.tsx +344 -260
  24. package/corpus/templates/clips/app/components/player/viewer-controls.tsx +2 -2
  25. package/corpus/templates/clips/app/components/recorder/camera-visualizer.tsx +0 -33
  26. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -1
  27. package/corpus/templates/clips/app/components/ui/tabs.tsx +1 -1
  28. package/corpus/templates/clips/app/hooks/use-command-search.ts +53 -0
  29. package/corpus/templates/clips/app/hooks/use-navigation-state.ts +32 -13
  30. package/corpus/templates/clips/app/hooks/use-upload-video-picker.tsx +1 -0
  31. package/corpus/templates/clips/app/i18n/en-US.ts +7 -0
  32. package/corpus/templates/clips/app/lib/agent-share.ts +16 -0
  33. package/corpus/templates/clips/app/lib/command-events.ts +7 -0
  34. package/corpus/templates/clips/app/root.tsx +6 -82
  35. package/corpus/templates/clips/app/routes/_app.dictate.tsx +25 -5
  36. package/corpus/templates/clips/app/routes/_app.library._index.tsx +2 -30
  37. package/corpus/templates/clips/app/routes/_app.library.folder.$folderId.tsx +13 -2
  38. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +4 -17
  39. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +111 -114
  40. package/corpus/templates/clips/app/routes/_app.spaces.$spaceId.folder.$folderId.tsx +25 -2
  41. package/corpus/templates/clips/app/routes/_app.spaces.$spaceId.tsx +14 -61
  42. package/corpus/templates/clips/app/routes/_app.spaces._index.tsx +34 -24
  43. package/corpus/templates/clips/app/routes/_app.trash.tsx +1 -1
  44. package/corpus/templates/clips/app/routes/_app.tsx +2 -4
  45. package/corpus/templates/clips/app/routes/bug-report.tsx +11 -189
  46. package/corpus/templates/clips/app/routes/share.$shareId.tsx +148 -123
  47. package/corpus/templates/clips/changelog/2026-09-08-comments-and-desktop-window-controls.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-09-08-global-command-search.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-09-09-agent-sidebar-fits-below-toolbar.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-09-09-desktop-switches-stay-clear-in-dark-mode.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-09-09-recording-agent-uses-global-panel.md +6 -0
  52. package/corpus/templates/clips/desktop/src/app.tsx +2 -2
  53. package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +5 -3
  54. package/corpus/templates/clips/desktop/src/components/SourceRow.tsx +1 -0
  55. package/corpus/templates/clips/desktop/src/components/Switch.tsx +2 -5
  56. package/corpus/templates/clips/desktop/src/components/ui/switch.tsx +21 -3
  57. package/corpus/templates/clips/desktop/src/hooks/useMediaDevices.ts +72 -52
  58. package/corpus/templates/clips/desktop/src/styles.css +18 -19
  59. package/corpus/templates/clips/desktop/src/tailwind.css +3 -0
  60. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +18 -10
  61. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +3 -5
  62. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +14 -3
  63. package/corpus/templates/design/AGENTS.md +2 -1
  64. package/corpus/templates/design/actions/export-png.ts +119 -0
  65. package/corpus/templates/design/actions/take-design-screenshot.ts +47 -13
  66. package/corpus/templates/design/server/plugins/agent-chat.ts +3 -1
  67. package/corpus/templates/factory/actions/dispatch-factory-item.ts +51 -22
  68. package/corpus/templates/factory/actions/list-triage-items.ts +21 -11
  69. package/corpus/templates/factory/actions/poll-slack-channel.ts +7 -2
  70. package/corpus/templates/factory/changelog/2026-09-09-slack-feedback-keeps-claimed-markers-after-poll.md +6 -0
  71. package/corpus/templates/factory/changelog/2026-09-09-slack-feedback-preserves-started-claimed-threads.md +6 -0
  72. package/corpus/templates/factory/changelog/2026-09-09-slack-feedback-skips-claimed-parents.md +6 -0
  73. package/corpus/templates/factory/server/lib/factory-automation-config.ts +1 -1
  74. package/corpus/templates/factory/server/lib/slack-feedback-prompt.ts +47 -0
  75. package/corpus/templates/factory/server/plugins/agent-chat.ts +5 -2
  76. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +5 -4
  77. package/corpus/templates/factory/server/triage/review-skill-alignment.ts +12 -7
  78. package/corpus/templates/factory/server/triage/slack-review-window.ts +39 -0
  79. package/dist/client/CommandMenu.js +13 -1
  80. package/dist/client/analytics.js +10 -0
  81. package/dist/collab/routes.d.ts +1 -1
  82. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  83. package/dist/notifications/routes.d.ts +2 -2
  84. package/dist/observability/routes.d.ts +5 -5
  85. package/dist/vite/client.js +25 -7
  86. package/dist/vite/sentry-source-maps.d.ts +12 -0
  87. package/dist/vite/sentry-source-maps.js +105 -0
  88. package/package.json +2 -1
@@ -18,9 +18,7 @@ import {
18
18
  IconExternalLink,
19
19
  IconLink,
20
20
  IconMail,
21
- IconMessage,
22
21
  IconPhoto,
23
- IconRefresh,
24
22
  IconShare3,
25
23
  } from "@tabler/icons-react";
26
24
  import {
@@ -33,6 +31,11 @@ import {
33
31
  } from "react";
34
32
  import { toast } from "sonner";
35
33
 
34
+ import {
35
+ ClaudeCodeLogo,
36
+ ClaudeLogo,
37
+ CodexLogo,
38
+ } from "@/components/agent-destination-logos";
36
39
  import {
37
40
  PageHeaderActionGroup,
38
41
  PageHeaderPrimaryAction,
@@ -64,7 +67,7 @@ import {
64
67
  PopoverTrigger,
65
68
  PopoverContent,
66
69
  } from "@/components/ui/popover";
67
- import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
70
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
68
71
  import {
69
72
  Tooltip,
70
73
  TooltipContent,
@@ -76,6 +79,10 @@ import { buildAgentApiUrls } from "../../../shared/agent-context";
76
79
  import { buildEmailPreviewMarkup } from "../../../shared/email-preview";
77
80
  import { withShareAttribution } from "../../../shared/share-attribution";
78
81
  import { preferredThumbnailVariant } from "../../../shared/share-meta";
82
+ import {
83
+ buildAgentShareDeepLink,
84
+ type AgentShareDestination,
85
+ } from "../../lib/agent-share";
79
86
  import { buildSocialShareUrl } from "../../lib/social-share";
80
87
  import { ViewerSwitch } from "./viewer-controls";
81
88
 
@@ -216,7 +223,7 @@ export function ShareRecordingPopover({
216
223
  <PopoverContent
217
224
  align="end"
218
225
  {...nestedLayerDismissGuards()}
219
- className="z-[260] w-[400px] max-w-[calc(100vw-1rem)] overflow-hidden border-border p-0"
226
+ className="z-[260] w-[360px] max-w-[calc(100vw-1rem)] overflow-hidden border-border p-0"
220
227
  >
221
228
  <ShareRecordingContent
222
229
  recordingId={recordingId}
@@ -259,7 +266,7 @@ export function ShareRecordingDialog({
259
266
  const t = useT();
260
267
  return (
261
268
  <Dialog open={open} onOpenChange={onOpenChange}>
262
- <DialogContent className="w-[calc(100vw-2rem)] overflow-hidden border-border p-0 sm:max-w-[400px] [&>button]:top-1.5">
269
+ <DialogContent className="w-[calc(100vw-2rem)] overflow-hidden border-border p-0 sm:max-w-[360px] [&>button]:top-1.5">
263
270
  <DialogTitle className="sr-only">
264
271
  {recordingTitle
265
272
  ? t("shareDialog.sharePlainTitle", { title: recordingTitle })
@@ -314,6 +321,7 @@ function ShareRecordingContent({
314
321
  }) {
315
322
  const t = useT();
316
323
  const [view, setView] = useState<"main" | "social" | "embed">("main");
324
+ const [shareMode, setShareMode] = useState<"people" | "agents">("people");
317
325
  const [passwordProtected, setPasswordProtected] = useState(
318
326
  Boolean(hasPassword),
319
327
  );
@@ -326,6 +334,11 @@ function ShareRecordingContent({
326
334
  useEffect(() => {
327
335
  setCurrentExpiry(expiresAt ?? null);
328
336
  }, [expiresAt]);
337
+
338
+ useEffect(() => {
339
+ setShareMode("people");
340
+ }, [recordingId]);
341
+
329
342
  const sharesQuery = useActionQuery<SharesResponse>(
330
343
  "list-resource-shares",
331
344
  { resourceType: "recording", resourceId: recordingId },
@@ -378,65 +391,109 @@ function ShareRecordingContent({
378
391
  : view === "embed"
379
392
  ? t("shareDialog.embed")
380
393
  : t("shareDialog.shareRecording");
394
+ const peopleTab = (
395
+ <PeopleTab
396
+ recordingId={recordingId}
397
+ sharesQuery={sharesQuery}
398
+ visibility={visibility}
399
+ visibilityPending={visibilityPending}
400
+ onVisibilityChange={setResourceVisibility}
401
+ canManage={canManage}
402
+ hasPassword={passwordProtected}
403
+ expiresAt={currentExpiry}
404
+ onPasswordChange={setPasswordProtected}
405
+ onExpiryChange={setCurrentExpiry}
406
+ canViewShares={canViewShares}
407
+ viewerReshareOnly={viewerReshareOnly}
408
+ canEmbed={canEmbed}
409
+ onOpenSocial={() => setView("social")}
410
+ onOpenEmbed={() => setView("embed")}
411
+ />
412
+ );
381
413
 
382
414
  return (
383
415
  <div className="min-w-0">
384
- <div
385
- className={cn(
386
- "flex h-10 items-center gap-2 border-b border-border px-3",
387
- reserveCloseButton && "pe-10",
388
- )}
389
- >
390
- {view === "main" ? (
391
- <span className="min-w-0 flex-1 truncate text-sm font-semibold">
392
- {viewTitle}
393
- </span>
394
- ) : (
395
- <Button
396
- type="button"
397
- variant="ghost"
398
- size="sm"
399
- className="-ms-1 h-7 min-w-0 gap-1.5 px-1.5"
400
- onClick={() => setView("main")}
401
- >
402
- <IconArrowLeft className="size-3.5 shrink-0" />
403
- <span className="truncate font-semibold">{viewTitle}</span>
404
- </Button>
405
- )}
406
- {view === "main" && showHeaderCopy ? (
407
- <CopyButton
408
- value={shareUrl}
409
- disabled={visibilityPending || !sharesLoaded}
410
- variant="ghost"
411
- className="h-7 shrink-0 px-2 text-xs text-primary hover:text-primary"
412
- resourceType="recording"
413
- resourceId={recordingId}
414
- linkType="share"
415
- >
416
- {t("shareUi.copyLink")}
417
- </CopyButton>
418
- ) : null}
419
- </div>
416
+ {view !== "main" || reserveCloseButton ? (
417
+ <div
418
+ className={cn(
419
+ "flex h-10 items-center gap-2 border-b border-border px-3",
420
+ reserveCloseButton && "pe-10",
421
+ )}
422
+ >
423
+ {view === "main" ? (
424
+ <span className="min-w-0 flex-1 truncate text-sm font-semibold">
425
+ {viewTitle}
426
+ </span>
427
+ ) : (
428
+ <Button
429
+ type="button"
430
+ variant="ghost"
431
+ size="sm"
432
+ className="-ms-1 h-7 min-w-0 gap-1.5 px-1.5"
433
+ onClick={() => setView("main")}
434
+ >
435
+ <IconArrowLeft className="size-3.5 shrink-0" />
436
+ <span className="truncate font-semibold">{viewTitle}</span>
437
+ </Button>
438
+ )}
439
+ {view === "main" && showHeaderCopy ? (
440
+ <CopyButton
441
+ value={shareUrl}
442
+ disabled={visibilityPending || !sharesLoaded}
443
+ variant="ghost"
444
+ className="h-7 shrink-0 px-2 text-xs text-primary hover:text-primary"
445
+ resourceType="recording"
446
+ resourceId={recordingId}
447
+ linkType="share"
448
+ >
449
+ {t("shareUi.copyLink")}
450
+ </CopyButton>
451
+ ) : null}
452
+ </div>
453
+ ) : null}
420
454
 
421
455
  <div className="px-3 py-2">
422
456
  {view === "main" ? (
423
- <LinkTab
424
- recordingId={recordingId}
425
- sharesQuery={sharesQuery}
426
- visibility={visibility}
427
- visibilityPending={visibilityPending}
428
- onVisibilityChange={setResourceVisibility}
429
- canManage={canManage}
430
- hasPassword={passwordProtected}
431
- expiresAt={currentExpiry}
432
- onPasswordChange={setPasswordProtected}
433
- onExpiryChange={setCurrentExpiry}
434
- canViewShares={canViewShares}
435
- viewerReshareOnly={viewerReshareOnly}
436
- canEmbed={canEmbed}
437
- onOpenSocial={() => setView("social")}
438
- onOpenEmbed={() => setView("embed")}
439
- />
457
+ viewerReshareOnly ? (
458
+ peopleTab
459
+ ) : (
460
+ <Tabs
461
+ value={shareMode}
462
+ onValueChange={(value) =>
463
+ setShareMode(value as "people" | "agents")
464
+ }
465
+ className="gap-3"
466
+ >
467
+ <TabsList
468
+ variant="line"
469
+ className="h-8 w-full justify-start gap-1 rounded-none px-0 py-0"
470
+ >
471
+ <TabsTrigger
472
+ value="people"
473
+ className="h-8 min-w-0 flex-none rounded-none px-2 py-0 text-sm data-[state=active]:after:bottom-0 data-[state=active]:after:inset-x-2"
474
+ >
475
+ {t("shareDialog.people")}
476
+ </TabsTrigger>
477
+ <TabsTrigger
478
+ value="agents"
479
+ className="h-8 min-w-0 flex-none rounded-none px-2 py-0 text-sm data-[state=active]:after:bottom-0 data-[state=active]:after:inset-x-2"
480
+ >
481
+ {t("shareDialog.agents")}
482
+ </TabsTrigger>
483
+ </TabsList>
484
+ <TabsContent value="people" className="m-0">
485
+ {peopleTab}
486
+ </TabsContent>
487
+ <TabsContent value="agents" className="m-0">
488
+ <AgentTab
489
+ recordingId={recordingId}
490
+ visibility={visibility}
491
+ hasPassword={passwordProtected}
492
+ active={shareMode === "agents"}
493
+ />
494
+ </TabsContent>
495
+ </Tabs>
496
+ )
440
497
  ) : view === "social" ? (
441
498
  <SocialTab
442
499
  shareUrl={shareUrl}
@@ -488,7 +545,7 @@ function ShareOptionRow({
488
545
  // Primary view — invite, access, and progressively disclosed destinations
489
546
  // ---------------------------------------------------------------------------
490
547
 
491
- function LinkTab({
548
+ function PeopleTab({
492
549
  recordingId,
493
550
  sharesQuery,
494
551
  visibility,
@@ -525,8 +582,130 @@ function LinkTab({
525
582
  onOpenEmbed: () => void;
526
583
  }) {
527
584
  const t = useT();
585
+
586
+ if (viewerReshareOnly) {
587
+ return (
588
+ <div className="-mx-1.5 flex flex-col">
589
+ <ShareOptionRow
590
+ icon={<IconShare3 className="size-3.5" />}
591
+ label={t("shareDialog.social")}
592
+ onClick={onOpenSocial}
593
+ />
594
+ {canEmbed ? (
595
+ <ShareOptionRow
596
+ icon={<IconExternalLink className="size-3.5" />}
597
+ label={t("shareDialog.embed")}
598
+ onClick={onOpenEmbed}
599
+ />
600
+ ) : null}
601
+ </div>
602
+ );
603
+ }
604
+
605
+ return (
606
+ <div className="flex flex-col gap-3">
607
+ <div className="flex flex-col gap-3">
608
+ {/* `share-resource` requires owner/admin, so anyone else would only
609
+ get a rejected submission. */}
610
+ {canManage ? (
611
+ <InvitePeopleField
612
+ resourceType="recording"
613
+ resourceId={recordingId}
614
+ resourceUrl={absoluteAppUrl(`/r/${recordingId}`)}
615
+ sharesQuery={sharesQuery}
616
+ onError={(err) =>
617
+ toast.error(
618
+ err instanceof Error
619
+ ? err.message
620
+ : t("clipsFinalRaw.inviteFailed"),
621
+ )
622
+ }
623
+ />
624
+ ) : null}
625
+
626
+ {canViewShares ? (
627
+ visibility ? (
628
+ <div className="flex flex-col gap-1.5">
629
+ <ShareSectionLabel>{t("shareUi.whoHasAccess")}</ShareSectionLabel>
630
+ <div className="flex flex-col gap-0.5">
631
+ <PeopleAccessSection
632
+ resourceType="recording"
633
+ resourceId={recordingId}
634
+ sharesQuery={sharesQuery}
635
+ canManage={canManage}
636
+ roleCopy={{
637
+ commenter: {
638
+ label: t("shareUi.recordingCommenter.label"),
639
+ description: t("shareUi.recordingCommenter.description"),
640
+ },
641
+ }}
642
+ onError={(err, action) =>
643
+ toast.error(
644
+ err instanceof Error
645
+ ? err.message
646
+ : action === "permission"
647
+ ? t("clipsFinalRaw.permissionUpdateFailed")
648
+ : t("clipsFinalRaw.removePersonFailed"),
649
+ )
650
+ }
651
+ />
652
+ <GeneralAccessSelect
653
+ visibility={visibility}
654
+ canManage={canManage}
655
+ isPending={visibilityPending}
656
+ onChange={onVisibilityChange}
657
+ />
658
+ <RecordingAccessControls
659
+ recordingId={recordingId}
660
+ hasPassword={Boolean(hasPassword)}
661
+ expiresAt={expiresAt}
662
+ canEdit={canViewShares}
663
+ onPasswordChange={onPasswordChange}
664
+ onExpiryChange={onExpiryChange}
665
+ />
666
+ </div>
667
+ </div>
668
+ ) : (
669
+ <div className="flex flex-col gap-1.5" aria-hidden>
670
+ <div className="h-3 w-24 animate-pulse rounded bg-muted" />
671
+ <div className="h-8 w-full animate-pulse rounded bg-muted" />
672
+ <div className="h-8 w-full animate-pulse rounded bg-muted" />
673
+ </div>
674
+ )
675
+ ) : null}
676
+ </div>
677
+
678
+ <div className="-mx-3 border-t border-border px-1.5 pt-1.5">
679
+ <ShareOptionRow
680
+ icon={<IconShare3 className="size-3.5" />}
681
+ label={t("shareDialog.social")}
682
+ onClick={onOpenSocial}
683
+ />
684
+ {canEmbed ? (
685
+ <ShareOptionRow
686
+ icon={<IconExternalLink className="size-3.5" />}
687
+ label={t("shareDialog.embed")}
688
+ onClick={onOpenEmbed}
689
+ />
690
+ ) : null}
691
+ </div>
692
+ </div>
693
+ );
694
+ }
695
+
696
+ function AgentTab({
697
+ recordingId,
698
+ visibility,
699
+ hasPassword,
700
+ active,
701
+ }: {
702
+ recordingId: string;
703
+ visibility: Visibility | null;
704
+ hasPassword?: boolean;
705
+ active: boolean;
706
+ }) {
707
+ const t = useT();
528
708
  const isPublic = visibility === "public";
529
- const sharesLoaded = visibility !== null;
530
709
  const needsScopedAgentContext = !isPublic || hasPassword !== false;
531
710
  const publicAgentContextUrl = useMemo(
532
711
  () =>
@@ -542,7 +721,6 @@ function LinkTab({
542
721
  const agentLinkRequestIdRef = useRef(0);
543
722
  const [agentContextUrl, setAgentContextUrl] = useState("");
544
723
  const [agentLinkError, setAgentLinkError] = useState(false);
545
- const [agentShareOpen, setAgentShareOpen] = useState(false);
546
724
 
547
725
  useEffect(() => {
548
726
  createAgentLinkAsyncRef.current = createAgentLink.mutateAsync;
@@ -551,7 +729,6 @@ function LinkTab({
551
729
  const loadAgentContextUrl = useCallback(async () => {
552
730
  const requestId = agentLinkRequestIdRef.current + 1;
553
731
  agentLinkRequestIdRef.current = requestId;
554
-
555
732
  setAgentContextUrl("");
556
733
  setAgentLinkError(false);
557
734
 
@@ -573,32 +750,27 @@ function LinkTab({
573
750
  }, [recordingId]);
574
751
 
575
752
  useEffect(() => {
753
+ if (!active) {
754
+ agentLinkRequestIdRef.current += 1;
755
+ return;
756
+ }
757
+
576
758
  setAgentContextUrl("");
577
759
  setAgentLinkError(false);
578
- if (!sharesLoaded || !agentShareOpen) return;
579
-
580
- if (needsScopedAgentContext) {
760
+ if (visibility !== null && needsScopedAgentContext) {
581
761
  void loadAgentContextUrl();
582
762
  }
583
763
 
584
764
  return () => {
585
765
  agentLinkRequestIdRef.current += 1;
586
766
  };
587
- }, [
588
- loadAgentContextUrl,
589
- needsScopedAgentContext,
590
- agentShareOpen,
591
- recordingId,
592
- sharesLoaded,
593
- visibility,
594
- ]);
767
+ }, [active, loadAgentContextUrl, needsScopedAgentContext, visibility]);
595
768
 
596
769
  const agentLink = isPublic
597
770
  ? publicAgentContextUrl || agentContextUrl
598
771
  : agentContextUrl;
599
772
  const agentShareDisabled =
600
- visibilityPending ||
601
- !sharesLoaded ||
773
+ visibility === null ||
602
774
  !agentLink ||
603
775
  (needsScopedAgentContext &&
604
776
  (createAgentLink.isPending || !agentContextUrl));
@@ -606,150 +778,95 @@ function LinkTab({
606
778
  ? t("shareDialog.agentPrompt", { agentContextUrl: agentLink })
607
779
  : "";
608
780
 
609
- if (viewerReshareOnly) return null;
781
+ const openAgentDestination = (destination: AgentShareDestination) => {
782
+ if (
783
+ agentShareDisabled ||
784
+ !agentCopyValue ||
785
+ typeof window === "undefined"
786
+ ) {
787
+ return;
788
+ }
610
789
 
611
- return (
612
- <div className="space-y-3">
613
- {/* `share-resource` requires owner/admin, so anyone else would only get
614
- a rejected submission. */}
615
- {canManage ? (
616
- <InvitePeopleField
617
- resourceType="recording"
618
- resourceId={recordingId}
619
- resourceUrl={absoluteAppUrl(`/r/${recordingId}`)}
620
- sharesQuery={sharesQuery}
621
- onError={(err) =>
622
- toast.error(
623
- err instanceof Error
624
- ? err.message
625
- : t("clipsFinalRaw.inviteFailed"),
626
- )
627
- }
628
- />
629
- ) : null}
790
+ trackEvent("agent_share_opened", {
791
+ resource_type: "recording",
792
+ resource_id: recordingId,
793
+ destination,
794
+ });
795
+ window.location.assign(
796
+ buildAgentShareDeepLink(destination, agentCopyValue),
797
+ );
798
+ };
630
799
 
631
- {canViewShares ? (
632
- visibility ? (
633
- <div className="space-y-1.5">
634
- <ShareSectionLabel>{t("shareUi.whoHasAccess")}</ShareSectionLabel>
635
- <div className="flex flex-col gap-0.5">
636
- <PeopleAccessSection
637
- resourceType="recording"
638
- resourceId={recordingId}
639
- sharesQuery={sharesQuery}
640
- canManage={canManage}
641
- roleCopy={{
642
- commenter: {
643
- label: t("shareUi.recordingCommenter.label"),
644
- description: t("shareUi.recordingCommenter.description"),
645
- },
646
- }}
647
- onError={(err, action) =>
648
- toast.error(
649
- err instanceof Error
650
- ? err.message
651
- : action === "permission"
652
- ? t("clipsFinalRaw.permissionUpdateFailed")
653
- : t("clipsFinalRaw.removePersonFailed"),
654
- )
655
- }
656
- />
657
- <GeneralAccessSelect
658
- visibility={visibility}
659
- canManage={canManage}
660
- isPending={visibilityPending}
661
- onChange={onVisibilityChange}
662
- />
663
- <RecordingAccessControls
664
- recordingId={recordingId}
665
- hasPassword={Boolean(hasPassword)}
666
- expiresAt={expiresAt}
667
- canEdit={canViewShares}
668
- onPasswordChange={onPasswordChange}
669
- onExpiryChange={onExpiryChange}
670
- />
671
- </div>
672
- </div>
673
- ) : (
674
- <div className="space-y-1.5" aria-hidden>
675
- <div className="h-3 w-24 animate-pulse rounded bg-muted" />
676
- <div className="h-8 w-full animate-pulse rounded bg-muted" />
677
- <div className="h-8 w-full animate-pulse rounded bg-muted" />
678
- </div>
679
- )
680
- ) : null}
800
+ const copyAgentPrompt = async () => {
801
+ if (agentShareDisabled || !agentCopyValue) return;
802
+ const copied = await writeClipboardText(agentCopyValue);
803
+ if (copied === false) return;
681
804
 
682
- <div className="-mx-1.5 border-t border-border pt-1.5">
683
- <ShareOptionRow
684
- icon={<IconShare3 className="size-3.5" />}
685
- label={t("shareDialog.social")}
686
- onClick={onOpenSocial}
687
- />
688
- {canEmbed ? (
689
- <ShareOptionRow
690
- icon={<IconExternalLink className="size-3.5" />}
691
- label={t("shareDialog.embed")}
692
- onClick={onOpenEmbed}
693
- />
694
- ) : null}
695
- </div>
805
+ trackEvent("share_link_copied", {
806
+ resource_type: "recording",
807
+ resource_id: recordingId,
808
+ link_type: "agent_context",
809
+ });
810
+ toast.success(t("shareUi.copied"));
811
+ };
696
812
 
697
- <Collapsible open={agentShareOpen} onOpenChange={setAgentShareOpen}>
698
- <CollapsibleTrigger asChild>
699
- <Button
700
- type="button"
701
- variant="ghost"
702
- className="h-8 w-full justify-between px-1.5 text-sm font-normal"
703
- >
704
- <span className="flex min-w-0 items-center gap-2">
705
- <IconMessage className="size-3.5 shrink-0 text-muted-foreground" />
706
- <span className="truncate">
707
- {t("shareDialog.shareWithAgents")}
708
- </span>
709
- </span>
710
- <IconChevronDown
711
- className={cn(
712
- "size-3.5 text-muted-foreground transition-transform",
713
- agentShareOpen && "rotate-180",
714
- )}
715
- />
716
- </Button>
717
- </CollapsibleTrigger>
718
- <CollapsibleContent className="px-1.5 pb-1 pt-1.5">
719
- <div className="flex items-center justify-between gap-4">
720
- <p className="text-xs text-muted-foreground">
721
- {/* A public, unprotected clip hands agents its permanent public
722
- context URL; everything else gets a short-lived scoped token. */}
723
- {needsScopedAgentContext
724
- ? t("shareDialog.agentTokenDescription")
725
- : t("shareDialog.agentPublicDescription")}
726
- </p>
727
- {agentLinkError ? (
728
- <Button
729
- type="button"
730
- variant="outline"
731
- size="sm"
732
- className="shrink-0"
733
- onClick={() => void loadAgentContextUrl()}
734
- disabled={createAgentLink.isPending}
735
- >
736
- {t("shareDialog.retryAgentLink")}
737
- </Button>
738
- ) : (
739
- <CopyButton
740
- value={agentCopyValue}
741
- disabled={agentShareDisabled}
742
- className="shrink-0"
743
- resourceType="recording"
744
- resourceId={recordingId}
745
- linkType="agent_context"
746
- >
747
- {t("shareUi.copy")}
748
- </CopyButton>
749
- )}
750
- </div>
751
- </CollapsibleContent>
752
- </Collapsible>
813
+ if (agentLinkError) {
814
+ return (
815
+ <Button
816
+ type="button"
817
+ variant="ghost"
818
+ className="h-9 w-full justify-start px-1.5 text-sm font-normal"
819
+ onClick={() => void loadAgentContextUrl()}
820
+ disabled={createAgentLink.isPending}
821
+ >
822
+ {t("shareDialog.retryAgentLink")}
823
+ </Button>
824
+ );
825
+ }
826
+
827
+ return (
828
+ <div className="-mx-1.5 flex flex-col gap-0.5">
829
+ <Button
830
+ type="button"
831
+ variant="ghost"
832
+ className="h-9 w-full justify-start gap-2 px-1.5 text-sm font-normal"
833
+ disabled={agentShareDisabled}
834
+ onClick={() => void copyAgentPrompt()}
835
+ >
836
+ <IconLink className="size-4 text-muted-foreground" />
837
+ {t("shareDialog.copyAgentPrompt")}
838
+ </Button>
839
+ <div className="my-1 border-t border-border" />
840
+ <Button
841
+ type="button"
842
+ variant="ghost"
843
+ className="h-9 w-full justify-start gap-2 px-1.5 text-sm font-normal"
844
+ disabled={agentShareDisabled}
845
+ onClick={() => openAgentDestination("claude")}
846
+ >
847
+ <ClaudeLogo className="size-4 text-muted-foreground" />
848
+ {t("shareDialog.openInClaude")}
849
+ </Button>
850
+ <Button
851
+ type="button"
852
+ variant="ghost"
853
+ className="h-9 w-full justify-start gap-2 px-1.5 text-sm font-normal"
854
+ disabled={agentShareDisabled}
855
+ onClick={() => openAgentDestination("claude-code")}
856
+ >
857
+ <ClaudeCodeLogo className="size-4 text-muted-foreground" />
858
+ {t("shareDialog.openInClaudeCode")}
859
+ </Button>
860
+ <Button
861
+ type="button"
862
+ variant="ghost"
863
+ className="h-9 w-full justify-start gap-2 px-1.5 text-sm font-normal"
864
+ disabled={agentShareDisabled}
865
+ onClick={() => openAgentDestination("codex")}
866
+ >
867
+ <CodexLogo className="size-4 text-muted-foreground" />
868
+ {t("shareDialog.openInCodex")}
869
+ </Button>
753
870
  </div>
754
871
  );
755
872
  }
@@ -799,32 +916,18 @@ function RecordingAccessControls({
799
916
  <div className="flex flex-col gap-0.5">
800
917
  <div className="rounded-md px-1.5 py-1">
801
918
  <div className="flex min-h-8 items-center gap-2">
802
- <div className="flex min-w-0 flex-1 items-center gap-2">
803
- <ViewerSwitch
804
- id="share-password-required"
805
- checked={passwordEnabled}
806
- disabled={!canEdit || updateRecording.isPending}
807
- onCheckedChange={setPasswordRequired}
808
- />
809
- <Label
810
- htmlFor="share-password-required"
811
- className="cursor-pointer text-sm font-normal"
812
- >
813
- {t("embedRoute.passwordRequired")}
814
- </Label>
815
- </div>
816
- {passwordEnabled && canEdit ? (
817
- <Button
818
- type="button"
819
- variant="ghost"
820
- size="sm"
821
- className="h-7 shrink-0 gap-1.5 px-2 text-xs"
822
- onClick={() => setPassword(generateSecurePassword())}
823
- >
824
- <IconRefresh className="size-3.5" />
825
- {t("playerSettings.generatePassword")}
826
- </Button>
827
- ) : null}
919
+ <Label
920
+ htmlFor="share-password-required"
921
+ className="min-w-0 flex-1 cursor-pointer text-sm font-normal"
922
+ >
923
+ {t("embedRoute.passwordRequired")}
924
+ </Label>
925
+ <ViewerSwitch
926
+ id="share-password-required"
927
+ checked={passwordEnabled}
928
+ disabled={!canEdit || updateRecording.isPending}
929
+ onCheckedChange={setPasswordRequired}
930
+ />
828
931
  </div>
829
932
 
830
933
  {passwordEnabled ? (
@@ -957,25 +1060,6 @@ function formatExpiry(iso: string | null): string {
957
1060
  }).format(date);
958
1061
  }
959
1062
 
960
- export function generateSecurePassword(length = 20): string {
961
- const alphabet =
962
- "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789-._~";
963
- const limit = Math.floor(256 / alphabet.length) * alphabet.length;
964
- const bytes = new Uint8Array(length * 2);
965
- let password = "";
966
-
967
- while (password.length < length) {
968
- crypto.getRandomValues(bytes);
969
- for (const byte of bytes) {
970
- if (byte >= limit) continue;
971
- password += alphabet[byte % alphabet.length];
972
- if (password.length === length) break;
973
- }
974
- }
975
-
976
- return password;
977
- }
978
-
979
1063
  // ---------------------------------------------------------------------------
980
1064
  // Social tab — destination-first share intents, no provider account required
981
1065
  // ---------------------------------------------------------------------------