@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/assets/agent-native.json +1 -1
- package/corpus/templates/calendar/app/hooks/use-bookings.ts +6 -2
- package/corpus/templates/calendar/app/hooks/use-public-data.ts +3 -2
- package/corpus/templates/calendar/app/pages/BookingPage.tsx +4 -3
- package/corpus/templates/calendar/changelog/2026-09-25-personal-booking-pages-use-the-host-s-saved-timezone-for-ava.md +6 -0
- package/corpus/templates/calendar/server/handlers/availability.ts +35 -4
- package/corpus/templates/calendar/server/handlers/booking-usernames.ts +6 -0
- package/corpus/templates/calendar/server/handlers/bookings.ts +40 -18
- package/corpus/templates/chat/app/root.tsx +1 -1
- package/corpus/templates/clips/AGENTS.md +7 -6
- package/corpus/templates/clips/CHANGELOG.md +1 -0
- package/corpus/templates/clips/actions/complete-workflow.ts +126 -0
- package/corpus/templates/clips/actions/generate-workflow.ts +72 -33
- package/corpus/templates/clips/actions/reconcile-workflow-generation.ts +19 -6
- package/corpus/templates/clips/actions/set-organization-branding.ts +5 -5
- package/corpus/templates/clips/agent-native.json +1 -1
- package/corpus/templates/clips/app/components/clips-command-menu.tsx +9 -1
- package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +41 -27
- package/corpus/templates/clips/app/components/workspace/organization-identity-card.tsx +7 -2
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +37 -16
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
- package/corpus/templates/clips/app/lib/organization-logo.ts +17 -0
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +9 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +17 -26
- package/corpus/templates/clips/changelog/2026-09-25-clips-clears-a-stale-recording-overlay-after-an-interrupted-.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-compact-no-comments-signup-prompt.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-concurrent-workflow-requests-no-longer-replace-an-active-gen.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-generated-workflows-stay-tied-to-the-request-that-created-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-organization-logos-can-be-stored-privately-and-load-in-setti.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-private-minio-and-s3-compatible-endpoints-now-work-with-clip.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-25-workflow-drafts-are-saved-before-the-agent-reports-them-comp.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +45 -13
- package/corpus/templates/clips/chrome-extension/src/native-recording-state.ts +17 -0
- package/corpus/templates/clips/server/db/index.ts +7 -1
- package/corpus/templates/clips/server/jobs/transactional-emails.ts +8 -1
- package/corpus/templates/clips/server/lib/organization-logo.ts +57 -0
- package/corpus/templates/clips/server/lib/s3-upload-provider.ts +240 -15
- package/corpus/templates/clips/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/clips/server/plugins/onboarding.ts +6 -1
- package/corpus/templates/clips/server/routes/api/media/index.post.ts +36 -16
- package/corpus/templates/clips/server/routes/api/media/organization-logo/[organizationId].get.ts +181 -0
- package/corpus/templates/clips/shared/organization-logo.ts +51 -0
- package/corpus/templates/clips/shared/workflow.ts +9 -0
- package/corpus/templates/content/agent-native.json +1 -1
- package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +4 -2
- package/corpus/templates/design/actions/export-html.ts +5 -1
- package/corpus/templates/design/actions/export-svg.ts +5 -1
- package/corpus/templates/design/agent-native.json +1 -1
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +31 -6
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -31
- package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +39 -19
- package/corpus/templates/design/app/components/design/inspector/GlslShaderPanel.tsx +2 -0
- package/corpus/templates/design/app/components/design/inspector/InspectorControlPopover.tsx +4 -1
- package/corpus/templates/design/app/pages/DesignSystems.tsx +10 -7
- package/corpus/templates/design/changelog/2026-09-25-builder-design-system-failures-preserve-readable-json-errors.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-design-html-export-stacks-screens.md +7 -0
- package/corpus/templates/design/changelog/2026-09-25-design-system-empty-states-show-one-creation-action.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-effect-values-stay-editable-after-blur-and-the-shader-picker.md +6 -0
- package/corpus/templates/design/changelog/2026-09-25-escape-cancels-unfinished-pen-paths-and-enter-selects-the-fi.md +6 -0
- package/corpus/templates/design/e2e/drag-and-drop.shared.ts +9 -12
- package/corpus/templates/design/e2e/helpers.ts +76 -23
- package/corpus/templates/design/server/handlers/design-system-decode-job-status.ts +2 -1
- package/corpus/templates/design/server/handlers/design-system-upload-start.ts +2 -1
- package/corpus/templates/design/server/handlers/index-design-system-sources.ts +3 -2
- package/corpus/templates/design/server/lib/design-export.ts +132 -13
- package/corpus/templates/forms/agent-native.json +1 -1
- package/corpus/templates/slides/actions/get-deck.ts +149 -1
- package/corpus/templates/slides/actions/import-file.ts +29 -1
- package/corpus/templates/slides/actions/import-from-url.ts +7 -1
- package/corpus/templates/slides/actions/import-google-slides-reference.ts +7 -3
- package/corpus/templates/slides/agent-native.json +1 -1
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +5 -3
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +451 -402
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +6 -2
- package/corpus/templates/slides/app/components/editor/QuestionFlow.tsx +1 -1
- package/corpus/templates/slides/app/components/layout/Layout.tsx +1 -0
- package/corpus/templates/slides/app/components/ui/progress.tsx +1 -0
- package/corpus/templates/slides/app/global.css +14 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +15 -0
- package/corpus/templates/slides/app/pages/Index.tsx +13 -9
- package/corpus/templates/slides/changelog/2026-09-25-agent-recognizes-blank-slides-and-uses-clear-prompts.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-compact-deck-reads-handle-imported-slides-with-invalid-selec.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-empty-decks-show-a-structured-slide-preview.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-imports-handle-common-file-and-connection-errors.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-keep-empty-deck-lists-clear-of-search-and-filter-controls.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-presentations-keep-slides-centered.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-selected-design-system-source-forms-open-directly-beneath-th.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-25-selected-presentation-options-keep-their-highlight-during-se.md +6 -0
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +2 -1
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +2 -1
- package/corpus/templates/slides/server/handlers/import/docx-parser.ts +30 -3
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +2 -1
- package/corpus/templates/slides/shared/deck-title.ts +14 -5
- package/corpus/templates/tasks/agent-native.json +1 -1
- package/dist/agent/production-agent.d.ts +3 -5
- package/dist/agent/production-agent.js +3 -5
- package/dist/client/chat/markdown-renderer.js +15 -1
- package/dist/client/chat/message-components.d.ts +4 -4
- package/dist/client/chat/message-components.js +11 -43
- package/dist/client/observability/OutputPreview.js +5 -3
- package/dist/client/onboarding/use-onboarding.js +37 -9
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +1 -1
- package/dist/extensions/url-safety.js +33 -2
- package/dist/file-upload/actions/upload-image.d.ts +3 -3
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/org/handlers.d.ts +1 -1
- package/dist/private-blob/index.d.ts +1 -1
- package/dist/private-blob/index.js +1 -1
- package/dist/private-blob/registry.d.ts +1 -0
- package/dist/private-blob/registry.js +22 -1
- package/dist/private-blob/types.d.ts +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/auth.js +60 -38
- package/dist/server/core-routes-plugin.d.ts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/server/workspace-provider-oauth.js +16 -2
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/templates/chat/app/root.tsx +1 -1
- package/dist/templates/default/app/root.tsx +1 -1
- package/package.json +3 -3
- package/src/templates/chat/app/root.tsx +1 -1
- package/src/templates/default/app/root.tsx +1 -1
package/corpus/README.md
CHANGED
|
@@ -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:
|
|
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),
|
|
@@ -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
|
-
//
|
|
92
|
-
//
|
|
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
|
|
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
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
:
|
|
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` |
|
|
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
|
|
81
|
+
| `import-loom-recording` | Import Loom or video URLs |
|
|
82
82
|
| `update-recording` | Title, password, expiry, visibility |
|
|
83
|
-
| `move-recording` | Move
|
|
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` |
|
|
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` |
|
|
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
|
|
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
|
|
11
|
-
*
|
|
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
|
-
//
|
|
59
|
-
//
|
|
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
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
`
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
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(
|