@agent-native/core-corpus 0.222.1 → 0.222.2-nightly-20261029182928

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 (96) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  3. package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  4. package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  5. package/corpus/templates/brain/server/plugins/auth.ts +1 -1
  6. package/corpus/templates/brain/server/routes/api/brain/queue/run.post.ts +1 -7
  7. package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  8. package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  9. package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  10. package/corpus/templates/clips/app/components/clips-command-menu.tsx +12 -7
  11. package/corpus/templates/clips/app/components/import-menu.tsx +4 -1
  12. package/corpus/templates/clips/app/components/library/library-grid.tsx +15 -8
  13. package/corpus/templates/clips/app/components/library/library-layout.tsx +10 -2
  14. package/corpus/templates/clips/app/components/player/share-dialog.tsx +4 -7
  15. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +1 -1
  16. package/corpus/templates/clips/app/routes/_app.screenshots.tsx +16 -0
  17. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +12 -1
  18. package/corpus/templates/clips/changelog/2026-10-28-fixed-the-share-advanced-popover-so-the-save-buttons-fit-and.md +6 -0
  19. package/corpus/templates/clips/changelog/2026-10-29-screenshots-are-available-when-enabled-in-labs.md +6 -0
  20. package/corpus/templates/clips/server/lib/content-clip-peer.ts +158 -28
  21. package/corpus/templates/clips/shared/content-clip-contract.ts +23 -1
  22. package/corpus/templates/clips/shared/labs.ts +9 -0
  23. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +8 -0
  24. package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  25. package/corpus/templates/content/actions/_clip-embed-access.ts +31 -1
  26. package/corpus/templates/content/actions/add-clip-comment.ts +95 -0
  27. package/corpus/templates/content/actions/execute-builder-source-execution.ts +91 -4
  28. package/corpus/templates/content/actions/list-clip-comments.ts +68 -30
  29. package/corpus/templates/content/actions/resolve-clip-comment.ts +56 -0
  30. package/corpus/templates/content/app/clip-embed-messages.ts +22 -0
  31. package/corpus/templates/content/app/components/editor/CommentStack.tsx +3 -2
  32. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +188 -62
  33. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +87 -10
  34. package/corpus/templates/content/app/components/editor/StaticDocumentBody.tsx +36 -0
  35. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +7 -2
  36. package/corpus/templates/content/app/components/editor/clip-embed/ClipEmbedBlock.tsx +113 -9
  37. package/corpus/templates/content/app/components/editor/clip-embed/ClipPlayer.tsx +33 -1
  38. package/corpus/templates/content/app/components/editor/clip-embed/clip-comments.ts +397 -0
  39. package/corpus/templates/content/app/components/editor/clip-embed/player-controls.tsx +17 -3
  40. package/corpus/templates/content/app/components/editor/clip-embed/scrubber-position.ts +12 -0
  41. package/corpus/templates/content/app/components/editor/clip-embed/scrubber.tsx +8 -13
  42. package/corpus/templates/content/app/components/editor/clip-embed/use-clip-comment-writes.ts +348 -0
  43. package/corpus/templates/content/app/components/editor/clip-embed/use-clip-playback.ts +22 -17
  44. package/corpus/templates/content/app/components/editor/comment-block-anchors.ts +13 -4
  45. package/corpus/templates/content/app/components/editor/comment-stacks.ts +6 -1
  46. package/corpus/templates/content/app/components/editor/discussion-panel.tsx +8 -5
  47. package/corpus/templates/content/app/components/editor/extensions/TablePresentation.ts +20 -2
  48. package/corpus/templates/content/app/components/editor/table-sizing.ts +749 -0
  49. package/corpus/templates/content/app/components/editor/table-view.ts +61 -84
  50. package/corpus/templates/content/app/global.css +119 -6
  51. package/corpus/templates/content/app/hooks/use-comments.ts +26 -7
  52. package/corpus/templates/content/app/lib/private-link-redirect.ts +68 -6
  53. package/corpus/templates/content/app/root.tsx +13 -0
  54. package/corpus/templates/content/app/routes/p.$id.tsx +6 -3
  55. package/corpus/templates/content/changelog/2026-10-22-shareable-local-file-copies-open-inline-with-document-editin.md +6 -0
  56. package/corpus/templates/content/changelog/2026-10-27-a-public-page-s-link-opens-a-read-only-copy-for-people-who-ar.md +6 -0
  57. package/corpus/templates/content/changelog/2026-10-27-two-collections-connected-to-the-same-builder-model-can-no-l.md +6 -0
  58. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +7 -0
  59. package/corpus/templates/content/parity/matrix.md +1 -1
  60. package/corpus/templates/content/parity/matrix.ts +11 -5
  61. package/corpus/templates/content/server/lib/chatgpt-directory-tools.ts +23 -20
  62. package/corpus/templates/content/server/lib/clips-peer.ts +67 -13
  63. package/corpus/templates/content/shared/clips-peer-contract.ts +40 -3
  64. package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  65. package/corpus/templates/design/.agents/skills/hubspot-email/SKILL.md +2 -2
  66. package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  67. package/corpus/templates/design/.generated/bridge/native-builtin-metadata.generated.ts +326 -0
  68. package/corpus/templates/design/.generated/bridge/native-worker-source.generated.ts +6 -0
  69. package/corpus/templates/design/README.md +14 -0
  70. package/corpus/templates/design/actions/enroll-first-output-experiment.ts +99 -0
  71. package/corpus/templates/design/actions/generate-design.ts +19 -16
  72. package/corpus/templates/design/actions/visual-edit-browser-request.ts +23 -1
  73. package/corpus/templates/design/app/hooks/use-agent-generating.ts +13 -0
  74. package/corpus/templates/design/app/lib/agent-run-status.ts +24 -0
  75. package/corpus/templates/design/app/lib/design-funnel-tracking.ts +5 -0
  76. package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +59 -0
  77. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-generation-and-access.ts +6 -0
  78. package/corpus/templates/design/app/pages/design-editor/view/first-output-next-step.tsx +95 -15
  79. package/corpus/templates/design/changelog/2026-10-22-generation-sessions-and-coding-handoffs-open-the-design-inli.md +6 -0
  80. package/corpus/templates/design/server/db/schema.ts +11 -0
  81. package/corpus/templates/design/server/lib/builder-identity-enabled.ts +23 -0
  82. package/corpus/templates/design/server/lib/builder-identity.ts +2 -21
  83. package/corpus/templates/design/server/lib/chatgpt-directory-tools.ts +16 -0
  84. package/corpus/templates/design/server/plugins/builder-host-embed-headers.ts +2 -7
  85. package/corpus/templates/design/server/plugins/db.ts +11 -0
  86. package/corpus/templates/design/shared/builder-host-origins.ts +28 -0
  87. package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  88. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +37 -8
  89. package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  90. package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  91. package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  92. package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  93. package/corpus/templates/slides/changelog/2026-10-29-deck-change-notifications-are-now-only-sent-to-people-who-ca.md +6 -0
  94. package/corpus/templates/slides/server/handlers/decks.ts +70 -24
  95. package/corpus/templates/slides-2/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  96. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 15073
