@agent-native/core 0.172.3 → 0.172.6
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/calendar/actions/get-settings.ts +2 -5
- package/corpus/templates/calendar/actions/list-events.ts +21 -80
- package/corpus/templates/calendar/actions/update-settings.ts +11 -16
- package/corpus/templates/calendar/actions/view-screen.ts +25 -10
- package/corpus/templates/calendar/app/components/calendar/CommandPalette.tsx +7 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +5 -2
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +3 -18
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +3 -62
- package/corpus/templates/calendar/changelog/2026-07-22-calendar-grid-local-time.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-19-the-calendar-loads-again-for-accounts-whose-saved-timezone-w.md +6 -0
- package/corpus/templates/calendar/server/handlers/settings.ts +8 -24
- package/corpus/templates/calendar/server/lib/calendar-settings.ts +58 -0
- package/corpus/templates/calendar/server/lib/find-time.ts +15 -93
- package/corpus/templates/calendar/shared/settings.ts +15 -9
- package/corpus/templates/calendar/shared/timezone.ts +127 -0
- package/corpus/templates/factory/AGENTS.md +5 -3
- package/corpus/templates/factory/actions/babysit-pull-request.ts +6 -0
- package/corpus/templates/factory/actions/list-factory-automations.ts +2 -2
- package/corpus/templates/factory/actions/save-factory-automation.ts +0 -5
- package/corpus/templates/factory/changelog/2026-08-24-slack-checking-works-for-teammate-created-factories.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-24-teammates-can-edit-and-run-factory-jobs.md +6 -0
- package/corpus/templates/factory/server/lib/factory-scope.ts +8 -0
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +1 -9
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +2 -1
- package/corpus/templates/slides/actions/list-decks.ts +79 -5
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +21 -1
- package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +49 -21
- package/corpus/templates/slides/app/context/DeckContext.tsx +49 -21
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +2 -2
- package/corpus/templates/slides/app/pages/Index.tsx +11 -13
- package/corpus/templates/slides/changelog/2026-08-24-deck-previews-and-sharing-live-in-the-overflow-menu.md +5 -0
- package/dist/agent/production-agent.js +5 -1
- package/dist/agent/types.d.ts +5 -0
- package/dist/automations/service.d.ts +6 -0
- package/dist/automations/service.js +20 -0
- package/dist/client/settings/SettingsPanel.js +80 -33
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/jobs/run-now.js +2 -2
- package/dist/observability/posthog-ai.d.ts +36 -11
- package/dist/observability/posthog-ai.js +99 -30
- package/dist/observability/routes.js +9 -2
- package/dist/observability/traces.js +330 -142
- package/dist/tracking/providers.js +9 -3
- package/dist/vite/client.js +5 -2
- package/docs/content/docs-components.mdx +20 -1
- package/docs/content/observability.mdx +27 -10
- package/docs/content/template-calendar-agent.mdx +63 -26
- package/docs/content/template-calendar-developers.mdx +165 -40
- package/docs/content/template-calendar-features.mdx +276 -0
- package/docs/content/template-calendar-integrations.mdx +78 -0
- package/docs/content/template-calendar.mdx +56 -123
- package/package.json +1 -1
- package/docs/content/template-calendar-booking-links.mdx +0 -73
- package/docs/content/template-calendar-scheduling.mdx +0 -128
|
@@ -3,6 +3,12 @@ import type {
|
|
|
3
3
|
FindTimeParticipant,
|
|
4
4
|
FindTimeSlot,
|
|
5
5
|
} from "../../shared/api.js";
|
|
6
|
+
import {
|
|
7
|
+
addDaysToDateKey,
|
|
8
|
+
dateKeyInTimezone,
|
|
9
|
+
dateTimeInTimezoneToIso,
|
|
10
|
+
isCalendarTimezone,
|
|
11
|
+
} from "../../shared/timezone.js";
|
|
6
12
|
|
|
7
13
|
export interface AvailabilitySchedule {
|
|
8
14
|
timezone: string;
|
|
@@ -37,108 +43,24 @@ const DEFAULT_SCHEDULE: AvailabilitySchedule["schedule"] = {
|
|
|
37
43
|
sunday: [],
|
|
38
44
|
};
|
|
39
45
|
|
|
46
|
+
/**
|
|
47
|
+
* These live in shared/timezone.ts so the grid, the actions, and this module
|
|
48
|
+
* cannot drift on DST edges. Kept under their original names because callers
|
|
49
|
+
* across the template import them from here.
|
|
50
|
+
*/
|
|
40
51
|
export function normalizeTimezone(timezone?: string): string {
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
|
44
|
-
return timezone;
|
|
45
|
-
} catch {
|
|
46
|
-
return "UTC";
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function datePartsInTimezone(date: Date, timezone: string) {
|
|
51
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
52
|
-
timeZone: timezone,
|
|
53
|
-
hourCycle: "h23",
|
|
54
|
-
year: "numeric",
|
|
55
|
-
month: "2-digit",
|
|
56
|
-
day: "2-digit",
|
|
57
|
-
hour: "2-digit",
|
|
58
|
-
minute: "2-digit",
|
|
59
|
-
second: "2-digit",
|
|
60
|
-
}).formatToParts(date);
|
|
61
|
-
const get = (type: string) =>
|
|
62
|
-
Number(parts.find((part) => part.type === type)?.value ?? "0");
|
|
63
|
-
return {
|
|
64
|
-
year: get("year"),
|
|
65
|
-
month: get("month"),
|
|
66
|
-
day: get("day"),
|
|
67
|
-
hour: get("hour"),
|
|
68
|
-
minute: get("minute"),
|
|
69
|
-
second: get("second"),
|
|
70
|
-
};
|
|
52
|
+
return isCalendarTimezone(timezone) ? timezone : "UTC";
|
|
71
53
|
}
|
|
72
54
|
|
|
73
|
-
export
|
|
74
|
-
|
|
75
|
-
return [
|
|
76
|
-
String(parts.year).padStart(4, "0"),
|
|
77
|
-
String(parts.month).padStart(2, "0"),
|
|
78
|
-
String(parts.day).padStart(2, "0"),
|
|
79
|
-
].join("-");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function addDaysToDateOnly(dateOnly: string, days: number): string {
|
|
83
|
-
const [year, month, day] = dateOnly.split("-").map(Number);
|
|
84
|
-
const date = new Date(Date.UTC(year, month - 1, day + days));
|
|
85
|
-
return [
|
|
86
|
-
String(date.getUTCFullYear()).padStart(4, "0"),
|
|
87
|
-
String(date.getUTCMonth() + 1).padStart(2, "0"),
|
|
88
|
-
String(date.getUTCDate()).padStart(2, "0"),
|
|
89
|
-
].join("-");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function offsetMsForTimezone(date: Date, timezone: string): number {
|
|
93
|
-
const parts = datePartsInTimezone(date, timezone);
|
|
94
|
-
const asUtc = Date.UTC(
|
|
95
|
-
parts.year,
|
|
96
|
-
parts.month - 1,
|
|
97
|
-
parts.day,
|
|
98
|
-
parts.hour,
|
|
99
|
-
parts.minute,
|
|
100
|
-
parts.second,
|
|
101
|
-
);
|
|
102
|
-
return asUtc - date.getTime();
|
|
103
|
-
}
|
|
55
|
+
export const dateOnlyInTimezone = dateKeyInTimezone;
|
|
56
|
+
export const addDaysToDateOnly = addDaysToDateKey;
|
|
104
57
|
|
|
105
58
|
export function zonedDateTimeToUtcIso(
|
|
106
59
|
dateOnly: string,
|
|
107
60
|
time: string,
|
|
108
61
|
timezone: string,
|
|
109
62
|
): string {
|
|
110
|
-
|
|
111
|
-
const [hour, minute] = time.split(":").map(Number);
|
|
112
|
-
const wallClockUtc = Date.UTC(year, month - 1, day, hour, minute, 0);
|
|
113
|
-
const offsets = new Set<number>();
|
|
114
|
-
for (let hours = -36; hours <= 36; hours += 6) {
|
|
115
|
-
offsets.add(
|
|
116
|
-
offsetMsForTimezone(
|
|
117
|
-
new Date(wallClockUtc + hours * 60 * 60 * 1000),
|
|
118
|
-
timezone,
|
|
119
|
-
),
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const candidates = [...offsets]
|
|
124
|
-
.map((offset) => new Date(wallClockUtc - offset))
|
|
125
|
-
.map((candidate) => ({
|
|
126
|
-
candidate,
|
|
127
|
-
localWallClock:
|
|
128
|
-
candidate.getTime() + offsetMsForTimezone(candidate, timezone),
|
|
129
|
-
}))
|
|
130
|
-
.sort((a, b) => {
|
|
131
|
-
const aDelta = a.localWallClock - wallClockUtc;
|
|
132
|
-
const bDelta = b.localWallClock - wallClockUtc;
|
|
133
|
-
if (aDelta === 0 && bDelta === 0) {
|
|
134
|
-
return a.candidate.getTime() - b.candidate.getTime();
|
|
135
|
-
}
|
|
136
|
-
if (aDelta >= 0 && bDelta < 0) return -1;
|
|
137
|
-
if (aDelta < 0 && bDelta >= 0) return 1;
|
|
138
|
-
return Math.abs(aDelta) - Math.abs(bDelta);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
return candidates[0].candidate.toISOString();
|
|
63
|
+
return dateTimeInTimezoneToIso(dateOnly, time, timezone);
|
|
142
64
|
}
|
|
143
65
|
|
|
144
66
|
function normalizeDateBound(value: string, timezone: string): string {
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_CALENDAR_WEEK_START,
|
|
4
4
|
isCalendarWeekStart,
|
|
5
5
|
} from "./calendar-week.js";
|
|
6
|
+
import { isCalendarTimezone } from "./timezone.js";
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_SETTINGS: Settings = {
|
|
8
9
|
timezone: "America/New_York",
|
|
@@ -12,33 +13,38 @@ export const DEFAULT_SETTINGS: Settings = {
|
|
|
12
13
|
weekStart: DEFAULT_CALENDAR_WEEK_START,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
export function normalizeCalendarSettings(
|
|
16
|
+
export function normalizeCalendarSettings(
|
|
17
|
+
input: unknown,
|
|
18
|
+
fallbacks?: Partial<Settings>,
|
|
19
|
+
): Settings {
|
|
20
|
+
const defaults = fallbacks
|
|
21
|
+
? normalizeCalendarSettings(fallbacks)
|
|
22
|
+
: DEFAULT_SETTINGS;
|
|
16
23
|
const raw =
|
|
17
24
|
input && typeof input === "object"
|
|
18
25
|
? (input as Partial<Settings>)
|
|
19
26
|
: ({} as Partial<Settings>);
|
|
20
27
|
|
|
21
28
|
return {
|
|
22
|
-
timezone:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
: DEFAULT_SETTINGS.timezone,
|
|
29
|
+
timezone: isCalendarTimezone(raw.timezone)
|
|
30
|
+
? raw.timezone
|
|
31
|
+
: defaults.timezone,
|
|
26
32
|
bookingPageTitle:
|
|
27
33
|
typeof raw.bookingPageTitle === "string"
|
|
28
34
|
? raw.bookingPageTitle
|
|
29
|
-
:
|
|
35
|
+
: defaults.bookingPageTitle,
|
|
30
36
|
bookingPageDescription:
|
|
31
37
|
typeof raw.bookingPageDescription === "string"
|
|
32
38
|
? raw.bookingPageDescription
|
|
33
|
-
:
|
|
39
|
+
: defaults.bookingPageDescription,
|
|
34
40
|
defaultEventDuration:
|
|
35
41
|
typeof raw.defaultEventDuration === "number" &&
|
|
36
42
|
Number.isFinite(raw.defaultEventDuration) &&
|
|
37
43
|
raw.defaultEventDuration > 0
|
|
38
44
|
? raw.defaultEventDuration
|
|
39
|
-
:
|
|
45
|
+
: defaults.defaultEventDuration,
|
|
40
46
|
weekStart: isCalendarWeekStart(raw.weekStart)
|
|
41
47
|
? raw.weekStart
|
|
42
|
-
:
|
|
48
|
+
: defaults.weekStart,
|
|
43
49
|
};
|
|
44
50
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constructing an `Intl.DateTimeFormat` costs ~45µs, and resolving one wall
|
|
3
|
+
* clock probes the zone a dozen times — so formatters are cached per zone and
|
|
4
|
+
* option set. Only the date varies per call, and that is an argument to
|
|
5
|
+
* `format`/`formatToParts`, never part of the formatter.
|
|
6
|
+
*/
|
|
7
|
+
const formatterCache = new Map<string, Intl.DateTimeFormat>();
|
|
8
|
+
|
|
9
|
+
export function timezoneFormatter(
|
|
10
|
+
timezone: string | undefined,
|
|
11
|
+
options: Intl.DateTimeFormatOptions,
|
|
12
|
+
locale?: string,
|
|
13
|
+
): Intl.DateTimeFormat {
|
|
14
|
+
const key = `${locale ?? ""}\u0000${timezone ?? ""}\u0000${JSON.stringify(options)}`;
|
|
15
|
+
let formatter = formatterCache.get(key);
|
|
16
|
+
if (!formatter) {
|
|
17
|
+
formatter = new Intl.DateTimeFormat(locale, {
|
|
18
|
+
...options,
|
|
19
|
+
timeZone: timezone,
|
|
20
|
+
});
|
|
21
|
+
formatterCache.set(key, formatter);
|
|
22
|
+
}
|
|
23
|
+
return formatter;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether a value names a zone this calendar can use. Only a `RangeError` means
|
|
28
|
+
* Intl rejected the zone; any other failure is a real fault and must surface
|
|
29
|
+
* instead of being reported as "invalid".
|
|
30
|
+
*
|
|
31
|
+
* Several older helpers around the template still run their own version of this
|
|
32
|
+
* check with a bare `catch`; prefer this one and delete those as you touch them.
|
|
33
|
+
*/
|
|
34
|
+
export function isCalendarTimezone(value: unknown): value is string {
|
|
35
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
36
|
+
try {
|
|
37
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value }).format();
|
|
38
|
+
return true;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof RangeError) return false;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const OFFSET_PROBE_OPTIONS: Intl.DateTimeFormatOptions = {
|
|
46
|
+
hourCycle: "h23",
|
|
47
|
+
year: "numeric",
|
|
48
|
+
month: "2-digit",
|
|
49
|
+
day: "2-digit",
|
|
50
|
+
hour: "2-digit",
|
|
51
|
+
minute: "2-digit",
|
|
52
|
+
second: "2-digit",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function offsetMsInTimezone(date: Date, timezone: string): number {
|
|
56
|
+
const parts = timezoneFormatter(
|
|
57
|
+
timezone,
|
|
58
|
+
OFFSET_PROBE_OPTIONS,
|
|
59
|
+
"en-US",
|
|
60
|
+
).formatToParts(date);
|
|
61
|
+
const values = new Map(parts.map((part) => [part.type, part.value]));
|
|
62
|
+
const asUtc = Date.UTC(
|
|
63
|
+
Number(values.get("year")),
|
|
64
|
+
Number(values.get("month")) - 1,
|
|
65
|
+
Number(values.get("day")),
|
|
66
|
+
Number(values.get("hour")),
|
|
67
|
+
Number(values.get("minute")),
|
|
68
|
+
Number(values.get("second")),
|
|
69
|
+
);
|
|
70
|
+
return asUtc - date.getTime();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function dateTimeInTimezoneToIso(
|
|
74
|
+
date: string,
|
|
75
|
+
time: string,
|
|
76
|
+
timezone: string,
|
|
77
|
+
): string {
|
|
78
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
79
|
+
const [hour, minute] = time.split(":").map(Number);
|
|
80
|
+
const wallClockUtc = Date.UTC(year, month - 1, day, hour, minute, 0);
|
|
81
|
+
const offsets = new Set<number>();
|
|
82
|
+
for (let hours = -36; hours <= 36; hours += 6) {
|
|
83
|
+
offsets.add(
|
|
84
|
+
offsetMsInTimezone(
|
|
85
|
+
new Date(wallClockUtc + hours * 60 * 60 * 1000),
|
|
86
|
+
timezone,
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const candidates = [...offsets]
|
|
92
|
+
.map((offset) => new Date(wallClockUtc - offset))
|
|
93
|
+
.map((candidate) => ({
|
|
94
|
+
candidate,
|
|
95
|
+
localWallClock:
|
|
96
|
+
candidate.getTime() + offsetMsInTimezone(candidate, timezone),
|
|
97
|
+
}))
|
|
98
|
+
.sort((a, b) => {
|
|
99
|
+
const aDelta = a.localWallClock - wallClockUtc;
|
|
100
|
+
const bDelta = b.localWallClock - wallClockUtc;
|
|
101
|
+
if (aDelta === 0 && bDelta === 0) {
|
|
102
|
+
return a.candidate.getTime() - b.candidate.getTime();
|
|
103
|
+
}
|
|
104
|
+
if (aDelta >= 0 && bDelta < 0) return -1;
|
|
105
|
+
if (aDelta < 0 && bDelta >= 0) return 1;
|
|
106
|
+
return Math.abs(aDelta) - Math.abs(bDelta);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return candidates[0].candidate.toISOString();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function dateKeyInTimezone(date: Date, timezone: string): string {
|
|
113
|
+
const parts = timezoneFormatter(
|
|
114
|
+
timezone,
|
|
115
|
+
{ year: "numeric", month: "2-digit", day: "2-digit" },
|
|
116
|
+
"en-CA",
|
|
117
|
+
).formatToParts(date);
|
|
118
|
+
const value = (type: string) =>
|
|
119
|
+
parts.find((part) => part.type === type)!.value;
|
|
120
|
+
return `${value("year")}-${value("month")}-${value("day")}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function addDaysToDateKey(date: string, amount: number): string {
|
|
124
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
125
|
+
const shifted = new Date(Date.UTC(year, month - 1, day + amount));
|
|
126
|
+
return shifted.toISOString().slice(0, 10);
|
|
127
|
+
}
|
|
@@ -76,9 +76,11 @@ decisions, feedback, agent runs, and provider audit records.
|
|
|
76
76
|
|
|
77
77
|
Rules start in shadow mode; hard guards apply. Organization automations use
|
|
78
78
|
stored prompts; external mutations require durable, idempotent runs and
|
|
79
|
-
provider confirmation.
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
provider confirmation. Poll and Builder/PR dispatch run only as this factory's
|
|
80
|
+
scheduled job, not chat and not a workspace-owner email match; teammates may
|
|
81
|
+
edit and Run now Factory jobs. Use the visual editor for graph changes and
|
|
82
|
+
agent chat for proposals; persist complete graphs with `save-factory-graph`.
|
|
83
|
+
Change rules through triage actions, never graph JSON.
|
|
82
84
|
|
|
83
85
|
## Source Changes
|
|
84
86
|
|
|
@@ -2,6 +2,7 @@ import { defineAction } from "@agent-native/core/action";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
import { resolveConnectorSecret } from "../server/connectors/credentials.js";
|
|
5
|
+
import { requireFactoryAutomation } from "../server/lib/require-factory-automation.js";
|
|
5
6
|
import {
|
|
6
7
|
requireWorkspaceMember,
|
|
7
8
|
workspaceMemberIdentityFromContext,
|
|
@@ -106,6 +107,11 @@ export function createBabysitPullRequestAction(
|
|
|
106
107
|
const { userEmail, orgId } = await requireWorkspaceMember(
|
|
107
108
|
workspaceMemberIdentityFromContext(context),
|
|
108
109
|
);
|
|
110
|
+
await requireFactoryAutomation(
|
|
111
|
+
context,
|
|
112
|
+
{ userEmail, orgId },
|
|
113
|
+
"prBabysit",
|
|
114
|
+
);
|
|
109
115
|
const { comments, truncated } = await fetchComments({
|
|
110
116
|
repo: input.repo,
|
|
111
117
|
pullRequestNumber: input.pullRequestNumber,
|
|
@@ -47,7 +47,7 @@ export default defineAction({
|
|
|
47
47
|
factoryId,
|
|
48
48
|
);
|
|
49
49
|
return Promise.all(
|
|
50
|
-
scoped.map(async ({ resource, name, meta, body
|
|
50
|
+
scoped.map(async ({ resource, name, meta, body }) => {
|
|
51
51
|
const runs = await listAutomationRuns({
|
|
52
52
|
owners: [resource.owner],
|
|
53
53
|
automation: automationRunHistoryKey(resource.path),
|
|
@@ -72,7 +72,7 @@ export default defineAction({
|
|
|
72
72
|
Number.isFinite(resource.updatedAt) && resource.updatedAt > 0
|
|
73
73
|
? new Date(resource.updatedAt).toISOString()
|
|
74
74
|
: null,
|
|
75
|
-
canUpdate,
|
|
75
|
+
canUpdate: true,
|
|
76
76
|
runs: runs.filter((run) => run.path === resource.path),
|
|
77
77
|
};
|
|
78
78
|
}),
|
|
@@ -76,11 +76,6 @@ export default defineAction({
|
|
|
76
76
|
"Factory automation id and name do not refer to the same automation.",
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
|
-
if (!definition.canUpdate) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
"Only the automation's creator or an organization admin can update it.",
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
79
|
if (definition.meta.triggerType === "schedule" && !isValidCron(schedule)) {
|
|
85
80
|
throw new Error(`Invalid cron expression "${schedule}".`);
|
|
86
81
|
}
|
|
@@ -352,6 +352,14 @@ export function resolveAutomationDisplayName(
|
|
|
352
352
|
return readAutomationDisplayName(content) ?? automationName;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
export function assignCreatedByIfMissing(
|
|
356
|
+
content: string,
|
|
357
|
+
createdBy: string,
|
|
358
|
+
): string {
|
|
359
|
+
if (readFrontmatterField(content, "createdBy")) return content;
|
|
360
|
+
return setAutomationFrontmatterField(content, "createdBy", createdBy);
|
|
361
|
+
}
|
|
362
|
+
|
|
355
363
|
export function setAutomationFrontmatterField(
|
|
356
364
|
content: string,
|
|
357
365
|
key: string,
|
|
@@ -25,12 +25,6 @@ const FACTORY_AUTOMATION_NAMES = {
|
|
|
25
25
|
|
|
26
26
|
export type FactoryAutomationRole = keyof typeof FACTORY_AUTOMATION_NAMES;
|
|
27
27
|
|
|
28
|
-
function workspaceOwnerEmail(): string | undefined {
|
|
29
|
-
const email = process.env.WORKSPACE_OWNER_EMAIL?.trim().toLowerCase(); // guard:allow-env-credential - deployment owner identity, not a user credential
|
|
30
|
-
if (!email || /[\r\n]/.test(email)) return undefined;
|
|
31
|
-
return email;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
28
|
export async function requireFactoryAutomation(
|
|
35
29
|
context: ActionRunContext | undefined,
|
|
36
30
|
identity: Pick<WorkspaceMemberIdentity, "userEmail" | "orgId">,
|
|
@@ -61,15 +55,13 @@ export async function requireFactoryAutomation(
|
|
|
61
55
|
"organization",
|
|
62
56
|
)
|
|
63
57
|
).find((entry) => entry.resource.id === lineage.triggerId);
|
|
64
|
-
const ownerEmail = workspaceOwnerEmail();
|
|
65
58
|
if (
|
|
66
59
|
!definition ||
|
|
67
60
|
definition.name !== lineage.triggerName ||
|
|
68
61
|
definition.meta.domain !== "factory" ||
|
|
69
62
|
definition.meta.orgId !== identity.orgId ||
|
|
70
63
|
definition.meta.runAs !== "creator" ||
|
|
71
|
-
!
|
|
72
|
-
definition.meta.createdBy?.trim().toLowerCase() !== ownerEmail
|
|
64
|
+
!definition.meta.createdBy?.trim()
|
|
73
65
|
) {
|
|
74
66
|
throw new Error(
|
|
75
67
|
"The action was not invoked by a governed Factory automation.",
|
|
@@ -21,6 +21,7 @@ import { triageConfig } from "../db/schema.js";
|
|
|
21
21
|
import { repairFactoryAutomationsFromConfig } from "../lib/factory-automation-repair.js";
|
|
22
22
|
import {
|
|
23
23
|
DEFAULT_FACTORY_ID,
|
|
24
|
+
assignCreatedByIfMissing,
|
|
24
25
|
factoryAutomationJobPath,
|
|
25
26
|
factoryConfigRowId,
|
|
26
27
|
patchAutomationResource,
|
|
@@ -541,7 +542,7 @@ export async function ensureFactoryAutomations(
|
|
|
541
542
|
repaired = setFrontmatterField(repaired, "appId", "factory");
|
|
542
543
|
repaired = setFrontmatterField(repaired, "orgId", orgId);
|
|
543
544
|
repaired = setFrontmatterField(repaired, "factoryId", factoryId);
|
|
544
|
-
repaired =
|
|
545
|
+
repaired = assignCreatedByIfMissing(repaired, ownerEmail);
|
|
545
546
|
repaired = setFrontmatterField(repaired, "runAs", "creator");
|
|
546
547
|
if (!frontmatterField(repaired, "model")) {
|
|
547
548
|
repaired = setFrontmatterField(repaired, "model", seed.model);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { isPostgres } from "@agent-native/core/db";
|
|
2
3
|
import { buildDeepLink } from "@agent-native/core/server";
|
|
3
4
|
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
4
5
|
import { accessFilter } from "@agent-native/core/sharing";
|
|
@@ -13,6 +14,16 @@ function slidesDeepLink(): string {
|
|
|
13
14
|
return buildDeepLink({ app: "slides", view: "list" });
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function parseJsonProjection(value: unknown, label: string): unknown {
|
|
18
|
+
if (value === null || value === undefined) return undefined;
|
|
19
|
+
if (typeof value !== "string") return value;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(value);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new Error(`Invalid ${label} JSON projection`, { cause: error });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
export default defineAction({
|
|
17
28
|
description: "List all decks from the database with metadata.",
|
|
18
29
|
schema: z.object({
|
|
@@ -26,13 +37,20 @@ export default defineAction({
|
|
|
26
37
|
.describe(
|
|
27
38
|
"Set to 'true' for full frontend deck payloads; omitted returns metadata only",
|
|
28
39
|
),
|
|
40
|
+
includePreview: z
|
|
41
|
+
.enum(["true", "false"])
|
|
42
|
+
.optional()
|
|
43
|
+
.describe(
|
|
44
|
+
"Set to 'true' with light mode to include only the first slide preview",
|
|
45
|
+
),
|
|
29
46
|
light: z
|
|
30
47
|
.enum(["true", "false"])
|
|
31
48
|
.optional()
|
|
32
49
|
.describe(
|
|
33
50
|
"Set to 'true' for a minimal id/title/updatedAt/visibility listing " +
|
|
34
51
|
"used for cheap add/remove diffing (e.g. background polling). " +
|
|
35
|
-
"
|
|
52
|
+
"By default never reads the deck body — no slides, no slideCount. " +
|
|
53
|
+
"Use includePreview for the first slide only.",
|
|
36
54
|
),
|
|
37
55
|
createdBy: z
|
|
38
56
|
.enum(["all", "me"])
|
|
@@ -50,7 +68,7 @@ export default defineAction({
|
|
|
50
68
|
const ownerEmail = getRequestUserEmail();
|
|
51
69
|
const normalizedOwnerEmail = normalizeOwnerEmail(ownerEmail);
|
|
52
70
|
if (
|
|
53
|
-
args.includeSlides === "true" &&
|
|
71
|
+
(args.includeSlides === "true" || args.includePreview === "true") &&
|
|
54
72
|
ctx?.caller === "frontend" &&
|
|
55
73
|
normalizedOwnerEmail === null
|
|
56
74
|
) {
|
|
@@ -75,9 +93,65 @@ export default defineAction({
|
|
|
75
93
|
if (args.light === "true") {
|
|
76
94
|
// Column-projected listing for cheap add/remove diffing (the client's
|
|
77
95
|
// background poll and SSE-reconnect resync). The `data` column holds
|
|
78
|
-
// each deck's entire slide JSON and can be large
|
|
79
|
-
//
|
|
80
|
-
//
|
|
96
|
+
// each deck's entire slide JSON and can be large. The home grid opts
|
|
97
|
+
// into the separate preview projection; polling keeps the metadata-only
|
|
98
|
+
// path below.
|
|
99
|
+
if (args.includePreview === "true") {
|
|
100
|
+
// Keep the list bounded at the database boundary. `data` is an opaque
|
|
101
|
+
// full-deck blob, so selecting it and parsing it here scales with every
|
|
102
|
+
// slide even though the caller only needs the first one.
|
|
103
|
+
const previewSlideProjection = isPostgres()
|
|
104
|
+
? sql<
|
|
105
|
+
string | null
|
|
106
|
+
>`(${schema.decks.data}::jsonb -> 'slides' -> 0)::text`
|
|
107
|
+
: sql<
|
|
108
|
+
string | null
|
|
109
|
+
>`json_extract(${schema.decks.data}, '$.slides[0]')`;
|
|
110
|
+
const aspectRatioProjection = isPostgres()
|
|
111
|
+
? sql<string | null>`(${schema.decks.data}::jsonb ->> 'aspectRatio')`
|
|
112
|
+
: sql<
|
|
113
|
+
string | null
|
|
114
|
+
>`json_extract(${schema.decks.data}, '$.aspectRatio')`;
|
|
115
|
+
const rows = await db
|
|
116
|
+
.select({
|
|
117
|
+
id: schema.decks.id,
|
|
118
|
+
title: schema.decks.title,
|
|
119
|
+
updatedAt: schema.decks.updatedAt,
|
|
120
|
+
visibility: schema.decks.visibility,
|
|
121
|
+
ownerEmail: schema.decks.ownerEmail,
|
|
122
|
+
previewSlide: previewSlideProjection,
|
|
123
|
+
aspectRatio: aspectRatioProjection,
|
|
124
|
+
})
|
|
125
|
+
.from(schema.decks)
|
|
126
|
+
.where(where)
|
|
127
|
+
.orderBy(desc(schema.decks.updatedAt));
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
count: rows.length,
|
|
131
|
+
decks: rows.map((row) => {
|
|
132
|
+
const previewSlide = parseJsonProjection(
|
|
133
|
+
row.previewSlide,
|
|
134
|
+
"first slide preview",
|
|
135
|
+
);
|
|
136
|
+
return {
|
|
137
|
+
id: row.id,
|
|
138
|
+
title: row.title,
|
|
139
|
+
updatedAt: row.updatedAt,
|
|
140
|
+
visibility: row.visibility,
|
|
141
|
+
createdByMe:
|
|
142
|
+
normalizedOwnerEmail !== null &&
|
|
143
|
+
normalizeOwnerEmail(row.ownerEmail) === normalizedOwnerEmail,
|
|
144
|
+
...(previewSlide && typeof previewSlide === "object"
|
|
145
|
+
? { previewSlide }
|
|
146
|
+
: {}),
|
|
147
|
+
...(typeof row.aspectRatio === "string"
|
|
148
|
+
? { aspectRatio: row.aspectRatio }
|
|
149
|
+
: {}),
|
|
150
|
+
};
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
81
155
|
const rows = await db
|
|
82
156
|
.select({
|
|
83
157
|
id: schema.decks.id,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
IconCopy,
|
|
9
9
|
IconPencil,
|
|
10
10
|
IconPlus,
|
|
11
|
+
IconShare2,
|
|
11
12
|
IconStar,
|
|
12
13
|
IconStarFilled,
|
|
13
14
|
} from "@tabler/icons-react";
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
import type { Deck } from "@/context/DeckContext";
|
|
25
26
|
import { getDeckListingPreviewFrameStyle } from "@/lib/deck-preview-frame";
|
|
26
27
|
|
|
28
|
+
import ShareDialog from "../editor/ShareDialog";
|
|
27
29
|
import SlideRenderer from "./SlideRenderer";
|
|
28
30
|
|
|
29
31
|
interface DeckCardProps {
|
|
@@ -48,16 +50,18 @@ export default function DeckCard({
|
|
|
48
50
|
onSetWorkspaceDefault,
|
|
49
51
|
}: DeckCardProps) {
|
|
50
52
|
const t = useT();
|
|
51
|
-
const firstSlide = deck.slides?.[0];
|
|
53
|
+
const firstSlide = deck.previewSlide ?? deck.slides?.[0];
|
|
52
54
|
const previewFrameStyle = getDeckListingPreviewFrameStyle(deck.aspectRatio);
|
|
53
55
|
const [isRenaming, setIsRenaming] = useState(false);
|
|
54
56
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
55
57
|
const [renameValue, setRenameValue] = useState(deck.title);
|
|
56
58
|
const [contextOpen, setContextOpen] = useState(false);
|
|
59
|
+
const [shareOpen, setShareOpen] = useState(false);
|
|
57
60
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
58
61
|
const pendingRenameRef = useRef(false);
|
|
59
62
|
const pendingDeleteRef = useRef(false);
|
|
60
63
|
const pendingWorkspaceDefaultRef = useRef(false);
|
|
64
|
+
const pendingShareRef = useRef(false);
|
|
61
65
|
|
|
62
66
|
useEffect(() => {
|
|
63
67
|
if (isRenaming) {
|
|
@@ -206,6 +210,11 @@ export default function DeckCard({
|
|
|
206
210
|
pendingWorkspaceDefaultRef.current = false;
|
|
207
211
|
onSetWorkspaceDefault?.(deck.id, !isWorkspaceDefault);
|
|
208
212
|
}
|
|
213
|
+
if (pendingShareRef.current) {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
pendingShareRef.current = false;
|
|
216
|
+
setTimeout(() => setShareOpen(true), 0);
|
|
217
|
+
}
|
|
209
218
|
if (pendingDeleteRef.current) {
|
|
210
219
|
e.preventDefault();
|
|
211
220
|
pendingDeleteRef.current = false;
|
|
@@ -226,6 +235,16 @@ export default function DeckCard({
|
|
|
226
235
|
<IconCopy className="w-3.5 h-3.5 me-2" />
|
|
227
236
|
Duplicate
|
|
228
237
|
</DropdownMenuItem>
|
|
238
|
+
<DropdownMenuItem
|
|
239
|
+
onSelect={(event) => {
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
pendingShareRef.current = true;
|
|
242
|
+
setMenuOpen(false);
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
<IconShare2 className="w-3.5 h-3.5 me-2" />
|
|
246
|
+
{t("share.title")}
|
|
247
|
+
</DropdownMenuItem>
|
|
229
248
|
<DropdownMenuItem
|
|
230
249
|
onSelect={(event) => {
|
|
231
250
|
event.preventDefault();
|
|
@@ -279,6 +298,7 @@ export default function DeckCard({
|
|
|
279
298
|
}}
|
|
280
299
|
canManage={deck.createdByMe}
|
|
281
300
|
/>
|
|
301
|
+
<ShareDialog deck={deck} open={shareOpen} onOpenChange={setShareOpen} />
|
|
282
302
|
</div>
|
|
283
303
|
);
|
|
284
304
|
}
|