@agent-native/core 0.141.2 → 0.141.4
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/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
- package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
- package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-durable-a2a-workers-avoid-startup-migration-contention.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +1 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
- package/corpus/templates/analytics/server/lib/emails.ts +51 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
- package/corpus/templates/analytics/server/lib/gong.ts +132 -33
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
- package/corpus/templates/analytics/server/plugins/db.ts +35 -10
- package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
- package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
- package/corpus/templates/calendar/server/lib/emails.ts +167 -0
- package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
- package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/clips/actions/invite-member.ts +39 -16
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
- package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
- package/corpus/templates/clips/desktop/src/styles.css +0 -3
- package/corpus/templates/clips/server/lib/emails.ts +256 -0
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
- package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
- package/corpus/templates/clips/shared/email-preview.ts +61 -0
- package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
- package/corpus/templates/content/server/lib/emails.ts +70 -0
- package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
- package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
- package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
- package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
- package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
- package/corpus/templates/factory/AGENTS.md +18 -17
- package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
- package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
- package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
- package/corpus/templates/factory/app/routes/factory.tsx +376 -206
- package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
- package/corpus/templates/factory/server/db/schema.ts +6 -0
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
- package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
- package/corpus/templates/forms/server/lib/emails.ts +52 -0
- package/corpus/templates/forms/server/lib/response-email.ts +2 -0
- package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
- package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
- package/corpus/templates/plan/server/lib/emails.ts +67 -0
- package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
- package/corpus/templates/slides/actions/import-from-url.ts +1 -0
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
- package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
- package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
- package/corpus/templates/slides/app/pages/Index.tsx +127 -137
- package/corpus/templates/slides/app/root.tsx +8 -0
- package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
- package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
- package/corpus/templates/slides/server/lib/emails.ts +40 -0
- package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
- package/dist/brand-kit/index.d.ts +1 -0
- package/dist/brand-kit/index.js +1 -0
- package/dist/brand-kit/tokens.d.ts +91 -0
- package/dist/brand-kit/tokens.js +279 -0
- package/dist/brand-kit/types.d.ts +25 -0
- package/dist/client/AssistantChat.js +7 -7
- package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
- package/dist/client/onboarding/first-run-registry.d.ts +25 -0
- package/dist/client/onboarding/first-run-registry.js +18 -0
- package/dist/client/onboarding/index.d.ts +2 -0
- package/dist/client/onboarding/index.js +1 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/migrations.d.ts +13 -3
- package/dist/db/migrations.js +19 -4
- package/dist/deploy/build.d.ts +8 -0
- package/dist/deploy/build.js +92 -3
- package/dist/deploy/workspace-deploy.js +86 -3
- package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
- package/dist/email-catalog/actions/list-email-activity.js +19 -0
- package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
- package/dist/email-catalog/actions/list-email-engagement.js +15 -0
- package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
- package/dist/email-catalog/actions/list-email-log.js +23 -0
- package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
- package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
- package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
- package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
- package/dist/email-catalog/authorize.d.ts +1 -0
- package/dist/email-catalog/authorize.js +9 -0
- package/dist/email-catalog/log.d.ts +60 -0
- package/dist/email-catalog/log.js +142 -0
- package/dist/email-catalog/provider-metrics.d.ts +34 -0
- package/dist/email-catalog/provider-metrics.js +182 -0
- package/dist/email-catalog/registry.d.ts +78 -0
- package/dist/email-catalog/registry.js +70 -0
- package/dist/email-catalog/schema.d.ts +231 -0
- package/dist/email-catalog/schema.js +53 -0
- package/dist/email-catalog/system-emails.d.ts +13 -0
- package/dist/email-catalog/system-emails.js +67 -0
- package/dist/jobs/index.d.ts +2 -0
- package/dist/jobs/index.js +2 -0
- package/dist/jobs/run-history.js +48 -1
- package/dist/jobs/scheduler-dispatch.d.ts +7 -0
- package/dist/jobs/scheduler-dispatch.js +7 -0
- package/dist/jobs/scheduler-health.d.ts +22 -0
- package/dist/jobs/scheduler-health.js +172 -0
- package/dist/jobs/scheduler.js +67 -2
- package/dist/localization/default-messages.d.ts +45 -0
- package/dist/localization/default-messages.js +45 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/handlers.js +9 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/server/action-discovery.js +22 -0
- package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
- package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
- package/dist/server/agent-chat-plugin.d.ts +2 -1
- package/dist/server/agent-chat-plugin.js +69 -22
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/better-auth-instance.js +17 -2
- package/dist/server/design-token-utils.d.ts +6 -1
- package/dist/server/design-token-utils.js +252 -59
- package/dist/server/email.d.ts +11 -0
- package/dist/server/email.js +54 -3
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.js +3 -2
- package/dist/vite/client.js +20 -14
- package/package.json +2 -1
|
@@ -120,6 +120,12 @@ export const triageConfig = table("factory_config", {
|
|
|
120
120
|
sentryProjectSlug: text("sentry_project_slug"),
|
|
121
121
|
sentryEnvironment: text("sentry_environment"),
|
|
122
122
|
lastSentrySeenAt: text("last_sentry_seen_at"),
|
|
123
|
+
automationFailureAlertsEnabled: integer("automation_failure_alerts_enabled")
|
|
124
|
+
.notNull()
|
|
125
|
+
.default(1),
|
|
126
|
+
automationFailureAlertEmail: text("automation_failure_alert_email"),
|
|
127
|
+
lastAutomationFailureAlertKey: text("last_automation_failure_alert_key"),
|
|
128
|
+
lastAutomationFailureAlertAt: text("last_automation_failure_alert_at"),
|
|
123
129
|
createdAt: text("created_at").notNull().default(now()),
|
|
124
130
|
updatedAt: text("updated_at").notNull().default(now()),
|
|
125
131
|
ownerEmail: text("owner_email").notNull(),
|
|
@@ -248,6 +248,16 @@ const migrations = [
|
|
|
248
248
|
ALTER TABLE factory_config ADD COLUMN last_sentry_seen_at TEXT;
|
|
249
249
|
`,
|
|
250
250
|
},
|
|
251
|
+
{
|
|
252
|
+
version: 17,
|
|
253
|
+
name: "factory-automation-failure-alert-settings",
|
|
254
|
+
sql: `
|
|
255
|
+
ALTER TABLE factory_config ADD COLUMN automation_failure_alerts_enabled INTEGER NOT NULL DEFAULT 1;
|
|
256
|
+
ALTER TABLE factory_config ADD COLUMN automation_failure_alert_email TEXT;
|
|
257
|
+
ALTER TABLE factory_config ADD COLUMN last_automation_failure_alert_key TEXT;
|
|
258
|
+
ALTER TABLE factory_config ADD COLUMN last_automation_failure_alert_at TEXT;
|
|
259
|
+
`,
|
|
260
|
+
},
|
|
251
261
|
];
|
|
252
262
|
|
|
253
263
|
export const runFactoryMigrations = runMigrations(migrations, {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { subscribe } from "@agent-native/core/event-bus";
|
|
2
|
+
import { notify } from "@agent-native/core/notifications";
|
|
1
3
|
import { resolveOrgIdForEmail } from "@agent-native/core/org";
|
|
2
4
|
import {
|
|
3
5
|
organizationResourceOwner,
|
|
@@ -5,8 +7,11 @@ import {
|
|
|
5
7
|
resourcePut,
|
|
6
8
|
WORKSPACE_OWNER,
|
|
7
9
|
} from "@agent-native/core/resources";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
+
import {
|
|
11
|
+
defineNitroPlugin,
|
|
12
|
+
runWithRequestContext,
|
|
13
|
+
} from "@agent-native/core/server";
|
|
14
|
+
import { and, eq, isNull, lt, ne, or } from "drizzle-orm";
|
|
10
15
|
|
|
11
16
|
import { getDb } from "../db/index.js";
|
|
12
17
|
import { triageConfig } from "../db/schema.js";
|
|
@@ -14,6 +19,139 @@ import { triageConfig } from "../db/schema.js";
|
|
|
14
19
|
const LEGACY_JOB_PATH = "jobs/factory-observation-scheduler.md";
|
|
15
20
|
const DEFAULT_SLACK_CHANNEL_ID = "C0ATH3CCZT4";
|
|
16
21
|
const DEFAULT_SLACK_CHANNEL_NAME = "product-agent-native-feedback";
|
|
22
|
+
const FAILURE_ALERT_COOLDOWN_MS = 15 * 60_000;
|
|
23
|
+
|
|
24
|
+
type AutomationRunFinishedEvent = {
|
|
25
|
+
automationRunId: string;
|
|
26
|
+
owner: string;
|
|
27
|
+
automation: string;
|
|
28
|
+
path: string;
|
|
29
|
+
orgId: string | null;
|
|
30
|
+
runId: string | null;
|
|
31
|
+
threadId: string | null;
|
|
32
|
+
status: "success" | "error" | "interrupted";
|
|
33
|
+
error: string | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let failureAlertSubscription: string | null = null;
|
|
37
|
+
|
|
38
|
+
function factoryPublicUrl(): string | undefined {
|
|
39
|
+
const value = process.env.FACTORY_PUBLIC_URL?.trim(); // guard:allow-env-credential - public callback origin, not a credential
|
|
40
|
+
if (!value || /[\r\n]/.test(value)) return undefined;
|
|
41
|
+
return value.replace(/\/+$/, "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function notifyFactoryAutomationFailure(
|
|
45
|
+
event: AutomationRunFinishedEvent,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
if (
|
|
48
|
+
(event.status !== "error" && event.status !== "interrupted") ||
|
|
49
|
+
!event.orgId ||
|
|
50
|
+
!event.path.startsWith("jobs/factory-")
|
|
51
|
+
) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const db = getDb();
|
|
56
|
+
const config = (
|
|
57
|
+
await db
|
|
58
|
+
.select({
|
|
59
|
+
ownerEmail: triageConfig.ownerEmail,
|
|
60
|
+
alertsEnabled: triageConfig.automationFailureAlertsEnabled,
|
|
61
|
+
alertEmail: triageConfig.automationFailureAlertEmail,
|
|
62
|
+
})
|
|
63
|
+
.from(triageConfig)
|
|
64
|
+
.where(
|
|
65
|
+
and(
|
|
66
|
+
eq(triageConfig.id, event.orgId),
|
|
67
|
+
eq(triageConfig.orgId, event.orgId),
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
.limit(1)
|
|
71
|
+
)[0];
|
|
72
|
+
if (!config || config.alertsEnabled !== 1) return;
|
|
73
|
+
|
|
74
|
+
const recipient = (config.alertEmail || config.ownerEmail || "")
|
|
75
|
+
.trim()
|
|
76
|
+
.toLowerCase();
|
|
77
|
+
if (!recipient) return;
|
|
78
|
+
|
|
79
|
+
const error =
|
|
80
|
+
event.error ||
|
|
81
|
+
"The automation ended without recording a terminal result. No delivery was confirmed.";
|
|
82
|
+
const alertKey = `${event.automation}\n${error}`.slice(0, 700);
|
|
83
|
+
const now = new Date();
|
|
84
|
+
const cutoff = new Date(now.getTime() - FAILURE_ALERT_COOLDOWN_MS);
|
|
85
|
+
const claimed = await db
|
|
86
|
+
.update(triageConfig)
|
|
87
|
+
.set({
|
|
88
|
+
lastAutomationFailureAlertKey: alertKey,
|
|
89
|
+
lastAutomationFailureAlertAt: now.toISOString(),
|
|
90
|
+
})
|
|
91
|
+
.where(
|
|
92
|
+
and(
|
|
93
|
+
eq(triageConfig.id, event.orgId),
|
|
94
|
+
eq(triageConfig.orgId, event.orgId),
|
|
95
|
+
eq(triageConfig.automationFailureAlertsEnabled, 1),
|
|
96
|
+
or(
|
|
97
|
+
isNull(triageConfig.lastAutomationFailureAlertKey),
|
|
98
|
+
ne(triageConfig.lastAutomationFailureAlertKey, alertKey),
|
|
99
|
+
isNull(triageConfig.lastAutomationFailureAlertAt),
|
|
100
|
+
lt(triageConfig.lastAutomationFailureAlertAt, cutoff.toISOString()),
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
.returning({ id: triageConfig.id });
|
|
105
|
+
if (claimed.length === 0) return;
|
|
106
|
+
|
|
107
|
+
const url = factoryPublicUrl();
|
|
108
|
+
const debugTarget = url
|
|
109
|
+
? `${url}/factory?tab=automations`
|
|
110
|
+
: "Factory > Automations";
|
|
111
|
+
const details = [
|
|
112
|
+
`Automation: ${event.automation}`,
|
|
113
|
+
`Run: ${event.automationRunId}`,
|
|
114
|
+
`Error: ${error}`,
|
|
115
|
+
`Debug: ${debugTarget}`,
|
|
116
|
+
"Next steps: open Scheduler health, then open this run's thread and inspect the last error. If health is stale, inspect the deployed scheduled function and background worker.",
|
|
117
|
+
event.threadId ? `Agent thread: ${event.threadId}` : "",
|
|
118
|
+
event.runId ? `Agent run: ${event.runId}` : "",
|
|
119
|
+
]
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.join("\n");
|
|
122
|
+
await runWithRequestContext(
|
|
123
|
+
{ userEmail: config.ownerEmail, orgId: event.orgId },
|
|
124
|
+
() =>
|
|
125
|
+
notify(
|
|
126
|
+
{
|
|
127
|
+
severity: "critical",
|
|
128
|
+
title: `Factory automation failed: ${event.automation}`,
|
|
129
|
+
body: details,
|
|
130
|
+
metadata: {
|
|
131
|
+
emailRecipients: [recipient],
|
|
132
|
+
emailSubject: `Factory automation failed: ${event.automation}`,
|
|
133
|
+
automationRunId: event.automationRunId,
|
|
134
|
+
},
|
|
135
|
+
channels: ["inbox", "email"],
|
|
136
|
+
},
|
|
137
|
+
{ owner: config.ownerEmail },
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function subscribeToAutomationFailures(): void {
|
|
143
|
+
if (failureAlertSubscription) return;
|
|
144
|
+
failureAlertSubscription = subscribe("automation.run.finished", (payload) =>
|
|
145
|
+
notifyFactoryAutomationFailure(payload as AutomationRunFinishedEvent).catch(
|
|
146
|
+
(error) => {
|
|
147
|
+
console.error(
|
|
148
|
+
"[factory-scheduler-job] automation failure alert failed:",
|
|
149
|
+
error,
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
17
155
|
|
|
18
156
|
type AutomationSeed = {
|
|
19
157
|
name: string;
|
|
@@ -256,6 +394,7 @@ async function ensureSchedulerJobs(): Promise<void> {
|
|
|
256
394
|
}
|
|
257
395
|
|
|
258
396
|
export default defineNitroPlugin(async () => {
|
|
397
|
+
subscribeToAutomationFailures();
|
|
259
398
|
try {
|
|
260
399
|
await ensureSchedulerJobs();
|
|
261
400
|
} catch (error) {
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
IconX,
|
|
22
22
|
} from "@tabler/icons-react";
|
|
23
23
|
import { useEffect, useMemo, useState } from "react";
|
|
24
|
-
import { useNavigate } from "react-router";
|
|
24
|
+
import { Link, useNavigate } from "react-router";
|
|
25
25
|
import { toast } from "sonner";
|
|
26
26
|
|
|
27
27
|
import { CloudUpgrade } from "@/components/CloudUpgrade";
|
|
@@ -469,6 +469,9 @@ export function FormsListPage() {
|
|
|
469
469
|
<div className="forms-list-shell overflow-hidden bg-card">
|
|
470
470
|
{forms.map((form: any) => {
|
|
471
471
|
const selected = selectedIds.has(form.id);
|
|
472
|
+
const formHref = isArchive
|
|
473
|
+
? `/forms/${form.id}/responses`
|
|
474
|
+
: `/forms/${form.id}`;
|
|
472
475
|
const dateLabel =
|
|
473
476
|
isArchive && (form as any).deletedAt
|
|
474
477
|
? t("forms.deletedDate", {
|
|
@@ -481,6 +484,20 @@ export function FormsListPage() {
|
|
|
481
484
|
month: "short",
|
|
482
485
|
day: "numeric",
|
|
483
486
|
});
|
|
487
|
+
const formDetails = (
|
|
488
|
+
<>
|
|
489
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
490
|
+
<h3 className="min-w-0 flex-1 truncate text-sm font-medium">
|
|
491
|
+
{form.title}
|
|
492
|
+
</h3>
|
|
493
|
+
</div>
|
|
494
|
+
{form.description && (
|
|
495
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
496
|
+
{form.description}
|
|
497
|
+
</p>
|
|
498
|
+
)}
|
|
499
|
+
</>
|
|
500
|
+
);
|
|
484
501
|
|
|
485
502
|
return (
|
|
486
503
|
<div
|
|
@@ -496,16 +513,22 @@ export function FormsListPage() {
|
|
|
496
513
|
role="button"
|
|
497
514
|
tabIndex={0}
|
|
498
515
|
aria-pressed={selectionMode ? selected : undefined}
|
|
499
|
-
onClick={() => {
|
|
516
|
+
onClick={(event) => {
|
|
517
|
+
if (
|
|
518
|
+
event.defaultPrevented ||
|
|
519
|
+
event.metaKey ||
|
|
520
|
+
event.ctrlKey ||
|
|
521
|
+
event.shiftKey ||
|
|
522
|
+
event.altKey ||
|
|
523
|
+
event.button !== 0
|
|
524
|
+
) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
500
527
|
if (selectionMode) {
|
|
501
528
|
toggleSelection(form.id);
|
|
502
529
|
return;
|
|
503
530
|
}
|
|
504
|
-
navigate(
|
|
505
|
-
isArchive
|
|
506
|
-
? `/forms/${form.id}/responses`
|
|
507
|
-
: `/forms/${form.id}`,
|
|
508
|
-
);
|
|
531
|
+
navigate(formHref);
|
|
509
532
|
}}
|
|
510
533
|
onKeyDown={(e) => {
|
|
511
534
|
if (
|
|
@@ -517,11 +540,7 @@ export function FormsListPage() {
|
|
|
517
540
|
toggleSelection(form.id);
|
|
518
541
|
return;
|
|
519
542
|
}
|
|
520
|
-
navigate(
|
|
521
|
-
isArchive
|
|
522
|
-
? `/forms/${form.id}/responses`
|
|
523
|
-
: `/forms/${form.id}`,
|
|
524
|
-
);
|
|
543
|
+
navigate(formHref);
|
|
525
544
|
}
|
|
526
545
|
}}
|
|
527
546
|
>
|
|
@@ -538,15 +557,16 @@ export function FormsListPage() {
|
|
|
538
557
|
/>
|
|
539
558
|
)}
|
|
540
559
|
<div className="min-w-0 flex-1">
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
560
|
+
{selectionMode ? (
|
|
561
|
+
formDetails
|
|
562
|
+
) : (
|
|
563
|
+
<Link
|
|
564
|
+
to={formHref}
|
|
565
|
+
className="block min-w-0 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
566
|
+
onClick={(event) => event.stopPropagation()}
|
|
567
|
+
>
|
|
568
|
+
{formDetails}
|
|
569
|
+
</Link>
|
|
550
570
|
)}
|
|
551
571
|
</div>
|
|
552
572
|
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog entries for the transactional emails Forms sends.
|
|
3
|
+
*
|
|
4
|
+
* Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
|
|
5
|
+
* and preview them without the app having sent anything yet.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
|
|
9
|
+
|
|
10
|
+
import { renderNewResponseEmail } from "./response-email.js";
|
|
11
|
+
|
|
12
|
+
export const FORMS_NEW_RESPONSE_EMAIL_ID = "forms.new-response";
|
|
13
|
+
|
|
14
|
+
let registered = false;
|
|
15
|
+
|
|
16
|
+
export function registerFormsEmails(): void {
|
|
17
|
+
if (registered) return;
|
|
18
|
+
registered = true;
|
|
19
|
+
|
|
20
|
+
defineTransactionalEmail({
|
|
21
|
+
id: FORMS_NEW_RESPONSE_EMAIL_ID,
|
|
22
|
+
name: "New form response",
|
|
23
|
+
trigger:
|
|
24
|
+
"A response is submitted and persisted for a form whose settings have `emailOnNewResponses` enabled. Delivery is best-effort: a send failure never rejects the submission.",
|
|
25
|
+
recipientLabel: "Form owner",
|
|
26
|
+
recipient:
|
|
27
|
+
"The form's `ownerEmail`, and only that address — the respondent is never emailed. Skipped when the form has no owner address.",
|
|
28
|
+
senderLabel: "Default sender",
|
|
29
|
+
sender:
|
|
30
|
+
"The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`, so a reply goes to the sending mailbox rather than to the respondent.",
|
|
31
|
+
preview: () =>
|
|
32
|
+
renderNewResponseEmail({
|
|
33
|
+
formTitle: "Beta feedback",
|
|
34
|
+
fields: [
|
|
35
|
+
{ id: "name", type: "text", label: "Name", required: true },
|
|
36
|
+
{ id: "email", type: "email", label: "Email", required: true },
|
|
37
|
+
{
|
|
38
|
+
id: "notes",
|
|
39
|
+
type: "textarea",
|
|
40
|
+
label: "What should we fix first?",
|
|
41
|
+
required: false,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
data: {
|
|
45
|
+
name: "Sam Rivera",
|
|
46
|
+
email: "sam.rivera@example.com",
|
|
47
|
+
notes: "The export button is hard to find on mobile.",
|
|
48
|
+
},
|
|
49
|
+
submittedAt: "2025-03-04T17:20:00.000Z",
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { emailStrong, renderEmail, sendEmail } from "@agent-native/core/server";
|
|
2
2
|
|
|
3
3
|
import type { FormField } from "../../shared/types.js";
|
|
4
|
+
import { FORMS_NEW_RESPONSE_EMAIL_ID } from "./emails.js";
|
|
4
5
|
|
|
5
6
|
export interface NewResponseEmailArgs {
|
|
6
7
|
to: string;
|
|
@@ -61,5 +62,6 @@ export async function sendNewResponseEmail(
|
|
|
61
62
|
await sendEmail({
|
|
62
63
|
to: args.to,
|
|
63
64
|
...renderNewResponseEmail(args),
|
|
65
|
+
templateId: FORMS_NEW_RESPONSE_EMAIL_ID,
|
|
64
66
|
});
|
|
65
67
|
}
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
} from "../server/lib/local-identity.js";
|
|
23
23
|
import { newId, nowIso, planPath, writeEvent } from "../server/plans.js";
|
|
24
24
|
|
|
25
|
+
export const PLAN_ACCESS_REQUEST_EMAIL_ID = "plan.access-request";
|
|
26
|
+
|
|
25
27
|
function httpError(message: string, statusCode: number): Error {
|
|
26
28
|
return Object.assign(new Error(message), { statusCode });
|
|
27
29
|
}
|
|
@@ -49,6 +51,28 @@ function absolutePlanUrl(planId: string, kind: "plan" | "recap"): string {
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
export function renderPlanAccessRequestEmail(input: {
|
|
55
|
+
requesterName: string;
|
|
56
|
+
requesterEmail: string;
|
|
57
|
+
planTitle: string;
|
|
58
|
+
url: string;
|
|
59
|
+
}) {
|
|
60
|
+
const subject = `${input.requesterName} requested access to "${input.planTitle}"`;
|
|
61
|
+
return {
|
|
62
|
+
subject,
|
|
63
|
+
...renderEmail({
|
|
64
|
+
preheader: subject,
|
|
65
|
+
heading: "Access request",
|
|
66
|
+
paragraphs: [
|
|
67
|
+
`${emailStrong(input.requesterName)} (${emailStrong(input.requesterEmail)}) requested access to ${emailStrong(input.planTitle)}.`,
|
|
68
|
+
"Open the plan and use Share to grant access if this request should be approved.",
|
|
69
|
+
],
|
|
70
|
+
cta: { label: "Open plan", url: input.url },
|
|
71
|
+
footer: "You received this because you own this Agent-Native Plan.",
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
async function notifyOwner(input: {
|
|
53
77
|
planId: string;
|
|
54
78
|
planKind: "plan" | "recap";
|
|
@@ -62,19 +86,16 @@ async function notifyOwner(input: {
|
|
|
62
86
|
return false;
|
|
63
87
|
}
|
|
64
88
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
cta: { label: "Open plan", url },
|
|
75
|
-
footer: "You received this because you own this Agent-Native Plan.",
|
|
89
|
+
await sendEmail({
|
|
90
|
+
...renderPlanAccessRequestEmail({
|
|
91
|
+
requesterName: input.requesterName,
|
|
92
|
+
requesterEmail: input.requesterEmail,
|
|
93
|
+
planTitle: input.planTitle,
|
|
94
|
+
url: absolutePlanUrl(input.planId, input.planKind),
|
|
95
|
+
}),
|
|
96
|
+
to: input.ownerEmail,
|
|
97
|
+
templateId: PLAN_ACCESS_REQUEST_EMAIL_ID,
|
|
76
98
|
});
|
|
77
|
-
await sendEmail({ to: input.ownerEmail, subject, html, text });
|
|
78
99
|
return true;
|
|
79
100
|
}
|
|
80
101
|
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
import type { PlanBundle, PlanComment } from "../../shared/types.js";
|
|
15
15
|
import { getDb, schema } from "../db/index.js";
|
|
16
16
|
|
|
17
|
+
export const PLAN_COMMENT_EMAIL_ID = "plan.plan-comment";
|
|
18
|
+
|
|
17
19
|
type CommentNotificationInput = {
|
|
18
20
|
bundle: PlanBundle;
|
|
19
21
|
insertedCommentIds: string[];
|
|
@@ -205,40 +207,64 @@ export function planCommentNotificationRecipients(input: {
|
|
|
205
207
|
return Array.from(recipients.values());
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
export function renderPlanCommentEmail(input: {
|
|
211
|
+
actor: string;
|
|
212
|
+
app: string;
|
|
211
213
|
planTitle: string;
|
|
212
|
-
|
|
214
|
+
planUrl: string;
|
|
215
|
+
message: string;
|
|
216
|
+
isReply: boolean;
|
|
217
|
+
reason: NotificationRecipient["reason"];
|
|
213
218
|
}) {
|
|
214
|
-
const actor = actorName(input.comment);
|
|
215
|
-
const app = appName();
|
|
216
|
-
const isReply = Boolean(input.comment.parentCommentId);
|
|
217
|
-
const subject = isReply
|
|
218
|
-
? `${actor} replied to a comment on "${input.planTitle}"`
|
|
219
|
-
: `${actor} commented on "${input.planTitle}"`;
|
|
220
219
|
const actionText =
|
|
221
|
-
input.
|
|
220
|
+
input.reason === "mention"
|
|
222
221
|
? "mentioned you in a comment on"
|
|
223
|
-
: input.
|
|
222
|
+
: input.reason === "plan-owner"
|
|
224
223
|
? "left a comment on your plan"
|
|
225
224
|
: "replied in a comment thread you participated in";
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
: input.
|
|
238
|
-
|
|
239
|
-
|
|
225
|
+
return {
|
|
226
|
+
subject: input.isReply
|
|
227
|
+
? `${input.actor} replied to a comment on "${input.planTitle}"`
|
|
228
|
+
: `${input.actor} commented on "${input.planTitle}"`,
|
|
229
|
+
...renderEmail({
|
|
230
|
+
preheader: `${input.actor} ${actionText} on ${input.app}.`,
|
|
231
|
+
heading: input.isReply
|
|
232
|
+
? "New reply on your plan"
|
|
233
|
+
: "New comment on your plan",
|
|
234
|
+
paragraphs: [
|
|
235
|
+
`${emailStrong(input.actor)} ${actionText} ${emailStrong(input.planTitle)}.`,
|
|
236
|
+
`Comment: "${commentExcerpt(input.message)}"`,
|
|
237
|
+
],
|
|
238
|
+
cta: { label: "Open plan", url: input.planUrl },
|
|
239
|
+
footer:
|
|
240
|
+
input.reason === "plan-owner"
|
|
241
|
+
? "You received this because you own this plan."
|
|
242
|
+
: input.reason === "mention"
|
|
243
|
+
? "You received this because you were mentioned in this comment."
|
|
244
|
+
: "You received this because you participated in this comment thread.",
|
|
245
|
+
}),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function sendPlanCommentNotification(input: {
|
|
250
|
+
recipient: NotificationRecipient;
|
|
251
|
+
comment: PlanComment;
|
|
252
|
+
planTitle: string;
|
|
253
|
+
planId: string;
|
|
254
|
+
}) {
|
|
255
|
+
await sendEmail({
|
|
256
|
+
...renderPlanCommentEmail({
|
|
257
|
+
actor: actorName(input.comment),
|
|
258
|
+
app: appName(),
|
|
259
|
+
planTitle: input.planTitle,
|
|
260
|
+
planUrl: planUrl(input.planId),
|
|
261
|
+
message: input.comment.message,
|
|
262
|
+
isReply: Boolean(input.comment.parentCommentId),
|
|
263
|
+
reason: input.recipient.reason,
|
|
264
|
+
}),
|
|
265
|
+
to: input.recipient.email,
|
|
266
|
+
templateId: PLAN_COMMENT_EMAIL_ID,
|
|
240
267
|
});
|
|
241
|
-
await sendEmail({ to: input.recipient.email, subject, html, text });
|
|
242
268
|
}
|
|
243
269
|
|
|
244
270
|
export async function notifyPlanCommentRecipients({
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog entries for the transactional emails Plan sends.
|
|
3
|
+
*
|
|
4
|
+
* Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
|
|
5
|
+
* and preview them without the app having sent anything yet.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
PLAN_ACCESS_REQUEST_EMAIL_ID,
|
|
12
|
+
renderPlanAccessRequestEmail,
|
|
13
|
+
} from "../../actions/request-plan-access.js";
|
|
14
|
+
import {
|
|
15
|
+
PLAN_COMMENT_EMAIL_ID,
|
|
16
|
+
renderPlanCommentEmail,
|
|
17
|
+
} from "./comment-notifications.js";
|
|
18
|
+
|
|
19
|
+
let registered = false;
|
|
20
|
+
|
|
21
|
+
export function registerPlanEmails(): void {
|
|
22
|
+
if (registered) return;
|
|
23
|
+
registered = true;
|
|
24
|
+
|
|
25
|
+
defineTransactionalEmail({
|
|
26
|
+
id: PLAN_COMMENT_EMAIL_ID,
|
|
27
|
+
name: "Plan comment",
|
|
28
|
+
trigger:
|
|
29
|
+
"A human (never the agent) posts a comment or reply on a plan or recap. The subject changes for a reply and the body changes with why the recipient was picked; all variants send under this id.",
|
|
30
|
+
recipientLabel: "Owner, mentions, authors",
|
|
31
|
+
recipient:
|
|
32
|
+
"The plan owner, everyone mentioned in the comment, and — for a reply — every other human author in the same thread. The comment's own author, synthetic QA addresses, and the owner when the comment mentions the source author are all dropped. One email per remaining address.",
|
|
33
|
+
senderLabel: "Default sender",
|
|
34
|
+
sender:
|
|
35
|
+
"The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`.",
|
|
36
|
+
preview: () =>
|
|
37
|
+
renderPlanCommentEmail({
|
|
38
|
+
actor: "Sam Rivera",
|
|
39
|
+
app: "Agent-Native Plan",
|
|
40
|
+
planTitle: "Checkout rewrite",
|
|
41
|
+
planUrl: "https://example.com/plans/plan_sample",
|
|
42
|
+
message: "Can we split the migration into two deploys?",
|
|
43
|
+
isReply: false,
|
|
44
|
+
reason: "plan-owner",
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
defineTransactionalEmail({
|
|
49
|
+
id: PLAN_ACCESS_REQUEST_EMAIL_ID,
|
|
50
|
+
name: "Plan access request",
|
|
51
|
+
trigger:
|
|
52
|
+
"A signed-in user who cannot resolve access calls `request-plan-access` on a private plan URL. Skipped when email is not configured or the requester is the owner.",
|
|
53
|
+
recipientLabel: "Plan owner",
|
|
54
|
+
recipient:
|
|
55
|
+
"The plan's `ownerEmail` column. Anonymous public viewers and guest author identities cannot reach this path.",
|
|
56
|
+
senderLabel: "Default sender",
|
|
57
|
+
sender:
|
|
58
|
+
"The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`, so the owner cannot reply straight to the requester.",
|
|
59
|
+
preview: () =>
|
|
60
|
+
renderPlanAccessRequestEmail({
|
|
61
|
+
requesterName: "Sam Rivera",
|
|
62
|
+
requesterEmail: "sam.rivera@example.com",
|
|
63
|
+
planTitle: "Checkout rewrite",
|
|
64
|
+
url: "https://example.com/plans/plan_sample",
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -6,6 +6,7 @@ export default defineAction({
|
|
|
6
6
|
description:
|
|
7
7
|
"Analyze a website URL to extract design tokens (colors, fonts, metadata) " +
|
|
8
8
|
"for use in creating or updating a design project. " +
|
|
9
|
+
"Follows bounded, SSRF-safe linked stylesheets and reports any stylesheet failures. " +
|
|
9
10
|
"Returns extracted CSS variables, font faces, colors, and meta information.",
|
|
10
11
|
schema: z.object({
|
|
11
12
|
url: z.string().describe("Website URL to analyze"),
|