35
+ - template files: 15141
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","analytics-alert-jobs","analytics-rollup-backfill-jobs","auth","collab","config","content-metrics-collection-jobs","core-routes","creative-context","dashboard-report-jobs","db","feature-flags","labs","onboarding","otel","session-replay-retention-jobs","transactional-emails","uptime-monitor-jobs"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","creative-context","db","labs","onboarding","otel"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","brain-jobs","core-routes","db","otel"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -13,5 +13,5 @@ export default createToolkitAuthPlugin({
13
13
  "Review company-wide knowledge through proposal workflows",
14
14
  ],
15
15
  },
16
- publicPaths: ["/api/_agent-native/brain/ingest"],
16
+ publicPaths: ["/api/_agent-native/brain/ingest", "/api/brain/queue/run"],
17
17
  });
@@ -13,12 +13,6 @@ declare global {
13
13
  export const BRAIN_QUEUE_SWEEP_LIMIT = 25;
14
14
  export const BRAIN_MAX_DISTILLATIONS_PER_SWEEP = 2;
15
15
 
16
- function productionLike() {
17
- return (
18
- process.env.NODE_ENV === "production" || process.env.NETLIFY === "true"
19
- );
20
- }
21
-
22
16
  function scheduledFunctionRuntime() {
23
17
  return globalThis.__AGENT_NATIVE_BRAIN_SCHEDULED_RUNTIME__ === true;
24
18
  }
@@ -35,7 +29,7 @@ function headerMatchesSecret(header: string | undefined, secret: string) {
35
29
  export default defineEventHandler(async (event) => {
36
30
  const secret = process.env.BRAIN_JOBS_SECRET?.trim(); // guard:allow-env-credential — deployment-level scheduler route secret
37
31
  const scheduled = scheduledFunctionRuntime();
38
- if (!secret && productionLike() && !scheduled) {
32
+ if (!secret && !scheduled) {
39
33
  throw createError({
40
34
  statusCode: 503,
41
35
  statusMessage: "BRAIN_JOBS_SECRET is required",
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","core-routes","db","event-rules","oauth-refresh","otel","transactional-emails"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","otel"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","calendar-jobs","config","core-routes","db","feature-flags","labs","onboarding","onboarding-calendar","otel","transactional-email-catalog"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -1,8 +1,10 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
+ import { useLab } from "@agent-native/core/client/labs";
2
3
  import { useOrgRole } from "@agent-native/core/client/org";
3
4
  import { docsUrl } from "@agent-native/core/shared";
4
5
  import { useCommandMenuShortcut } from "@agent-native/toolkit/app/shared";
5
6
  import { CommandMenu } from "@agent-native/toolkit/app/shared";
7
+ import { CLIPS_SCREENSHOTS } from "@shared/labs";
6
8
  import {
7
9
  IconArchive,
8
10
  IconPhoto,
@@ -393,6 +395,7 @@ export function ClipsCommandMenu({
393
395
  const location = useLocation();
394
396
  const navigate = useNavigate();
395
397
  const t = useT();
398
+ const screenshotsEnabled = useLab(CLIPS_SCREENSHOTS);
396
399
  const { canManageOrg } = useOrgRole();
397
400
  const context = useMemo(
398
401
  () => commandRouteContext(location.pathname),
@@ -614,13 +617,15 @@ export function ClipsCommandMenu({
614
617
  {t("navigation.dictate")}
615
618
  <CommandMenu.Shortcut>G D</CommandMenu.Shortcut>
616
619
  </CommandMenu.Item>
617
- <CommandMenu.Item
618
- onSelect={() => void navigate("/screenshots")}
619
- keywords={["screenshots", "screenshot", "images", "stills"]}
620
- >
621
- <IconPhoto size={16} />
622
- {t("navigation.screenshots")}
623
- </CommandMenu.Item>
620
+ {screenshotsEnabled && (
621
+ <CommandMenu.Item
622
+ onSelect={() => void navigate("/screenshots")}
623
+ keywords={["screenshots", "screenshot", "images", "stills"]}
624
+ >
625
+ <IconPhoto size={16} />
626
+ {t("navigation.screenshots")}
627
+ </CommandMenu.Item>
628
+ )}
624
629
  <CommandMenu.Item
625
630
  onSelect={() => void navigate("/archive")}
626
631
  keywords={["archive"]}
@@ -1,4 +1,6 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
+ import { useLab } from "@agent-native/core/client/labs";
3
+ import { CLIPS_SCREENSHOTS } from "@shared/labs";
2
4
  import { IconChevronDown, IconUpload } from "@tabler/icons-react";
3
5
  import { useState } from "react";
4
6
 
@@ -63,6 +65,7 @@ export function ImportMenu({
63
65
  variant = "outline",
64
66
  }: ImportMenuProps) {
65
67
  const t = useT();
68
+ const screenshotsEnabled = useLab(CLIPS_SCREENSHOTS);
66
69
  const { input, openUploadPicker } = useUploadVideoPicker();
67
70
  const [loomDialogOpen, setLoomDialogOpen] = useState(false);
68
71
 
@@ -118,7 +121,7 @@ export function ImportMenu({
118
121
  {t("preRecord.uploadVideo")}
119
122
  </DropdownMenuItem>
120
123
  ) : null}
121
- {onScreenshot ? (
124
+ {onScreenshot && screenshotsEnabled ? (
122
125
  <DropdownMenuItem
123
126
  disabled={screenshotPending}
124
127
  onSelect={(event) => {
@@ -6,7 +6,7 @@ import {
6
6
  import { useT } from "@agent-native/core/client/i18n";
7
7
  import { normalizeDocumentTitle } from "@agent-native/core/shared";
8
8
  import { FileStorageSetupPopover } from "@agent-native/toolkit/app/chat/FileStorageSetupPopover";
9
- import { IconAlertTriangle } from "@tabler/icons-react";
9
+ import { IconAlertTriangle, IconPhoto } from "@tabler/icons-react";
10
10
  import {
11
11
  type DragEvent,
12
12
  type ReactElement,
@@ -59,7 +59,7 @@ import { resolveVideoMimeType } from "@/lib/video-metadata";
59
59
 
60
60
  import { BulkActionToolbar, type BulkMoveTarget } from "./bulk-action-toolbar";
61
61
  import { DroppedUploadCard } from "./dropped-upload-card";
62
- import { EmptyState } from "./empty-state";
62
+ import { AppEmptyState, EmptyState } from "./empty-state";
63
63
  import { FilterChips, type FilterChip } from "./filter-chips";
64
64
  import { FolderCard } from "./folder-card";
65
65
  import { buildLibraryActionHrefs } from "./library-action-hrefs";
@@ -757,7 +757,7 @@ export function LibraryGrid({
757
757
  />
758
758
  )}
759
759
  <div className="ms-auto flex shrink-0 items-center gap-2 lg:col-start-3 lg:ms-0 lg:justify-self-end">
760
- {!isEmptyState && extraActions}
760
+ {(!isEmptyState || kind === "image") && extraActions}
761
761
  {!isEmptyState && (
762
762
  <SortMenu value={sort} onChange={handleSortChange} />
763
763
  )}
@@ -834,11 +834,18 @@ export function LibraryGrid({
834
834
  ) : recordings.length === 0 &&
835
835
  visibleFolders.length === 0 &&
836
836
  uploads.length === 0 ? (
837
- <EmptyState
838
- kind={resolvedEmptyKind}
839
- spaceId={spaceId}
840
- folderId={folderId}
841
- />
837
+ kind === "image" ? (
838
+ <AppEmptyState
839
+ icon={IconPhoto}
840
+ title={t("navigation.screenshots")}
841
+ />
842
+ ) : (
843
+ <EmptyState
844
+ kind={resolvedEmptyKind}
845
+ spaceId={spaceId}
846
+ folderId={folderId}
847
+ />
848
+ )
842
849
  ) : (
843
850
  <div className="flex flex-col gap-8">
844
851
  {visibleFolders.length > 0 && (
@@ -12,7 +12,11 @@ import {
12
12
  AppSidebarFooter,
13
13
  AppSidebarHeader,
14
14
  } from "@agent-native/toolkit/app/shared";
15
- import { CLIPS_MEETINGS, CLIPS_VOICE_DICTATION } from "@shared/labs";
15
+ import {
16
+ CLIPS_MEETINGS,
17
+ CLIPS_SCREENSHOTS,
18
+ CLIPS_VOICE_DICTATION,
19
+ } from "@shared/labs";
16
20
  import {
17
21
  IconInbox,
18
22
  IconPhoto,
@@ -195,6 +199,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
195
199
  const t = useT();
196
200
  const isRecordingRoute = location.pathname.startsWith("/r/");
197
201
  const meetingsLabEnabled = useLab(CLIPS_MEETINGS);
202
+ const screenshotsLabEnabled = useLab(CLIPS_SCREENSHOTS);
198
203
  const voiceDictationLabEnabled = useLab(CLIPS_VOICE_DICTATION);
199
204
  const recordingScope = useMemo(() => {
200
205
  const match = location.pathname.match(/^\/(?:r|share)\/([^/]+)/);
@@ -564,7 +569,10 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
564
569
  ];
565
570
 
566
571
  const primaryNavItems = navItems.filter(
567
- ({ to }) => to !== "/archive" && to !== "/trash",
572
+ ({ to }) =>
573
+ to !== "/archive" &&
574
+ to !== "/trash" &&
575
+ (to !== "/screenshots" || screenshotsLabEnabled),
568
576
  );
569
577
  const lifecycleNavItems = navItems.filter(
570
578
  ({ to }) => to === "/archive" || to === "/trash",
@@ -64,6 +64,7 @@ import {
64
64
  PopoverTrigger,
65
65
  PopoverContent,
66
66
  } from "@/components/ui/popover";
67
+ import { Switch } from "@/components/ui/switch";
67
68
  import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
68
69
  import { cn } from "@/lib/utils";
69
70
 
@@ -755,7 +756,7 @@ function RecordingAdvancedAccess({
755
756
  >
756
757
  {t("embedRoute.passwordRequired")}
757
758
  </Label>
758
- <ViewerSwitch
759
+ <Switch
759
760
  id="share-password-required"
760
761
  checked={passwordEnabled}
761
762
  disabled={!canEdit || updateRecording.isPending}
@@ -765,7 +766,7 @@ function RecordingAdvancedAccess({
765
766
 
766
767
  {passwordEnabled ? (
767
768
  <>
768
- <div className="flex gap-2">
769
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
769
770
  <Input
770
771
  size="sm"
771
772
  type="text"
@@ -778,12 +779,10 @@ function RecordingAdvancedAccess({
778
779
  ? t("playerSettings.passwordSetPlaceholder")
779
780
  : t("playerSettings.passwordInputPlaceholder")
780
781
  }
781
- className="min-w-0"
782
782
  />
783
783
  <Button
784
784
  type="button"
785
785
  size="sm"
786
- className="shrink-0"
787
786
  disabled={
788
787
  !canEdit || updateRecording.isPending || !password.trim()
789
788
  }
@@ -815,7 +814,7 @@ function RecordingAdvancedAccess({
815
814
  <Label htmlFor="share-expiry" className="text-sm font-normal">
816
815
  {t("shareDialog.linkExpires")}
817
816
  </Label>
818
- <div className="flex gap-2">
817
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
819
818
  <Input
820
819
  id="share-expiry"
821
820
  size="sm"
@@ -825,12 +824,10 @@ function RecordingAdvancedAccess({
825
824
  onChange={(event) =>
826
825
  setExpiryDraft(fromDatetimeLocal(event.target.value))
827
826
  }
828
- className="min-w-0"
829
827
  />
830
828
  <Button
831
829
  type="button"
832
830
  size="sm"
833
- className="shrink-0"
834
831
  disabled={!canEdit || updateRecording.isPending}
835
832
  onClick={() => {
836
833
  const nextExpiry = expiryDraft || null;
@@ -92,7 +92,7 @@ export function ShareAdvancedPopover({
92
92
  align="start"
93
93
  sideOffset={6}
94
94
  onOpenAutoFocus={(event) => event.preventDefault()}
95
- className="w-72 space-y-3 p-3"
95
+ className="w-80 max-w-[calc(100vw-2rem)] space-y-3 p-3"
96
96
  >
97
97
  {children}
98
98
  </PopoverContent>
@@ -1,7 +1,11 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
+ import { useLabState } from "@agent-native/core/client/labs";
3
+ import { CLIPS_SCREENSHOTS } from "@shared/labs";
4
+ import { Navigate } from "react-router";
2
5
 
3
6
  import { LibraryGrid } from "@/components/library/library-grid";
4
7
  import { ScreenshotPrimaryAction } from "@/components/library/library-primary-actions";
8
+ import { Button } from "@/components/ui/button";
5
9
  import enMessages from "@/i18n/en-US";
6
10
 
7
11
  export function meta() {
@@ -16,6 +20,18 @@ export function meta() {
16
20
  */
17
21
  export default function ScreenshotsRoute() {
18
22
  const t = useT();
23
+ const lab = useLabState(CLIPS_SCREENSHOTS.key);
24
+ if (lab.isLoading) return null;
25
+ if (lab.isError) {
26
+ return (
27
+ <div role="alert" className="flex flex-col items-center gap-4 py-20">
28
+ <p>{t("sharePage.somethingWentWrong")}</p>
29
+ <Button onClick={lab.refetch}>{t("libraryGrid.retry")}</Button>
30
+ </div>
31
+ );
32
+ }
33
+ if (!lab.isSuccess) return null;
34
+ if (!lab.enabled) return <Navigate replace to="/library" />;
19
35
  return (
20
36
  <LibraryGrid
21
37
  view="library"
@@ -4,7 +4,11 @@ import {
4
4
  SettingsTabsPage,
5
5
  useAgentSettingsTabs,
6
6
  } from "@agent-native/toolkit/app/settings";
7
- import { CLIPS_LABS, CLIPS_VOICE_DICTATION } from "@shared/labs";
7
+ import {
8
+ CLIPS_LABS,
9
+ CLIPS_SCREENSHOTS,
10
+ CLIPS_VOICE_DICTATION,
11
+ } from "@shared/labs";
8
12
  import { useMemo } from "react";
9
13
 
10
14
  import { useClipsSettingsRedesign } from "@/components/settings/clips-settings-redesign";
@@ -54,6 +58,13 @@ export default function SettingsIndexRoute() {
54
58
  description: t("settings.labLookbackContextDescription"),
55
59
  };
56
60
  }
61
+ if (lab.key === CLIPS_SCREENSHOTS.key) {
62
+ return {
63
+ ...lab,
64
+ displayName: t("navigation.screenshots"),
65
+ description: t("preRecord.takeScreenshot"),
66
+ };
67
+ }
57
68
  if (lab.key === CLIPS_VOICE_DICTATION.key) {
58
69
  return {
59
70
  ...lab,
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-28
4
+ ---
5
+
6
+ Fixed the Share → Advanced popover so the Save buttons fit and the password toggle matches other switches
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: changed
3
+ date: 2026-10-29
4
+ ---
5
+
6
+ Screenshots are available when enabled in Labs.
@@ -13,7 +13,18 @@ import {
13
13
  } from "@agent-native/core/server";
14
14
  import { ForbiddenError, resolveAccess } from "@agent-native/core/sharing";
15
15
  import { isEmailDerivedName } from "@agent-native/core/user-profile";
16
- import { and, asc, eq, isNull, sql } from "drizzle-orm";
16
+ import {
17
+ and,
18
+ asc,
19
+ count,
20
+ eq,
21
+ inArray,
22
+ isNotNull,
23
+ isNull,
24
+ sql,
25
+ type SQL,
26
+ } from "drizzle-orm";
27
+ import { alias } from "drizzle-orm/pg-core";
17
28
  import { getQuery, getRequestHeader, getRouterParam, type H3Event } from "h3";
18
29
  import { z } from "zod";
19
30
 
@@ -29,8 +40,13 @@ import {
29
40
  CONTENT_CLIP_CLAIMS,
30
41
  CONTENT_CLIP_COMMENT_AUTHOR_CHARS,
31
42
  CONTENT_CLIP_COMMENT_PREVIEW_CHARS,
43
+ CONTENT_CLIP_COMMENT_TEXT_CHARS,
44
+ CONTENT_CLIP_MAX_COMMENT_CHARS,
32
45
  CONTENT_CLIP_MAX_COMMENT_THREADS,
46
+ CONTENT_CLIP_MAX_LISTED_REPLIES,
33
47
  CONTENT_CLIP_MAX_TOKEN_SECONDS,
48
+ CONTENT_CLIP_MAX_VIDEO_TIMESTAMP_MS,
49
+ CONTENT_CLIP_OPEN_THREADS_VIEW,
34
50
  CONTENT_CLIP_PUBLIC_READER,
35
51
  CONTENT_CLIP_RECORDING_TITLE_CHARS,
36
52
  CONTENT_CLIP_SCOPES,
@@ -92,10 +108,8 @@ const MAX_DOCUMENT_ID_CHARS = 256;
92
108
  const MAX_ISSUER_CHARS = 2048;
93
109
  const MAX_VIEWER_NAME_CHARS = 200;
94
110
  const ISSUED_AT_SKEW_SECONDS = 60;
95
- const MAX_COMMENT_CHARS = 10_000;
96
111
  const MAX_COMMENT_ID_CHARS = 128;
97
112
  const MAX_EMOJI_CHARS = 32;
98
- const MAX_VIDEO_TIMESTAMP_MS = 7 * 24 * 60 * 60 * 1000;
99
113
  const MAX_BODY_BYTES = 64 * 1024;
100
114
  const viewerEmailSchema = z.string().email().max(320);
101
115
 
@@ -556,50 +570,152 @@ const commentListQuerySchema = z.object({
556
570
  .transform(Number)
557
571
  .pipe(z.number().int().min(1).max(CONTENT_CLIP_MAX_COMMENT_THREADS))
558
572
  .default(CONTENT_CLIP_MAX_COMMENT_THREADS),
573
+ view: z.literal(CONTENT_CLIP_OPEN_THREADS_VIEW).optional(),
559
574
  });
560
575
 
561
576
  /**
562
577
  * The first `limit` thread starts by video time, as `list-comments` orders
563
- * them, reading one more to tell a full page from the whole list. Text and
564
- * names are cut to the contract's lengths. A signed-out reader's list carries
565
- * no commenter's email, nor a name made from one.
578
+ * them, reading one more to tell a full page from the whole list. The
579
+ * open-threads view lists only open threads, each with its replies up to the
580
+ * contract's bound. Text and names are cut to the contract's lengths. A
581
+ * signed-out reader's list carries no commenter's email, nor a name made
582
+ * from one.
566
583
  */
567
- async function listCommentThreads(peer: ContentClipPeer, limit: number) {
584
+ async function listCommentThreads(
585
+ peer: ContentClipPeer,
586
+ limit: number,
587
+ openThreads: boolean,
588
+ ) {
568
589
  const { recordingId } = peer;
569
590
  const signedOut = peer.viewerEmail === null;
570
591
  await assertCommentAccess(recordingId, "read");
571
592
  const comments = schema.recordingComments;
593
+ const text = sql<string>`left(${comments.content}, ${
594
+ openThreads
595
+ ? CONTENT_CLIP_COMMENT_TEXT_CHARS
596
+ : CONTENT_CLIP_COMMENT_PREVIEW_CHARS
597
+ })`;
572
598
  const rows = await getDb()
573
599
  .select({
574
600
  id: comments.id,
575
601
  authorEmail: comments.authorEmail,
576
602
  authorName: comments.authorName,
577
- content: sql<string>`left(${comments.content}, ${CONTENT_CLIP_COMMENT_PREVIEW_CHARS})`,
603
+ content: text,
578
604
  videoTimestampMs: comments.videoTimestampMs,
605
+ createdAt: comments.createdAt,
579
606
  })
580
607
  .from(comments)
581
608
  .where(
582
- and(eq(comments.recordingId, recordingId), isNull(comments.parentId)),
609
+ and(
610
+ eq(comments.recordingId, recordingId),
611
+ isNull(comments.parentId),
612
+ openThreads ? eq(comments.resolved, false) : undefined,
613
+ ),
614
+ )
615
+ .orderBy(
616
+ asc(comments.videoTimestampMs),
617
+ asc(comments.createdAt),
618
+ asc(comments.id),
583
619
  )
584
- .orderBy(asc(comments.videoTimestampMs), asc(comments.createdAt))
585
620
  .limit(limit + 1);
621
+ const author = <
622
+ Row extends { authorEmail: string; authorName: string | null },
623
+ >(
624
+ row: Row,
625
+ ) => {
626
+ const authorName =
627
+ signedOut && isEmailDerivedName(row.authorName, row.authorEmail)
628
+ ? null
629
+ : row.authorName;
630
+ return {
631
+ ...row,
632
+ authorEmail: signedOut
633
+ ? null
634
+ : row.authorEmail.slice(0, CONTENT_CLIP_COMMENT_AUTHOR_CHARS),
635
+ authorName:
636
+ authorName?.slice(0, CONTENT_CLIP_COMMENT_AUTHOR_CHARS) ?? null,
637
+ };
638
+ };
586
639
  const threads = await hydrateCommentAuthorNames(rows.slice(0, limit));
640
+ const truncated = rows.length > limit;
641
+ if (!openThreads) {
642
+ return {
643
+ comments: threads.map(({ createdAt: _createdAt, ...thread }) =>
644
+ author(thread),
645
+ ),
646
+ truncated,
647
+ };
648
+ }
649
+
650
+ const threadIds = threads.map((thread) => thread.id);
651
+ const replies = threadIds.length
652
+ ? await listThreadReplies(recordingId, threadIds, text)
653
+ : { rows: [], counts: new Map<string, number>() };
654
+ const repliesOf = new Map<string, unknown[]>();
655
+ for (const { threadId, ...reply } of await hydrateCommentAuthorNames(
656
+ replies.rows,
657
+ )) {
658
+ repliesOf.set(threadId, [
659
+ ...(repliesOf.get(threadId) ?? []),
660
+ author(reply),
661
+ ]);
662
+ }
587
663
  return {
588
- comments: threads.map((thread) => {
589
- const authorName =
590
- signedOut && isEmailDerivedName(thread.authorName, thread.authorEmail)
591
- ? null
592
- : thread.authorName;
593
- return {
594
- ...thread,
595
- authorEmail: signedOut
596
- ? null
597
- : thread.authorEmail.slice(0, CONTENT_CLIP_COMMENT_AUTHOR_CHARS),
598
- authorName:
599
- authorName?.slice(0, CONTENT_CLIP_COMMENT_AUTHOR_CHARS) ?? null,
600
- };
601
- }),
602
- truncated: rows.length > limit,
664
+ comments: threads.map((thread) => ({
665
+ ...author(thread),
666
+ replyCount: replies.counts.get(thread.id) ?? 0,
667
+ replies: repliesOf.get(thread.id) ?? [],
668
+ })),
669
+ truncated,
670
+ };
671
+ }
672
+
673
+ /**
674
+ * The listed threads' replies, oldest first, from the earliest thread on and
675
+ * no more than the contract's bound, with how many each thread has.
676
+ */
677
+ async function listThreadReplies(
678
+ recordingId: string,
679
+ threadIds: string[],
680
+ text: SQL<string>,
681
+ ) {
682
+ const comments = schema.recordingComments;
683
+ const root = alias(comments, "thread_start");
684
+ const inThreads = and(
685
+ eq(comments.recordingId, recordingId),
686
+ inArray(comments.threadId, threadIds),
687
+ isNotNull(comments.parentId),
688
+ );
689
+ const [rows, counts] = await Promise.all([
690
+ getDb()
691
+ .select({
692
+ id: comments.id,
693
+ threadId: comments.threadId,
694
+ authorEmail: comments.authorEmail,
695
+ authorName: comments.authorName,
696
+ content: text,
697
+ createdAt: comments.createdAt,
698
+ })
699
+ .from(comments)
700
+ .innerJoin(root, eq(root.id, comments.threadId))
701
+ .where(inThreads)
702
+ .orderBy(
703
+ asc(root.videoTimestampMs),
704
+ asc(root.createdAt),
705
+ asc(root.id),
706
+ asc(comments.createdAt),
707
+ asc(comments.id),
708
+ )
709
+ .limit(CONTENT_CLIP_MAX_LISTED_REPLIES),
710
+ getDb()
711
+ .select({ threadId: comments.threadId, replies: count() })
712
+ .from(comments)
713
+ .where(inThreads)
714
+ .groupBy(comments.threadId),
715
+ ]);
716
+ return {
717
+ rows,
718
+ counts: new Map(counts.map((row) => [row.threadId, Number(row.replies)])),
603
719
  };
604
720
  }
605
721
 
@@ -629,21 +745,35 @@ export async function handleContentClipComments(
629
745
  );
630
746
  }
631
747
  try {
632
- return json(await listCommentThreads(peer, query.data.limit));
748
+ return json(
749
+ await listCommentThreads(
750
+ peer,
751
+ query.data.limit,
752
+ query.data.view === CONTENT_CLIP_OPEN_THREADS_VIEW,
753
+ ),
754
+ );
633
755
  } catch (error) {
634
756
  return forbiddenResponse(error);
635
757
  }
636
758
  });
637
759
  }
638
760
 
639
- const commentText = z.string().trim().min(1).max(MAX_COMMENT_CHARS);
761
+ const commentText = z
762
+ .string()
763
+ .trim()
764
+ .min(1)
765
+ .max(CONTENT_CLIP_MAX_COMMENT_CHARS);
640
766
  const commentId = z.string().trim().min(1).max(MAX_COMMENT_ID_CHARS);
641
767
  const commentWriteSchema = z.discriminatedUnion("operation", [
642
768
  z
643
769
  .object({
644
770
  operation: z.literal("add"),
645
771
  content: commentText,
646
- videoTimestampMs: z.number().int().min(0).max(MAX_VIDEO_TIMESTAMP_MS),
772
+ videoTimestampMs: z
773
+ .number()
774
+ .int()
775
+ .min(0)
776
+ .max(CONTENT_CLIP_MAX_VIDEO_TIMESTAMP_MS),
647
777
  })
648
778
  .strict(),
649
779
  z