@agent-native/core 0.190.0 → 0.191.0-nightly-20260926043001

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 (129) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/assets/agent-native.json +1 -1
  3. package/corpus/templates/calendar/app/hooks/use-bookings.ts +6 -2
  4. package/corpus/templates/calendar/app/hooks/use-public-data.ts +3 -2
  5. package/corpus/templates/calendar/app/pages/BookingPage.tsx +4 -3
  6. package/corpus/templates/calendar/changelog/2026-09-25-personal-booking-pages-use-the-host-s-saved-timezone-for-ava.md +6 -0
  7. package/corpus/templates/calendar/server/handlers/availability.ts +35 -4
  8. package/corpus/templates/calendar/server/handlers/booking-usernames.ts +6 -0
  9. package/corpus/templates/calendar/server/handlers/bookings.ts +40 -18
  10. package/corpus/templates/chat/app/root.tsx +1 -1
  11. package/corpus/templates/clips/AGENTS.md +7 -6
  12. package/corpus/templates/clips/CHANGELOG.md +1 -0
  13. package/corpus/templates/clips/actions/complete-workflow.ts +126 -0
  14. package/corpus/templates/clips/actions/generate-workflow.ts +72 -33
  15. package/corpus/templates/clips/actions/reconcile-workflow-generation.ts +19 -6
  16. package/corpus/templates/clips/actions/set-organization-branding.ts +5 -5
  17. package/corpus/templates/clips/agent-native.json +1 -1
  18. package/corpus/templates/clips/app/components/clips-command-menu.tsx +9 -1
  19. package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +41 -27
  20. package/corpus/templates/clips/app/components/workspace/organization-identity-card.tsx +7 -2
  21. package/corpus/templates/clips/app/hooks/use-auto-title.ts +37 -16
  22. package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
  23. package/corpus/templates/clips/app/lib/organization-logo.ts +17 -0
  24. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +9 -1
  25. package/corpus/templates/clips/app/routes/share.$shareId.tsx +17 -26
  26. package/corpus/templates/clips/changelog/2026-09-25-clips-clears-a-stale-recording-overlay-after-an-interrupted-.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-09-25-compact-no-comments-signup-prompt.md +6 -0
  28. package/corpus/templates/clips/changelog/2026-09-25-concurrent-workflow-requests-no-longer-replace-an-active-gen.md +6 -0
  29. package/corpus/templates/clips/changelog/2026-09-25-generated-workflows-stay-tied-to-the-request-that-created-th.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-09-25-organization-logos-can-be-stored-privately-and-load-in-setti.md +6 -0
  31. package/corpus/templates/clips/changelog/2026-09-25-private-minio-and-s3-compatible-endpoints-now-work-with-clip.md +6 -0
  32. package/corpus/templates/clips/changelog/2026-09-25-workflow-drafts-are-saved-before-the-agent-reports-them-comp.md +6 -0
  33. package/corpus/templates/clips/chrome-extension/src/background.ts +45 -13
  34. package/corpus/templates/clips/chrome-extension/src/native-recording-state.ts +17 -0
  35. package/corpus/templates/clips/server/db/index.ts +7 -1
  36. package/corpus/templates/clips/server/jobs/transactional-emails.ts +8 -1
  37. package/corpus/templates/clips/server/lib/organization-logo.ts +57 -0
  38. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +240 -15
  39. package/corpus/templates/clips/server/plugins/agent-chat.ts +1 -0
  40. package/corpus/templates/clips/server/plugins/onboarding.ts +6 -1
  41. package/corpus/templates/clips/server/routes/api/media/index.post.ts +36 -16
  42. package/corpus/templates/clips/server/routes/api/media/organization-logo/[organizationId].get.ts +181 -0
  43. package/corpus/templates/clips/shared/organization-logo.ts +51 -0
  44. package/corpus/templates/clips/shared/workflow.ts +9 -0
  45. package/corpus/templates/content/agent-native.json +1 -1
  46. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +4 -2
  47. package/corpus/templates/design/actions/export-html.ts +5 -1
  48. package/corpus/templates/design/actions/export-svg.ts +5 -1
  49. package/corpus/templates/design/agent-native.json +1 -1
  50. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +31 -6
  51. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -31
  52. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +39 -19
  53. package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +2 -0
  54. package/corpus/templates/design/app/components/design/inspector/InspectorControlPopover.tsx +4 -1
  55. package/corpus/templates/design/app/pages/DesignSystems.tsx +10 -7
  56. package/corpus/templates/design/changelog/2026-09-25-builder-design-system-failures-preserve-readable-json-errors.md +6 -0
  57. package/corpus/templates/design/changelog/2026-09-25-design-html-export-stacks-screens.md +7 -0
  58. package/corpus/templates/design/changelog/2026-09-25-design-system-empty-states-show-one-creation-action.md +6 -0
  59. package/corpus/templates/design/changelog/2026-09-25-effect-values-stay-editable-after-blur-and-the-shader-picker.md +6 -0
  60. package/corpus/templates/design/changelog/2026-09-25-escape-cancels-unfinished-pen-paths-and-enter-selects-the-fi.md +6 -0
  61. package/corpus/templates/design/e2e/drag-and-drop.shared.ts +9 -12
  62. package/corpus/templates/design/e2e/helpers.ts +76 -23
  63. package/corpus/templates/design/server/handlers/design-system-decode-job-status.ts +2 -1
  64. package/corpus/templates/design/server/handlers/design-system-upload-start.ts +2 -1
  65. package/corpus/templates/design/server/handlers/index-design-system-sources.ts +3 -2
  66. package/corpus/templates/design/server/lib/design-export.ts +132 -13
  67. package/corpus/templates/forms/agent-native.json +1 -1
  68. package/corpus/templates/slides/actions/get-deck.ts +149 -1
  69. package/corpus/templates/slides/actions/import-file.ts +29 -1
  70. package/corpus/templates/slides/actions/import-from-url.ts +7 -1
  71. package/corpus/templates/slides/actions/import-google-slides-reference.ts +7 -3
  72. package/corpus/templates/slides/agent-native.json +1 -1
  73. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +5 -3
  74. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +451 -402
  75. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +6 -2
  76. package/corpus/templates/slides/app/components/editor/QuestionFlow.tsx +1 -1
  77. package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -0
  78. package/corpus/templates/slides/app/components/ui/progress.tsx +1 -0
  79. package/corpus/templates/slides/app/global.css +14 -0
  80. package/corpus/templates/slides/app/pages/DeckEditor.tsx +15 -0
  81. package/corpus/templates/slides/app/pages/Index.tsx +13 -9
  82. package/corpus/templates/slides/changelog/2026-09-25-agent-recognizes-blank-slides-and-uses-clear-prompts.md +6 -0
  83. package/corpus/templates/slides/changelog/2026-09-25-compact-deck-reads-handle-imported-slides-with-invalid-selec.md +6 -0
  84. package/corpus/templates/slides/changelog/2026-09-25-empty-decks-show-a-structured-slide-preview.md +6 -0
  85. package/corpus/templates/slides/changelog/2026-09-25-imports-handle-common-file-and-connection-errors.md +6 -0
  86. package/corpus/templates/slides/changelog/2026-09-25-keep-empty-deck-lists-clear-of-search-and-filter-controls.md +6 -0
  87. package/corpus/templates/slides/changelog/2026-09-25-presentations-keep-slides-centered.md +6 -0
  88. package/corpus/templates/slides/changelog/2026-09-25-selected-design-system-source-forms-open-directly-beneath-th.md +6 -0
  89. package/corpus/templates/slides/changelog/2026-09-25-selected-presentation-options-keep-their-highlight-during-se.md +6 -0
  90. package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +2 -1
  91. package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +2 -1
  92. package/corpus/templates/slides/server/handlers/import/docx-parser.ts +30 -3
  93. package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +2 -1
  94. package/corpus/templates/slides/shared/deck-title.ts +14 -5
  95. package/corpus/templates/tasks/agent-native.json +1 -1
  96. package/dist/agent/production-agent.d.ts +3 -5
  97. package/dist/agent/production-agent.js +3 -5
  98. package/dist/client/chat/markdown-renderer.js +15 -1
  99. package/dist/client/chat/message-components.d.ts +4 -4
  100. package/dist/client/chat/message-components.js +11 -43
  101. package/dist/client/observability/OutputPreview.js +5 -3
  102. package/dist/client/onboarding/use-onboarding.js +37 -9
  103. package/dist/collab/awareness.d.ts +2 -2
  104. package/dist/collab/struct-routes.d.ts +1 -1
  105. package/dist/deploy/build.js +1 -1
  106. package/dist/extensions/url-safety.js +33 -2
  107. package/dist/file-upload/actions/upload-image.d.ts +3 -3
  108. package/dist/notifications/routes.d.ts +3 -3
  109. package/dist/observability/routes.d.ts +5 -5
  110. package/dist/org/handlers.d.ts +1 -1
  111. package/dist/private-blob/index.d.ts +1 -1
  112. package/dist/private-blob/index.js +1 -1
  113. package/dist/private-blob/registry.d.ts +1 -0
  114. package/dist/private-blob/registry.js +22 -1
  115. package/dist/private-blob/types.d.ts +1 -0
  116. package/dist/progress/routes.d.ts +1 -1
  117. package/dist/resources/handlers.d.ts +1 -1
  118. package/dist/server/auth.js +60 -38
  119. package/dist/server/core-routes-plugin.d.ts +1 -1
  120. package/dist/server/index.d.ts +1 -1
  121. package/dist/server/index.js +1 -1
  122. package/dist/server/transcribe-voice.d.ts +1 -1
  123. package/dist/server/workspace-provider-oauth.js +16 -2
  124. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  125. package/dist/templates/chat/app/root.tsx +1 -1
  126. package/dist/templates/default/app/root.tsx +1 -1
  127. package/package.json +3 -3
  128. package/src/templates/chat/app/root.tsx +1 -1
  129. package/src/templates/default/app/root.tsx +1 -1
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 10001
34
+ - template files: 10028
@@ -3,7 +3,7 @@
3
3
  "onboarding": {
4
4
  "firstRun": {
5
5
  "development": "off",
6
- "production": "connect-and-integrations"
6
+ "production": "connect"
7
7
  }
8
8
  },
9
9
  "doctor": {
@@ -28,15 +28,17 @@ export function useAvailableSlots(
28
28
  duration: number,
29
29
  slug?: string,
30
30
  draft?: BookingAvailabilityPreview,
31
+ username?: string,
31
32
  ) {
32
33
  const draftParam = draft ? JSON.stringify(draft) : undefined;
33
34
 
34
35
  return useQuery<{ start: string; end: string }[]>({
35
- queryKey: ["available-slots", date, duration, slug, draftParam],
36
+ queryKey: ["available-slots", date, duration, slug, draftParam, username],
36
37
  queryFn: async () => {
37
38
  const params = new URLSearchParams({ date, duration: String(duration) });
38
39
  if (slug) params.set("slug", slug);
39
40
  if (draftParam) params.set("draft", draftParam);
41
+ if (username) params.set("username", username);
40
42
  const res = await fetch(
41
43
  appApiPath(`/api/bookings/available-slots?${params}`),
42
44
  );
@@ -55,10 +57,11 @@ export function useAvailableDays(
55
57
  to: string,
56
58
  duration: number,
57
59
  slug?: string,
60
+ username?: string,
58
61
  enabled = true,
59
62
  ) {
60
63
  return useQuery<string[]>({
61
- queryKey: ["available-days", from, to, duration, slug],
64
+ queryKey: ["available-days", from, to, duration, slug, username],
62
65
  queryFn: async () => {
63
66
  const params = new URLSearchParams({
64
67
  from,
@@ -66,6 +69,7 @@ export function useAvailableDays(
66
69
  duration: String(duration),
67
70
  });
68
71
  if (slug) params.set("slug", slug);
72
+ if (username) params.set("username", username);
69
73
  const res = await fetch(
70
74
  appApiPath(`/api/bookings/available-slots?${params}`),
71
75
  );
@@ -16,12 +16,13 @@ export function usePublicSettings() {
16
16
  }
17
17
 
18
18
  /** Fetches availability from the public (unauthenticated) endpoint */
19
- export function usePublicAvailability(slug?: string) {
19
+ export function usePublicAvailability(slug?: string, username?: string) {
20
20
  return useQuery<AvailabilityConfig>({
21
- queryKey: ["public-availability", slug],
21
+ queryKey: ["public-availability", slug, username],
22
22
  queryFn: async () => {
23
23
  const params = new URLSearchParams();
24
24
  if (slug) params.set("slug", slug);
25
+ if (username) params.set("username", username);
25
26
  const path = params.size
26
27
  ? `/api/public/availability?${params}`
27
28
  : "/api/public/availability";
@@ -79,7 +79,7 @@ function BookingPageShell({
79
79
  <LanguagePicker variant="ghost-icon" />
80
80
  <ThemeToggle />
81
81
  </div>
82
- <div className="fixed bottom-[21px] left-4 z-50 max-sm:static max-sm:mx-auto max-sm:mt-8 [&_.an-powered-logo]:!h-3.5 [&_.an-powered-logo]:brightness-0 dark:[&_.an-powered-logo]:invert">
82
+ <div className="fixed bottom-[21px] left-4 z-50 max-sm:static max-sm:mx-auto max-sm:pt-8 [&_.an-powered-logo]:!h-3.5 [&_.an-powered-logo]:brightness-0 dark:[&_.an-powered-logo]:invert">
83
83
  <PoweredByBadge variant="plain" embedded />
84
84
  </div>
85
85
  <div className="relative z-10 min-h-screen overflow-x-hidden p-4">
@@ -96,7 +96,7 @@ export default function BookingPage() {
96
96
  const navigate = useNavigate();
97
97
  const { data: settings, isLoading: settingsLoading } = usePublicSettings();
98
98
  const { data: availability, isLoading: availabilityLoading } =
99
- usePublicAvailability(slug);
99
+ usePublicAvailability(slug, username);
100
100
  const {
101
101
  data: bookingLink,
102
102
  isLoading: bookingLinkLoading,
@@ -233,7 +233,7 @@ export default function BookingPage() {
233
233
  data: slots = [],
234
234
  isLoading: slotsLoading,
235
235
  error: slotsError,
236
- } = useAvailableSlots(dateStr, duration, slug);
236
+ } = useAvailableSlots(dateStr, duration, slug, undefined, username);
237
237
  const monthStart = format(startOfMonth(viewMonth), "yyyy-MM-dd");
238
238
  const monthEnd = format(endOfMonth(viewMonth), "yyyy-MM-dd");
239
239
  const {
@@ -245,6 +245,7 @@ export default function BookingPage() {
245
245
  monthEnd,
246
246
  duration,
247
247
  slug,
248
+ username,
248
249
  step === "date" &&
249
250
  !!availability &&
250
251
  (!hasDurationChoice || selectedDuration !== null),
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-25
4
+ ---
5
+
6
+ Personal booking pages use the host's saved timezone for availability.
@@ -8,12 +8,14 @@ import { eq } from "drizzle-orm";
8
8
  import {
9
9
  defineEventHandler,
10
10
  getQuery,
11
+ createError,
11
12
  setResponseStatus,
12
13
  type H3Event,
13
14
  } from "h3";
14
15
 
15
16
  import type { AvailabilityConfig } from "../../shared/api.js";
16
17
  import { getDb, schema } from "../db/index.js";
18
+ import { getBookingUsernameOwner } from "./booking-usernames.js";
17
19
 
18
20
  function createDefaultAvailability(timezone: string): AvailabilityConfig {
19
21
  return {
@@ -66,6 +68,7 @@ export const getPublicAvailability = defineEventHandler(
66
68
  async (event: H3Event) => {
67
69
  const query = getQuery(event);
68
70
  const slug = typeof query.slug === "string" ? query.slug : "";
71
+ const username = typeof query.username === "string" ? query.username : "";
69
72
  if (slug) {
70
73
  const link = await getDb()
71
74
  .select({ ownerEmail: schema.bookingLinks.ownerEmail })
@@ -86,12 +89,40 @@ export const getPublicAvailability = defineEventHandler(
86
89
  ownerSettings?.timezone || "America/New_York",
87
90
  );
88
91
  }
92
+
93
+ if (username) {
94
+ const ownerEmail = await getBookingUsernameOwner(username);
95
+ if (!ownerEmail) {
96
+ throw createError({
97
+ statusCode: 404,
98
+ statusMessage: "Booking page not found",
99
+ });
100
+ }
101
+
102
+ const ownerConfig = (await getUserSetting(
103
+ ownerEmail,
104
+ "calendar-availability",
105
+ )) as unknown as AvailabilityConfig | null;
106
+ if (ownerConfig?.bookingPageSlug === slug) return ownerConfig;
107
+ if (!ownerConfig && slug === "book") {
108
+ const ownerSettings = (await getUserSetting(
109
+ ownerEmail,
110
+ "calendar-settings",
111
+ )) as { timezone?: string } | null;
112
+ return createDefaultAvailability(
113
+ ownerSettings?.timezone || "America/New_York",
114
+ );
115
+ }
116
+
117
+ throw createError({
118
+ statusCode: 404,
119
+ statusMessage: "Booking page not found",
120
+ });
121
+ }
89
122
  }
90
123
 
91
- // Fall back to defaults — never read the unscoped `calendar-availability`
92
- // setting. That key was historically dual-written by every user's update
93
- // (see the matching fix in updateAvailability), which meant a brand-new
94
- // user's public booking link advertised whoever last saved their hours.
124
+ // Username-scoped pages fail closed above; legacy links without a username
125
+ // still use defaults and never read the unscoped availability setting.
95
126
  return createDefaultAvailability("America/New_York");
96
127
  },
97
128
  );
@@ -64,6 +64,12 @@ async function findUsernameOwner(username: string): Promise<string | null> {
64
64
  return row?.ownerEmail ?? null;
65
65
  }
66
66
 
67
+ export function getBookingUsernameOwner(
68
+ username: string,
69
+ ): Promise<string | null> {
70
+ return findUsernameOwner(username);
71
+ }
72
+
67
73
  async function chooseAvailableUsername(
68
74
  base: string,
69
75
  ownerEmail: string,
@@ -61,6 +61,7 @@ import { getOwnerBookingTimeZone } from "../lib/booking-timezone.js";
61
61
  import { eventBlocksAvailability } from "../lib/calendar-availability.js";
62
62
  import * as googleCalendar from "../lib/google-calendar.js";
63
63
  import { createZoomMeeting } from "../lib/zoom.js";
64
+ import { getBookingUsernameOwner } from "./booking-usernames.js";
64
65
 
65
66
  async function requireRequestContext<T>(
66
67
  event: H3Event,
@@ -598,10 +599,12 @@ function unavailableAvailabilityResponse(event: H3Event) {
598
599
 
599
600
  async function resolveAvailabilityContext({
600
601
  slug,
602
+ username,
601
603
  draft,
602
604
  db = getDb(),
603
605
  }: {
604
606
  slug: string;
607
+ username?: string;
605
608
  draft?: BookingAvailabilityDraft;
606
609
  db?: ConflictDb;
607
610
  }): Promise<AvailabilityContext> {
@@ -634,26 +637,42 @@ async function resolveAvailabilityContext({
634
637
  });
635
638
  }
636
639
  const config = configRaw as unknown as AvailabilityConfig | null;
637
- const ownerEmail = bookingLink?.ownerEmail;
640
+ const usernameOwnerEmail =
641
+ !bookingLink && username && !draft
642
+ ? await getBookingUsernameOwner(username)
643
+ : null;
644
+ const candidateOwnerEmail = bookingLink?.ownerEmail || usernameOwnerEmail;
645
+ const [candidateOwnerConfigRaw, candidateOwnerSettingsRaw] =
646
+ candidateOwnerEmail
647
+ ? await Promise.all([
648
+ getUserSetting(candidateOwnerEmail, "calendar-availability"),
649
+ getUserSetting(candidateOwnerEmail, "calendar-settings"),
650
+ ])
651
+ : [null, null];
652
+ const candidateOwnerConfig =
653
+ candidateOwnerConfigRaw as AvailabilityConfig | null;
654
+ const usernameSlugMatches =
655
+ !usernameOwnerEmail ||
656
+ candidateOwnerConfig?.bookingPageSlug === slug ||
657
+ (!candidateOwnerConfig && slug === "book");
658
+ const ownerEmail =
659
+ bookingLink?.ownerEmail ||
660
+ (usernameSlugMatches ? usernameOwnerEmail || undefined : undefined);
638
661
  const overrides = bookingLink
639
662
  ? resolveBookingLinkAvailabilityOverrides({ bookingLink, draft })
640
663
  : undefined;
641
664
  const hostEmails = overrides?.hostEmails ?? (ownerEmail ? [ownerEmail] : []);
642
- const [ownerConfigRaw, ownerSettingsRaw, conflictSlugs] = await Promise.all([
643
- ownerEmail
644
- ? getUserSetting(ownerEmail, "calendar-availability")
645
- : Promise.resolve(null),
646
- ownerEmail
647
- ? getUserSetting(ownerEmail, "calendar-settings")
648
- : Promise.resolve(null),
649
- ownerEmail
650
- ? getBookingLinkSlugsForOwners(hostEmails, db)
651
- : slug
652
- ? Promise.resolve([slug])
653
- : Promise.resolve([]),
654
- ]);
655
- const ownerConfig = ownerConfigRaw as unknown as AvailabilityConfig | null;
656
- const ownerSettings = ownerSettingsRaw as { timezone?: string } | null;
665
+ const ownerConfig = ownerEmail ? candidateOwnerConfig : null;
666
+ const ownerSettings = ownerEmail
667
+ ? (candidateOwnerSettingsRaw as { timezone?: string } | null)
668
+ : null;
669
+ const conflictSlugs = ownerEmail
670
+ ? await getBookingLinkSlugsForOwners(hostEmails, db).then((slugs) =>
671
+ Array.from(new Set([slug, ...slugs])),
672
+ )
673
+ : slug
674
+ ? [slug]
675
+ : [];
657
676
  const eligibleHosts = await getEligibleHostAvailability(
658
677
  ownerEmail,
659
678
  hostEmails,
@@ -666,7 +685,9 @@ async function resolveAvailabilityContext({
666
685
  ? createDefaultAvailability(
667
686
  ownerSettings?.timezone || "America/New_York",
668
687
  )
669
- : config),
688
+ : username
689
+ ? null
690
+ : config),
670
691
  ownerEmail,
671
692
  hostEmails,
672
693
  eligibleHosts,
@@ -1700,6 +1721,7 @@ async function getAvailableSlotsForQuery(
1700
1721
  const to = parseDateOnly(query.to);
1701
1722
  const hasRangeQuery = query.from !== undefined || query.to !== undefined;
1702
1723
  const slug = typeof query.slug === "string" ? query.slug : "";
1724
+ const username = typeof query.username === "string" ? query.username : "";
1703
1725
 
1704
1726
  if (hasRangeQuery) {
1705
1727
  if (!from || !to) {
@@ -1724,7 +1746,7 @@ async function getAvailableSlotsForQuery(
1724
1746
  return { error: "date query parameter is required" };
1725
1747
  }
1726
1748
 
1727
- const context = await resolveAvailabilityContext({ slug, draft });
1749
+ const context = await resolveAvailabilityContext({ slug, username, draft });
1728
1750
  if (!context.effectiveConfig) {
1729
1751
  return hasRangeQuery ? { dates: [] } : { slots: [] };
1730
1752
  }
@@ -57,7 +57,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
57
57
  <meta charSet="utf-8" />
58
58
  <meta
59
59
  name="viewport"
60
- content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
60
+ content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, interactive-widget=resizes-content"
61
61
  />
62
62
  <script
63
63
  suppressHydrationWarning
@@ -76,22 +76,23 @@ video sharing app. The agent and the UI share the same SQL data and actions.
76
76
  | --- | --- |
77
77
  | `view-screen`, `navigate` | Read context; open a surface |
78
78
  | `list-recordings`, `search-recordings` | Library, trash, `--view=shared` |
79
- | `get-recording-player-data` | Full transcript, chapters, diagnostics |
79
+ | `get-recording-player-data` | Transcript, chapters, diagnostics |
80
80
  | `create-recording`, `finalize-recording` | Create row; finish upload |
81
- | `import-loom-recording` | Import Loom or direct MP4/WebM URL |
81
+ | `import-loom-recording` | Import Loom or video URLs |
82
82
  | `update-recording` | Title, password, expiry, visibility |
83
- | `move-recording` | Move `id` or `ids` to a folder or root |
83
+ | `move-recording` | Move recordings to folder/root |
84
84
  | `archive-`, `trash-`, `restore-recording` | Lifecycle |
85
85
  | `reprocess-recording` | Repair unseekable/frozen media |
86
- | `generate-filmstrip` | Editor timeline frame sprite; `all` backfills |
86
+ | `generate-filmstrip` | Timeline frames |
87
87
  | `request-transcript`, `cleanup-transcript` | Transcribe; `force`/`regenerate` |
88
88
  | `regenerate-title`, `-summary`, `-chapters` | AI metadata |
89
+ | `generate-workflow`, `complete-workflow` | Generate and save workflow |
89
90
  | `trim-`, `split-recording`, `remove-silences`, `remove-filler-words` | Edits |
90
91
  | `list-meetings`, `get-`, `update-`, `finalize-meeting` | Meetings |
91
- | `search-meetings` | Find a meeting by title, summary, notes, attendee, or transcript |
92
+ | `search-meetings` | Search meetings |
92
93
  | `list-dictations`, `cleanup-dictation` | Dictation history |
93
94
  | `add-comment`, `update-comment`, `create-folder`, `create-space` | Comments, folders |
94
- | `get-clips-notification-prefs`, `update-clips-notification-prefs` | Read or change optional email notifications |
95
+ | `get-clips-notification-prefs`, `update-clips-notification-prefs` | Read or update email alerts |
95
96
  | `share-resource`, `set-resource-visibility`, `build-embed-url` | Share, embed |
96
97
  | `create-recording-agent-link` | Two-hour `agent_access` share URL |
97
98
  | `prepare-crm-call-evidence` | Opaque clip id plus `/r/<id>` for CRM |
@@ -7,6 +7,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
7
7
 
8
8
  ### Improved
9
9
 
10
+ - The no-comments sidebar gives viewers a concise reason to try Clips and a clear path to sign up.
10
11
  - Shared clips remember your sidebar choice and help new viewers understand why to sign up.
11
12
 
12
13
  ### Fixed
@@ -0,0 +1,126 @@
1
+ import { defineAction, fail } from "@agent-native/core/action";
2
+ import {
3
+ compareAndSetAppState,
4
+ readAppState,
5
+ } from "@agent-native/core/application-state";
6
+ import { assertAccess } from "@agent-native/core/sharing";
7
+ import { z } from "zod";
8
+
9
+ import {
10
+ matchesWorkflowRequest,
11
+ WorkflowKindSchema,
12
+ } from "../shared/workflow.js";
13
+
14
+ const WorkflowStateSchema = z
15
+ .object({
16
+ kind: WorkflowKindSchema.optional(),
17
+ status: z.string(),
18
+ recordingId: z.string().min(1),
19
+ requestedAt: z.string().min(1),
20
+ requestId: z.string().min(1).optional(),
21
+ })
22
+ .passthrough();
23
+
24
+ export default defineAction({
25
+ description:
26
+ "Save the final markdown for a Clips workflow request. Returns saved only after the matching request is persisted as ready.",
27
+ schema: z.object({
28
+ recordingId: z.string().min(1).describe("Recording that owns the workflow"),
29
+ requestedAt: z
30
+ .string()
31
+ .min(1)
32
+ .describe("Exact request timestamp from the generate-workflow context"),
33
+ requestId: z
34
+ .string()
35
+ .min(1)
36
+ .optional()
37
+ .describe(
38
+ "Exact workflow request ID when present in the request context",
39
+ ),
40
+ content: z
41
+ .string()
42
+ .trim()
43
+ .min(1)
44
+ .max(50_000)
45
+ .describe("Final workflow document in markdown"),
46
+ }),
47
+ run: async ({ recordingId, requestedAt, requestId, content }) => {
48
+ await assertAccess("recording", recordingId, "viewer");
49
+
50
+ const stateKey = `clips-workflow-${recordingId}`;
51
+ const current = await readAppState(stateKey);
52
+ if (current === null) {
53
+ fail("No active workflow request was found for this recording.", {
54
+ statusCode: 409,
55
+ errorCode: "workflow_not_active",
56
+ });
57
+ }
58
+
59
+ const parsed = WorkflowStateSchema.safeParse(current);
60
+ if (!parsed.success) {
61
+ throw new Error(`Invalid generated workflow state for ${recordingId}`);
62
+ }
63
+ if (parsed.data.recordingId !== recordingId) {
64
+ throw new Error(`Generated workflow state does not match ${recordingId}`);
65
+ }
66
+ const expected = { requestedAt, requestId };
67
+ if (!matchesWorkflowRequest(parsed.data, expected)) {
68
+ fail("This workflow request has been replaced by a newer request.", {
69
+ statusCode: 409,
70
+ errorCode: "stale_workflow_request",
71
+ });
72
+ }
73
+ const result = {
74
+ recordingId,
75
+ requestedAt,
76
+ ...(requestId ? { requestId } : {}),
77
+ };
78
+
79
+ if (parsed.data.status === "ready" && current.content === content) {
80
+ return { saved: true, ...result, alreadySaved: true };
81
+ }
82
+ if (parsed.data.status !== "generating") {
83
+ fail("This workflow request is no longer generating.", {
84
+ statusCode: 409,
85
+ errorCode: "workflow_not_generating",
86
+ });
87
+ }
88
+
89
+ const nextState = {
90
+ ...current,
91
+ status: "ready",
92
+ content,
93
+ completedAt: new Date().toISOString(),
94
+ };
95
+ const saved = await compareAndSetAppState(stateKey, current, nextState);
96
+ if (!saved) {
97
+ const latest = await readAppState(stateKey);
98
+ if (
99
+ latest?.status === "ready" &&
100
+ latest.recordingId === recordingId &&
101
+ matchesWorkflowRequest(latest, expected) &&
102
+ latest.content === content
103
+ ) {
104
+ return { saved: true, ...result, alreadySaved: true };
105
+ }
106
+ fail("The workflow changed before its result could be saved.", {
107
+ statusCode: 409,
108
+ errorCode: "workflow_changed",
109
+ });
110
+ }
111
+
112
+ const persisted = await readAppState(stateKey);
113
+ if (
114
+ persisted?.status !== "ready" ||
115
+ persisted.recordingId !== recordingId ||
116
+ !matchesWorkflowRequest(persisted, expected) ||
117
+ persisted.content !== content
118
+ ) {
119
+ throw new Error(
120
+ `Generated workflow was not persisted for ${recordingId}`,
121
+ );
122
+ }
123
+
124
+ return { saved: true, ...result };
125
+ },
126
+ });
@@ -7,15 +7,18 @@
7
7
  * - ticket — a bug/issue ticket
8
8
  * - email — a ready-to-send email
9
9
  *
10
- * The agent composes the document and stores it in application_state under
11
- * `clips-workflow-<recordingId>` so the UI can pick it up and display it.
10
+ * The agent composes the document and saves it with complete-workflow so the
11
+ * UI can pick it up and display it.
12
12
  *
13
13
  * Usage:
14
14
  * pnpm action generate-workflow --recordingId=<id> --kind=pr
15
15
  */
16
16
 
17
+ import { randomUUID } from "node:crypto";
18
+
17
19
  import { defineAction } from "@agent-native/core/action";
18
20
  import {
21
+ compareAndSetManyAppState,
19
22
  readAppState,
20
23
  writeAppState,
21
24
  } from "@agent-native/core/application-state";
@@ -55,14 +58,23 @@ Output markdown.`,
55
58
  Keep it concise, warm, and professional.`,
56
59
  } as const;
57
60
 
58
- // Actions for the same recording can arrive concurrently when the player and
59
- // agent both retry a request. Keep the read-check-write/enqueue sequence
60
- // single-flight within this runtime so only one request can claim generation.
61
+ // Skip duplicate database work in this runtime; the multi-key CAS below also
62
+ // prevents separate runtimes from claiming the same generation.
61
63
  const workflowGenerationLocks = new Set<string>();
62
64
 
65
+ function isRecentGeneration(state: Record<string, unknown> | null): boolean {
66
+ if (state?.status !== "generating") return false;
67
+ const requestedAt = Date.parse(
68
+ typeof state.requestedAt === "string" ? state.requestedAt : "",
69
+ );
70
+ return (
71
+ !Number.isFinite(requestedAt) || Date.now() - requestedAt < 10 * 60_000
72
+ );
73
+ }
74
+
63
75
  export default defineAction({
64
76
  description:
65
- "Ask the agent to generate a structured workflow doc (pr/sop/ticket/email) from this recording's transcript (and the full video when Include full video is enabled). The agent writes the result to clips-workflow-<recordingId> in application_state.",
77
+ "Ask the agent to generate a structured workflow doc (pr/sop/ticket/email) from this recording's transcript (and the full video when Include full video is enabled). The agent saves the result with complete-workflow.",
66
78
  schema: z.object({
67
79
  recordingId: z.string().describe("Recording ID"),
68
80
  kind: WorkflowKindSchema.describe("Workflow kind"),
@@ -104,47 +116,44 @@ export default defineAction({
104
116
  const includeFullVideoInAi = await readIncludeFullVideoInAi();
105
117
 
106
118
  const existing = await readAppState(stateKey);
107
- if (existing?.status === "generating") {
108
- const requestedAt = Date.parse(
109
- typeof existing.requestedAt === "string" ? existing.requestedAt : "",
110
- );
111
- const isRecent =
112
- !Number.isFinite(requestedAt) ||
113
- Date.now() - requestedAt < 10 * 60_000;
114
- if (isRecent) {
115
- return {
116
- queued: false,
117
- duplicate: true,
118
- recordingId: args.recordingId,
119
- kind: existing.kind ?? args.kind,
120
- stateKey,
121
- };
122
- }
119
+ if (isRecentGeneration(existing)) {
120
+ return {
121
+ queued: false,
122
+ duplicate: true,
123
+ recordingId: args.recordingId,
124
+ kind: existing?.kind ?? args.kind,
125
+ stateKey,
126
+ };
123
127
  }
124
128
 
129
+ const requestKey = `clips-ai-request-${args.recordingId}`;
130
+ const existingRequest = await readAppState(requestKey);
125
131
  const requestedAt = new Date().toISOString();
126
- // Seed the output state with a "generating" placeholder so the UI can show
127
- // a loading state immediately.
128
- await writeAppState(stateKey, {
132
+ const requestId = randomUUID();
133
+ const workflowState = {
129
134
  kind: args.kind,
130
135
  status: "generating",
131
136
  recordingId: args.recordingId,
132
137
  requestedAt,
133
- } as any);
138
+ requestId,
139
+ };
134
140
 
135
141
  const baseMessage =
136
142
  `Generate a ${args.kind.toUpperCase()} workflow document from recording ${args.recordingId} ` +
137
143
  `(title: "${rec.title}"). Read the transcript from this request's context. ` +
138
144
  `${KIND_PROMPTS[args.kind]} ` +
139
- `Then write the final markdown to application_state key "${stateKey}" as ` +
140
- `\`{ kind: "${args.kind}", status: "ready", content: "...", recordingId: "${args.recordingId}" }\`. ` +
141
- `Finish by replying in chat with the same generated markdown so the user can read it immediately.`;
145
+ `Then call complete-workflow with recordingId "${args.recordingId}", ` +
146
+ `the exact requestedAt and requestId "${requestId}" from this request's context, ` +
147
+ `and the final markdown. ` +
148
+ `Do not report completion unless that action returns saved: true. ` +
149
+ `Finish by replying in chat with the same generated markdown.`;
142
150
 
143
151
  const request = {
144
152
  kind: "generate-workflow" as const,
145
153
  workflowKind: args.kind,
146
154
  recordingId: args.recordingId,
147
155
  requestedAt,
156
+ requestId,
148
157
  recordingTitle: rec.title,
149
158
  recordingDescription: rec.description,
150
159
  transcriptStatus: transcript?.status ?? "pending",
@@ -160,10 +169,40 @@ export default defineAction({
160
169
  ),
161
170
  };
162
171
 
163
- await writeAppState(
164
- `clips-ai-request-${args.recordingId}`,
165
- request as any,
166
- );
172
+ const claimed = await compareAndSetManyAppState([
173
+ {
174
+ key: stateKey,
175
+ expectedValue: existing,
176
+ nextValue: workflowState,
177
+ },
178
+ {
179
+ key: requestKey,
180
+ expectedValue: existingRequest,
181
+ nextValue: request,
182
+ },
183
+ ]);
184
+ if (!claimed) {
185
+ const current = await readAppState(stateKey);
186
+ if (isRecentGeneration(current)) {
187
+ return {
188
+ queued: false,
189
+ duplicate: true,
190
+ recordingId: args.recordingId,
191
+ kind: current?.kind ?? args.kind,
192
+ stateKey,
193
+ };
194
+ }
195
+ return {
196
+ queued: false,
197
+ duplicate: false,
198
+ retry: true,
199
+ reason: "claim-contended",
200
+ recordingId: args.recordingId,
201
+ kind: args.kind,
202
+ stateKey,
203
+ };
204
+ }
205
+
167
206
  await writeAppState("refresh-signal", { ts: Date.now() });
168
207
 
169
208
  console.log(